mulmocast 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,7 @@
55
55
 
56
56
  // === MulmoAnimation Helper Class ===
57
57
 
58
- const TRANSFORM_PROPS = { translateX: 'px', translateY: 'px', scale: '', rotate: 'deg' };
58
+ const TRANSFORM_PROPS = { translateX: 'px', translateY: 'px', scale: '', rotate: 'deg', rotateX: 'deg', rotateY: 'deg', rotateZ: 'deg' };
59
59
  const SVG_PROPS = ['r', 'cx', 'cy', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'rx', 'ry',
60
60
  'width', 'height', 'stroke-width', 'stroke-dashoffset', 'stroke-dasharray', 'opacity'];
61
61
 
@@ -176,7 +176,7 @@
176
176
 
177
177
  if (entry.kind === 'animate') {
178
178
  const startFrame = (opts.start || 0) * fps;
179
- const endFrame = (opts.end || 0) * fps;
179
+ const endFrame = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
180
180
  const progress = Math.max(0, Math.min(1, endFrame === startFrame ? 1 : (frame - startFrame) / (endFrame - startFrame)));
181
181
  const el = document.querySelector(entry.selector);
182
182
  this._applyProps(el, entry.props, progress, easingFn);
@@ -196,7 +196,7 @@
196
196
 
197
197
  } else if (entry.kind === 'typewriter') {
198
198
  const twStart = (opts.start || 0) * fps;
199
- const twEnd = (opts.end || 0) * fps;
199
+ const twEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
200
200
  const twProgress = Math.max(0, Math.min(1, twEnd === twStart ? 1 : (frame - twStart) / (twEnd - twStart)));
201
201
  const charCount = Math.floor(twProgress * entry.text.length);
202
202
  const twEl = document.querySelector(entry.selector);
@@ -204,7 +204,7 @@
204
204
 
205
205
  } else if (entry.kind === 'counter') {
206
206
  const cStart = (opts.start || 0) * fps;
207
- const cEnd = (opts.end || 0) * fps;
207
+ const cEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
208
208
  const cProgress = Math.max(0, Math.min(1, cEnd === cStart ? 1 : (frame - cStart) / (cEnd - cStart)));
209
209
  const cVal = entry.range[0] + easingFn(cProgress) * (entry.range[1] - entry.range[0]);
210
210
  const decimals = opts.decimals || 0;
@@ -214,7 +214,7 @@
214
214
 
215
215
  } else if (entry.kind === 'codeReveal') {
216
216
  const crStart = (opts.start || 0) * fps;
217
- const crEnd = (opts.end || 0) * fps;
217
+ const crEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
218
218
  const crProgress = Math.max(0, Math.min(1, crEnd === crStart ? 1 : (frame - crStart) / (crEnd - crStart)));
219
219
  const lineCount = Math.floor(crProgress * entry.lines.length);
220
220
  const crEl = document.querySelector(entry.selector);
@@ -41,6 +41,7 @@ type ImageHtmlPreprocessAgentResponse = {
41
41
  };
42
42
  type ImageOnlyMoviePreprocessAgentResponse = ImagePreprocessAgentResponseBase & {
43
43
  imageFromMovie: boolean;
44
+ useLastFrame?: boolean;
44
45
  };
45
46
  type ImagePluginPreprocessAgentResponse = ImagePreprocessAgentResponseBase & {
46
47
  referenceImageForMovie: string;
@@ -85,6 +85,7 @@ export const imagePreprocessAgent = async (namedInputs) => {
85
85
  imagePath, // for thumbnail extraction
86
86
  movieFile: animatedVideoPath, // .mp4 path for the pipeline
87
87
  imageFromMovie: true, // triggers extractImageFromMovie
88
+ useLastFrame: true, // extract last frame for PDF/static (animation complete state)
88
89
  referenceImageForMovie: pluginPath,
89
90
  markdown,
90
91
  html,
@@ -122,7 +123,9 @@ export const imagePluginAgent = async (namedInputs) => {
122
123
  const effectiveImagePath = isAnimatedHtml ? getBeatAnimatedVideoPath(context, index) : imagePath;
123
124
  try {
124
125
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, true);
125
- const processorParams = { beat, context, imagePath: effectiveImagePath, imageRefs, ...htmlStyle(context, beat) };
126
+ const studioBeat = context.studio.beats[index];
127
+ const beatDuration = beat.duration ?? studioBeat?.duration;
128
+ const processorParams = { beat, context, imagePath: effectiveImagePath, imageRefs, beatDuration, ...htmlStyle(context, beat) };
126
129
  await plugin.process(processorParams);
127
130
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, false);
128
131
  }
@@ -118,6 +118,7 @@ export declare const beat_graph_data: {
118
118
  imagePath?: string;
119
119
  } & {
120
120
  imageFromMovie: boolean;
121
+ useLastFrame?: boolean;
121
122
  }) | ({
122
123
  imageParams?: MulmoImageParams;
123
124
  movieFile?: string;
@@ -286,11 +287,13 @@ export declare const beat_graph_data: {
286
287
  agent: (namedInputs: {
287
288
  movieFile: string;
288
289
  imageFile: string;
290
+ useLastFrame: boolean;
289
291
  }) => Promise<object>;
290
292
  inputs: {
291
293
  onComplete: string[];
292
294
  imageFile: string;
293
295
  movieFile: string;
296
+ useLastFrame: string;
294
297
  };
295
298
  defaultValue: {};
296
299
  };
@@ -187,12 +187,13 @@ export const beat_graph_data = {
187
187
  imageFromMovie: {
188
188
  if: ":preprocessor.imageFromMovie",
189
189
  agent: async (namedInputs) => {
190
- return await extractImageFromMovie(namedInputs.movieFile, namedInputs.imageFile);
190
+ return await extractImageFromMovie(namedInputs.movieFile, namedInputs.imageFile, namedInputs.useLastFrame);
191
191
  },
192
192
  inputs: {
193
193
  onComplete: [":movieGenerator", ":imagePlugin"], // :imagePlugin for animated html_tailwind video generation
194
194
  imageFile: ":preprocessor.imagePath",
195
195
  movieFile: ":preprocessor.movieFile",
196
+ useLastFrame: ":preprocessor.useLastFrame",
196
197
  },
197
198
  defaultValue: {},
198
199
  },
@@ -84,6 +84,7 @@ export type ImageProcessorParams = {
84
84
  textSlideStyle: string;
85
85
  canvasSize: MulmoCanvasDimension;
86
86
  imageRefs?: Record<string, string>;
87
+ beatDuration?: number;
87
88
  };
88
89
  export type PDFMode = (typeof pdf_modes)[number];
89
90
  export type PDFSize = (typeof pdf_sizes)[number];
@@ -25,7 +25,7 @@ export declare const ffmpegGetMediaDuration: (filePath: string) => Promise<{
25
25
  duration: number;
26
26
  hasAudio: boolean;
27
27
  }>;
28
- export declare const extractImageFromMovie: (movieFile: string, imagePath: string) => Promise<object>;
28
+ export declare const extractImageFromMovie: (movieFile: string, imagePath: string, useLastFrame?: boolean) => Promise<object>;
29
29
  export declare const trimMusic: (inputFile: string, startTime: number, duration: number) => Promise<Buffer>;
30
30
  export declare const createSilentAudio: (filePath: string, durationSec: number) => Promise<void>;
31
31
  export declare const pcmToMp3: (rawPcm: Buffer, sampleRate?: number) => Promise<Buffer>;
@@ -117,9 +117,13 @@ export const ffmpegGetMediaDuration = (filePath) => {
117
117
  });
118
118
  });
119
119
  };
120
- export const extractImageFromMovie = (movieFile, imagePath) => {
120
+ export const extractImageFromMovie = (movieFile, imagePath, useLastFrame = false) => {
121
121
  return new Promise((resolve, reject) => {
122
- ffmpeg(movieFile)
122
+ const command = ffmpeg(movieFile);
123
+ if (useLastFrame) {
124
+ command.inputOptions(["-sseof", "-0.1"]);
125
+ }
126
+ command
123
127
  .outputOptions(["-frames:v 1"])
124
128
  .output(imagePath)
125
129
  .on("end", () => resolve({}))
@@ -34,9 +34,9 @@ const processHtmlTailwindAnimated = async (params) => {
34
34
  const animConfig = getAnimationConfig(params);
35
35
  if (!animConfig)
36
36
  return;
37
- const duration = beat.duration;
37
+ const duration = params.beatDuration ?? beat.duration;
38
38
  if (duration === undefined) {
39
- throw new Error("html_tailwind animation requires explicit beat.duration. Set duration in the beat definition.");
39
+ throw new Error("html_tailwind animation requires beat.duration or audio-derived duration. Set duration in the beat or ensure audio is generated first.");
40
40
  }
41
41
  const fps = animConfig.fps;
42
42
  const totalFrames = Math.floor(duration * fps);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -546,6 +546,83 @@
546
546
  "animation": { "fps": 24 }
547
547
  }
548
548
  },
549
+ {
550
+ "id": "demo_3d_card_flip",
551
+ "speaker": "Presenter",
552
+ "duration": 3,
553
+ "image": {
554
+ "type": "html_tailwind",
555
+ "html": [
556
+ "<div class='h-full flex items-center justify-center bg-gradient-to-br from-slate-900 to-indigo-950'>",
557
+ " <div style='perspective:1000px'>",
558
+ " <div id='card' class='relative' style='width:340px;height:200px;transform-style:preserve-3d'>",
559
+ " <div class='absolute inset-0 rounded-2xl flex flex-col items-center justify-center' style='backface-visibility:hidden;background:linear-gradient(135deg,#3b82f6,#06b6d4);box-shadow:0 20px 60px rgba(6,182,212,0.3)'>",
560
+ " <p class='text-white text-3xl font-bold tracking-wide'>MulmoCast</p>",
561
+ " <p class='text-blue-200 text-sm mt-2 tracking-wider'>FRONT SIDE</p>",
562
+ " </div>",
563
+ " <div class='absolute inset-0 rounded-2xl flex flex-col items-center justify-center' style='backface-visibility:hidden;transform:rotateY(180deg);background:linear-gradient(135deg,#8b5cf6,#ec4899);box-shadow:0 20px 60px rgba(139,92,246,0.3)'>",
564
+ " <p class='text-white text-3xl font-bold tracking-wide'>AI-Native</p>",
565
+ " <p class='text-purple-200 text-sm mt-2 tracking-wider'>BACK SIDE</p>",
566
+ " </div>",
567
+ " </div>",
568
+ " </div>",
569
+ "</div>"
570
+ ],
571
+ "script": [
572
+ "const animation = new MulmoAnimation();",
573
+ "animation.animate('#card', { rotateY: [0, 180] }, { start: 0.5, end: 2.5, easing: 'easeInOut' });"
574
+ ],
575
+ "animation": true
576
+ }
577
+ },
578
+ {
579
+ "id": "demo_3d_title_reveal",
580
+ "speaker": "Presenter",
581
+ "duration": 3,
582
+ "image": {
583
+ "type": "html_tailwind",
584
+ "html": [
585
+ "<div class='h-full flex flex-col items-center justify-center bg-black' style='perspective:800px'>",
586
+ " <h1 id='title' class='text-7xl font-bold tracking-wider' style='opacity:0;font-family:Impact,sans-serif;color:white;text-shadow:0 0 40px rgba(6,182,212,0.5)'>CINEMATIC</h1>",
587
+ " <div id='line' class='h-0.5 mt-6 rounded' style='width:0;background:linear-gradient(90deg,transparent,#06b6d4,transparent)'></div>",
588
+ " <p id='sub' class='text-lg mt-6 tracking-[0.4em]' style='opacity:0;color:#64748b;font-family:monospace'>3D PERSPECTIVE REVEAL</p>",
589
+ "</div>"
590
+ ],
591
+ "script": [
592
+ "const animation = new MulmoAnimation();",
593
+ "animation.animate('#title', { opacity: [0, 1], rotateX: [90, 0] }, { start: 0.2, end: 1.2, easing: 'easeOut' });",
594
+ "animation.animate('#line', { width: [0, 400, 'px'] }, { start: 1.0, end: 1.8, easing: 'easeOut' });",
595
+ "animation.animate('#sub', { opacity: [0, 1] }, { start: 1.5, end: 2.2, easing: 'easeOut' });"
596
+ ],
597
+ "animation": true
598
+ }
599
+ },
600
+ {
601
+ "id": "demo_split_reveal",
602
+ "speaker": "Presenter",
603
+ "duration": 3,
604
+ "image": {
605
+ "type": "html_tailwind",
606
+ "html": [
607
+ "<div class='h-full flex bg-black overflow-hidden'>",
608
+ " <div id='left' class='flex-1 flex items-center justify-center' style='background:linear-gradient(135deg,#1e3a5f,#0f172a);opacity:0'>",
609
+ " <p class='text-6xl font-bold text-white' style='font-family:Georgia,serif'>Create</p>",
610
+ " </div>",
611
+ " <div id='divider' class='w-1' style='background:linear-gradient(to bottom,transparent,#06b6d4,transparent);opacity:0'></div>",
612
+ " <div id='right' class='flex-1 flex items-center justify-center' style='background:linear-gradient(225deg,#4c1d95,#0f172a);opacity:0'>",
613
+ " <p class='text-6xl font-bold text-white' style='font-family:Georgia,serif'>Inspire</p>",
614
+ " </div>",
615
+ "</div>"
616
+ ],
617
+ "script": [
618
+ "const animation = new MulmoAnimation();",
619
+ "animation.animate('#left', { translateX: [-640, 0], opacity: [0, 1] }, { start: 0, end: 1.0, easing: 'easeOut' });",
620
+ "animation.animate('#right', { translateX: [640, 0], opacity: [0, 1] }, { start: 0.3, end: 1.3, easing: 'easeOut' });",
621
+ "animation.animate('#divider', { opacity: [0, 1] }, { start: 1.2, end: 1.8 });"
622
+ ],
623
+ "animation": true
624
+ }
625
+ },
549
626
  {
550
627
  "speaker": "Presenter",
551
628
  "duration": 2,