mulmocast 2.4.8 → 2.5.0

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.
@@ -1,259 +1,40 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en" class="h-full">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <script src="https://cdn.tailwindcss.com"></script>
7
- <style>
8
- /* Disable all CSS animations/transitions for deterministic frame-based rendering */
9
- *, *::before, *::after {
10
- animation-play-state: paused !important;
11
- transition: none !important;
12
- }
13
- ${custom_style}
14
- </style>
15
- </head>
16
- <body class="bg-white text-gray-800 h-full flex flex-col">
17
- ${html_body}
18
-
19
- <script>
20
- // === MulmoCast Animation Helpers ===
21
-
22
- /**
23
- * Easing functions for non-linear interpolation.
24
- */
25
- const Easing = {
26
- linear: (t) => t,
27
- easeIn: (t) => t * t,
28
- easeOut: (t) => 1 - (1 - t) * (1 - t),
29
- easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
30
- };
31
-
32
- /**
33
- * Interpolation with clamping and optional easing.
34
- *
35
- * @param {number} value - Current value (typically frame number)
36
- * @param {Object} opts - { input: { inMin, inMax }, output: { outMin, outMax }, easing?: string | function }
37
- * @returns {number} Interpolated and clamped value
38
- *
39
- * @example
40
- * interpolate(frame, { input: { inMin: 0, inMax: 30 }, output: { outMin: 0, outMax: 1 } })
41
- * interpolate(frame, { input: { inMin: 0, inMax: 30 }, output: { outMin: 0, outMax: 1 }, easing: 'easeOut' })
42
- */
43
- function interpolate(value, opts) {
44
- const { inMin, inMax } = opts.input;
45
- const { outMin, outMax } = opts.output;
46
- if (inMax === inMin) {
47
- return outMin;
48
- }
49
- const easing = !opts.easing ? Easing.linear
50
- : typeof opts.easing === 'function' ? opts.easing
51
- : Easing[opts.easing] || Easing.linear;
52
- const progress = Math.max(0, Math.min(1, (value - inMin) / (inMax - inMin)));
53
- return outMin + easing(progress) * (outMax - outMin);
54
- }
55
-
56
- // === MulmoAnimation Helper Class ===
57
-
58
- const TRANSFORM_PROPS = { translateX: 'px', translateY: 'px', scale: '', rotate: 'deg', rotateX: 'deg', rotateY: 'deg', rotateZ: 'deg' };
59
- const SVG_PROPS = ['r', 'cx', 'cy', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'rx', 'ry',
60
- 'width', 'height', 'stroke-width', 'stroke-dashoffset', 'stroke-dasharray', 'opacity'];
61
-
62
- function MulmoAnimation() {
63
- this._entries = [];
64
- }
65
-
66
- /**
67
- * Register a property animation on a single element.
68
- * @param {string} selector - CSS selector (e.g. '#title')
69
- * @param {Object} props - { opacity: [0, 1], translateY: [30, 0], width: [0, 80, '%'] }
70
- * @param {Object} opts - { start, end, easing } (start/end in seconds)
71
- */
72
- MulmoAnimation.prototype.animate = function(selector, props, opts) {
73
- this._entries.push({ kind: 'animate', selector, props, opts: opts || {} });
74
- return this;
75
- };
76
-
77
- /**
78
- * Stagger animation across numbered elements.
79
- * Selector must contain {i} placeholder (e.g. '#item{i}').
80
- * @param {string} selector - e.g. '#item{i}'
81
- * @param {number} count - number of elements (0-indexed)
82
- * @param {Object} props - same as animate()
83
- * @param {Object} opts - { start, stagger, duration, easing }
84
- */
85
- MulmoAnimation.prototype.stagger = function(selector, count, props, opts) {
86
- this._entries.push({ kind: 'stagger', selector, count, props, opts: opts || {} });
87
- return this;
88
- };
89
-
90
- /**
91
- * Typewriter effect — reveal text character by character.
92
- * @param {string} selector - target element selector
93
- * @param {string} text - full text to reveal
94
- * @param {Object} opts - { start, end }
95
- */
96
- MulmoAnimation.prototype.typewriter = function(selector, text, opts) {
97
- this._entries.push({ kind: 'typewriter', selector, text, opts: opts || {} });
98
- return this;
99
- };
100
-
101
- /**
102
- * Animated counter — interpolate a number and display with optional prefix/suffix.
103
- * @param {string} selector - target element selector
104
- * @param {[number, number]} range - [from, to]
105
- * @param {Object} opts - { start, end, prefix, suffix, decimals }
106
- */
107
- MulmoAnimation.prototype.counter = function(selector, range, opts) {
108
- this._entries.push({ kind: 'counter', selector, range, opts: opts || {} });
109
- return this;
110
- };
111
-
112
- /**
113
- * Code reveal — show lines of code one by one (line-level typewriter).
114
- * @param {string} selector - target element selector
115
- * @param {string[]} lines - array of code lines
116
- * @param {Object} opts - { start, end }
117
- */
118
- MulmoAnimation.prototype.codeReveal = function(selector, lines, opts) {
119
- this._entries.push({ kind: 'codeReveal', selector, lines, opts: opts || {} });
120
- return this;
121
- };
122
-
123
- /**
124
- * Blink — periodic show/hide toggle (e.g. cursor blinking).
125
- * @param {string} selector - target element selector
126
- * @param {Object} opts - { interval } (half-cycle seconds, default 0.5)
127
- */
128
- MulmoAnimation.prototype.blink = function(selector, opts) {
129
- this._entries.push({ kind: 'blink', selector, opts: opts || {} });
130
- return this;
131
- };
132
-
133
- /** Resolve easing name string or function to an easing function */
134
- MulmoAnimation.prototype._resolveEasing = function(e) {
135
- if (!e) return Easing.linear;
136
- if (typeof e === 'function') return e;
137
- return Easing[e] || Easing.linear;
138
- };
139
-
140
- /** Apply props to element at a given progress (0-1) with easing */
141
- MulmoAnimation.prototype._applyProps = function(el, props, progress, easingFn) {
142
- if (!el) return;
143
- const transforms = [];
144
- Object.keys(props).forEach((prop) => {
145
- const spec = props[prop];
146
- const from = spec[0], to = spec[1];
147
- const unit = (spec.length > 2) ? spec[2] : null;
148
- const val = from + easingFn(progress) * (to - from);
149
-
150
- if (TRANSFORM_PROPS.hasOwnProperty(prop)) {
151
- const tUnit = unit || TRANSFORM_PROPS[prop];
152
- transforms.push(prop === 'scale' ? 'scale(' + val + ')' : prop + '(' + val + tUnit + ')');
153
- } else if (el instanceof SVGElement && SVG_PROPS.indexOf(prop) !== -1) {
154
- el.setAttribute(prop, val);
155
- } else if (prop === 'opacity') {
156
- el.style.opacity = val;
157
- } else {
158
- const cssUnit = unit || 'px';
159
- el.style[prop] = val + cssUnit;
160
- }
161
- });
162
- if (transforms.length > 0) {
163
- el.style.transform = transforms.join(' ');
164
- }
165
- };
166
-
167
- /**
168
- * Update all registered animations for the given frame.
169
- * @param {number} frame - current frame number
170
- * @param {number} fps - frames per second
171
- */
172
- MulmoAnimation.prototype.update = function(frame, fps) {
173
- this._entries.forEach((entry) => {
174
- const opts = entry.opts;
175
- const easingFn = this._resolveEasing(opts.easing);
176
-
177
- if (entry.kind === 'animate') {
178
- const startFrame = (opts.start || 0) * fps;
179
- const endFrame = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
180
- const progress = Math.max(0, Math.min(1, endFrame === startFrame ? 1 : (frame - startFrame) / (endFrame - startFrame)));
181
- const el = document.querySelector(entry.selector);
182
- this._applyProps(el, entry.props, progress, easingFn);
183
-
184
- } else if (entry.kind === 'stagger') {
185
- const baseStart = (opts.start || 0) * fps;
186
- const staggerDelay = (opts.stagger || 0.2) * fps;
187
- const dur = (opts.duration || 0.5) * fps;
188
- for (let j = 0; j < entry.count; j++) {
189
- const sel = entry.selector.replace(/\{i\}/g, j);
190
- const sEl = document.querySelector(sel);
191
- const sStart = baseStart + j * staggerDelay;
192
- const sEnd = sStart + dur;
193
- const sProgress = Math.max(0, Math.min(1, sEnd === sStart ? 1 : (frame - sStart) / (sEnd - sStart)));
194
- this._applyProps(sEl, entry.props, sProgress, easingFn);
195
- }
196
-
197
- } else if (entry.kind === 'typewriter') {
198
- const twStart = (opts.start || 0) * fps;
199
- const twEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
200
- const twProgress = Math.max(0, Math.min(1, twEnd === twStart ? 1 : (frame - twStart) / (twEnd - twStart)));
201
- const charCount = Math.floor(twProgress * entry.text.length);
202
- const twEl = document.querySelector(entry.selector);
203
- if (twEl) twEl.textContent = entry.text.substring(0, charCount);
204
-
205
- } else if (entry.kind === 'counter') {
206
- const cStart = (opts.start || 0) * fps;
207
- const cEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
208
- const cProgress = Math.max(0, Math.min(1, cEnd === cStart ? 1 : (frame - cStart) / (cEnd - cStart)));
209
- const cVal = entry.range[0] + easingFn(cProgress) * (entry.range[1] - entry.range[0]);
210
- const decimals = opts.decimals || 0;
211
- const display = (opts.prefix || '') + cVal.toFixed(decimals) + (opts.suffix || '');
212
- const cEl = document.querySelector(entry.selector);
213
- if (cEl) cEl.textContent = display;
214
-
215
- } else if (entry.kind === 'codeReveal') {
216
- const crStart = (opts.start || 0) * fps;
217
- const crEnd = (opts.end === 'auto' ? window.__MULMO.totalFrames / fps : (opts.end || 0)) * fps;
218
- const crProgress = Math.max(0, Math.min(1, crEnd === crStart ? 1 : (frame - crStart) / (crEnd - crStart)));
219
- const lineCount = Math.floor(crProgress * entry.lines.length);
220
- const crEl = document.querySelector(entry.selector);
221
- if (crEl) crEl.textContent = entry.lines.slice(0, lineCount).join('\n');
222
-
223
- } else if (entry.kind === 'blink') {
224
- const interval_s = opts.interval || 0.5;
225
- const blinkEl = document.querySelector(entry.selector);
226
- if (blinkEl) {
227
- const cycle = (frame / fps) / interval_s;
228
- blinkEl.style.opacity = (Math.floor(cycle) % 2 === 0) ? 1 : 0;
229
- }
230
- }
231
- });
232
- };
233
-
234
- // === MulmoCast Frame State (updated by Puppeteer per frame) ===
235
- window.__MULMO = {
236
- frame: 0,
237
- totalFrames: ${totalFrames},
238
- fps: ${fps},
239
- };
240
- </script>
241
-
242
- ${user_script}
243
-
244
- <script>
245
- // Auto-render: if MulmoAnimation is used but render() is not defined, generate it
246
- if (typeof render !== 'function' && typeof animation !== 'undefined' && animation instanceof MulmoAnimation) {
247
- window.render = function(frame, totalFrames, fps) { animation.update(frame, fps); };
248
- }
249
-
250
- // Initial render (frame 0)
251
- if (typeof render === 'function') {
252
- const result = render(0, window.__MULMO.totalFrames, window.__MULMO.fps);
253
- if (result && typeof result.then === 'function') {
254
- result.catch(console.error);
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <style>
8
+ /* Disable all CSS animations/transitions for deterministic frame-based rendering */
9
+ *, *::before, *::after {
10
+ animation-play-state: paused !important;
11
+ transition: none !important;
255
12
  }
256
- }
257
- </script>
258
- </body>
13
+ ${custom_style}
14
+ </style>
15
+ </head>
16
+ <body class="bg-white text-gray-800 h-full flex flex-col">
17
+ ${html_body}
18
+
19
+ <script>
20
+ ${animation_runtime}
21
+
22
+ // === MulmoCast Frame State (updated by Puppeteer per frame) ===
23
+ window.__MULMO = {
24
+ frame: 0,
25
+ totalFrames: ${totalFrames},
26
+ fps: ${fps},
27
+ };
28
+ </script>
29
+
30
+ ${user_script}
31
+
32
+ <script>
33
+ ${data_attribute_registration}
34
+ </script>
35
+
36
+ <script>
37
+ ${auto_render}
38
+ </script>
39
+ </body>
259
40
  </html>
@@ -33,7 +33,7 @@ export const generateReferenceImage = async (inputs) => {
33
33
  },
34
34
  params: {
35
35
  model: imageAgentInfo.imageParams.model,
36
- canvasSize: context.presentationStyle.canvasSize,
36
+ canvasSize: image.canvasSize ?? context.presentationStyle.canvasSize,
37
37
  },
38
38
  },
39
39
  },
@@ -1,11 +1,13 @@
1
1
  import { MulmoBeat } from "../types/index.js";
2
2
  type AnimationConfig = {
3
3
  fps?: number;
4
+ movie?: boolean;
4
5
  };
5
6
  export declare const MulmoBeatMethods: {
6
7
  isAnimationEnabled: (animation: unknown) => animation is true | AnimationConfig;
7
8
  isAnimationObject: (animation: unknown) => animation is AnimationConfig;
8
9
  isAnimatedHtmlTailwind: (beat: MulmoBeat) => boolean;
10
+ isMovieMode: (animation: unknown) => boolean;
9
11
  getHtmlPrompt(beat: MulmoBeat): string | undefined;
10
12
  getPlugin(beat: MulmoBeat): {
11
13
  imageType: string;
@@ -7,6 +7,10 @@ const isAnimationObject = (animation) => {
7
7
  const isAnimationEnabled = (animation) => {
8
8
  return animation === true || isAnimationObject(animation);
9
9
  };
10
+ /** Check if movie mode (CDP screencast) is enabled */
11
+ const isMovieMode = (animation) => {
12
+ return isAnimationObject(animation) && animation.movie === true;
13
+ };
10
14
  /** Check if a beat has html_tailwind animation enabled */
11
15
  const isAnimatedHtmlTailwind = (beat) => {
12
16
  if (!beat.image || beat.image.type !== "html_tailwind")
@@ -18,6 +22,7 @@ export const MulmoBeatMethods = {
18
22
  isAnimationEnabled,
19
23
  isAnimationObject,
20
24
  isAnimatedHtmlTailwind,
25
+ isMovieMode,
21
26
  getHtmlPrompt(beat) {
22
27
  if (beat?.htmlPrompt?.data) {
23
28
  return beat.htmlPrompt.prompt + "\n\n[data]\n" + JSON.stringify(beat.htmlPrompt.data, null, 2);
@@ -374,6 +374,7 @@ export declare const mulmoMermaidMediaSchema: z.ZodObject<{
374
374
  }, z.core.$strict>;
375
375
  export declare const htmlTailwindAnimationSchema: z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
376
376
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
377
+ movie: z.ZodOptional<z.ZodBoolean>;
377
378
  }, z.core.$strip>]>;
378
379
  export declare const mulmoHtmlTailwindMediaSchema: z.ZodObject<{
379
380
  type: z.ZodLiteral<"html_tailwind">;
@@ -381,6 +382,7 @@ export declare const mulmoHtmlTailwindMediaSchema: z.ZodObject<{
381
382
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
382
383
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
383
384
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
385
+ movie: z.ZodOptional<z.ZodBoolean>;
384
386
  }, z.core.$strip>]>>;
385
387
  }, z.core.$strict>;
386
388
  export declare const mulmoBeatReferenceMediaSchema: z.ZodObject<{
@@ -553,6 +555,7 @@ export declare const mulmoImageAssetSchema: z.ZodUnion<readonly [z.ZodObject<{
553
555
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
554
556
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
555
557
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
558
+ movie: z.ZodOptional<z.ZodBoolean>;
556
559
  }, z.core.$strip>]>>;
557
560
  }, z.core.$strict>, z.ZodObject<{
558
561
  type: z.ZodLiteral<"beat">;
@@ -2954,6 +2957,10 @@ export declare const mulmoAudioAssetSchema: z.ZodUnion<readonly [z.ZodObject<{
2954
2957
  export declare const mulmoImagePromptMediaSchema: z.ZodObject<{
2955
2958
  type: z.ZodLiteral<"imagePrompt">;
2956
2959
  prompt: z.ZodString;
2960
+ canvasSize: z.ZodOptional<z.ZodObject<{
2961
+ width: z.ZodNumber;
2962
+ height: z.ZodNumber;
2963
+ }, z.core.$strict>>;
2957
2964
  }, z.core.$strict>;
2958
2965
  export declare const mulmoImageParamsImagesValueSchema: z.ZodUnion<readonly [z.ZodObject<{
2959
2966
  type: z.ZodLiteral<"image">;
@@ -2970,6 +2977,10 @@ export declare const mulmoImageParamsImagesValueSchema: z.ZodUnion<readonly [z.Z
2970
2977
  }, z.core.$strict>, z.ZodObject<{
2971
2978
  type: z.ZodLiteral<"imagePrompt">;
2972
2979
  prompt: z.ZodString;
2980
+ canvasSize: z.ZodOptional<z.ZodObject<{
2981
+ width: z.ZodNumber;
2982
+ height: z.ZodNumber;
2983
+ }, z.core.$strict>>;
2973
2984
  }, z.core.$strict>]>;
2974
2985
  export declare const mulmoImageParamsImagesSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
2975
2986
  type: z.ZodLiteral<"image">;
@@ -2986,6 +2997,10 @@ export declare const mulmoImageParamsImagesSchema: z.ZodRecord<z.ZodString, z.Zo
2986
2997
  }, z.core.$strict>, z.ZodObject<{
2987
2998
  type: z.ZodLiteral<"imagePrompt">;
2988
2999
  prompt: z.ZodString;
3000
+ canvasSize: z.ZodOptional<z.ZodObject<{
3001
+ width: z.ZodNumber;
3002
+ height: z.ZodNumber;
3003
+ }, z.core.$strict>>;
2989
3004
  }, z.core.$strict>]>>;
2990
3005
  export declare const mulmoFillOptionSchema: z.ZodObject<{
2991
3006
  style: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
@@ -3054,6 +3069,10 @@ export declare const mulmoImageParamsSchema: z.ZodObject<{
3054
3069
  }, z.core.$strict>, z.ZodObject<{
3055
3070
  type: z.ZodLiteral<"imagePrompt">;
3056
3071
  prompt: z.ZodString;
3072
+ canvasSize: z.ZodOptional<z.ZodObject<{
3073
+ width: z.ZodNumber;
3074
+ height: z.ZodNumber;
3075
+ }, z.core.$strict>>;
3057
3076
  }, z.core.$strict>]>>>;
3058
3077
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
3059
3078
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -3606,6 +3625,7 @@ export declare const mulmoBeatSchema: z.ZodObject<{
3606
3625
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
3607
3626
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
3608
3627
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
3628
+ movie: z.ZodOptional<z.ZodBoolean>;
3609
3629
  }, z.core.$strip>]>>;
3610
3630
  }, z.core.$strict>, z.ZodObject<{
3611
3631
  type: z.ZodLiteral<"beat">;
@@ -6401,6 +6421,10 @@ export declare const mulmoPresentationStyleSchema: z.ZodObject<{
6401
6421
  }, z.core.$strict>, z.ZodObject<{
6402
6422
  type: z.ZodLiteral<"imagePrompt">;
6403
6423
  prompt: z.ZodString;
6424
+ canvasSize: z.ZodOptional<z.ZodObject<{
6425
+ width: z.ZodNumber;
6426
+ height: z.ZodNumber;
6427
+ }, z.core.$strict>>;
6404
6428
  }, z.core.$strict>]>>>;
6405
6429
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
6406
6430
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -6857,6 +6881,10 @@ export declare const mulmoScriptSchema: z.ZodObject<{
6857
6881
  }, z.core.$strict>, z.ZodObject<{
6858
6882
  type: z.ZodLiteral<"imagePrompt">;
6859
6883
  prompt: z.ZodString;
6884
+ canvasSize: z.ZodOptional<z.ZodObject<{
6885
+ width: z.ZodNumber;
6886
+ height: z.ZodNumber;
6887
+ }, z.core.$strict>>;
6860
6888
  }, z.core.$strict>]>>>;
6861
6889
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
6862
6890
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -7404,6 +7432,7 @@ export declare const mulmoScriptSchema: z.ZodObject<{
7404
7432
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
7405
7433
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
7406
7434
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
7435
+ movie: z.ZodOptional<z.ZodBoolean>;
7407
7436
  }, z.core.$strip>]>>;
7408
7437
  }, z.core.$strict>, z.ZodObject<{
7409
7438
  type: z.ZodLiteral<"beat">;
@@ -10274,6 +10303,10 @@ export declare const mulmoStudioSchema: z.ZodObject<{
10274
10303
  }, z.core.$strict>, z.ZodObject<{
10275
10304
  type: z.ZodLiteral<"imagePrompt">;
10276
10305
  prompt: z.ZodString;
10306
+ canvasSize: z.ZodOptional<z.ZodObject<{
10307
+ width: z.ZodNumber;
10308
+ height: z.ZodNumber;
10309
+ }, z.core.$strict>>;
10277
10310
  }, z.core.$strict>]>>>;
10278
10311
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
10279
10312
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -10821,6 +10854,7 @@ export declare const mulmoStudioSchema: z.ZodObject<{
10821
10854
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
10822
10855
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
10823
10856
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10857
+ movie: z.ZodOptional<z.ZodBoolean>;
10824
10858
  }, z.core.$strip>]>>;
10825
10859
  }, z.core.$strict>, z.ZodObject<{
10826
10860
  type: z.ZodLiteral<"beat">;
@@ -13627,6 +13661,10 @@ export declare const mulmoPromptTemplateSchema: z.ZodObject<{
13627
13661
  }, z.core.$strict>, z.ZodObject<{
13628
13662
  type: z.ZodLiteral<"imagePrompt">;
13629
13663
  prompt: z.ZodString;
13664
+ canvasSize: z.ZodOptional<z.ZodObject<{
13665
+ width: z.ZodNumber;
13666
+ height: z.ZodNumber;
13667
+ }, z.core.$strict>>;
13630
13668
  }, z.core.$strict>]>>>;
13631
13669
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
13632
13670
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -14077,6 +14115,10 @@ export declare const mulmoPromptTemplateFileSchema: z.ZodObject<{
14077
14115
  }, z.core.$strict>, z.ZodObject<{
14078
14116
  type: z.ZodLiteral<"imagePrompt">;
14079
14117
  prompt: z.ZodString;
14118
+ canvasSize: z.ZodOptional<z.ZodObject<{
14119
+ width: z.ZodNumber;
14120
+ height: z.ZodNumber;
14121
+ }, z.core.$strict>>;
14080
14122
  }, z.core.$strict>]>>>;
14081
14123
  backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
14082
14124
  source: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -198,6 +198,7 @@ export const htmlTailwindAnimationSchema = z.union([
198
198
  z.literal(true),
199
199
  z.object({
200
200
  fps: z.number().min(1).max(60).optional().default(30),
201
+ movie: z.boolean().optional().describe("Use CDP screencast for real-time recording (experimental, faster). Default: false (frame-by-frame screenshot)."),
201
202
  }),
202
203
  ]);
203
204
  export const mulmoHtmlTailwindMediaSchema = z
@@ -263,6 +264,7 @@ export const mulmoImagePromptMediaSchema = z
263
264
  .object({
264
265
  type: z.literal("imagePrompt"),
265
266
  prompt: z.string().min(1),
267
+ canvasSize: z.object({ width: z.number(), height: z.number() }).strict().optional(),
266
268
  })
267
269
  .strict();
268
270
  export const mulmoImageParamsImagesValueSchema = z.union([mulmoImageMediaSchema, mulmoImagePromptMediaSchema]);
@@ -0,0 +1,5 @@
1
+ import puppeteer from "puppeteer";
2
+ /** Get a shared browser instance. Launches one if none exists. */
3
+ export declare const getBrowser: () => Promise<puppeteer.Browser>;
4
+ /** Close the shared browser instance. Call at the end of processing. */
5
+ export declare const closeBrowser: () => Promise<void>;
@@ -0,0 +1,39 @@
1
+ import puppeteer from "puppeteer";
2
+ const isCI = process.env.CI === "true";
3
+ const launchArgs = isCI ? ["--no-sandbox", "--allow-file-access-from-files"] : ["--allow-file-access-from-files"];
4
+ let browserInstance = null;
5
+ let launchPromise = null;
6
+ const launchBrowser = async () => {
7
+ const browser = await puppeteer.launch({ args: launchArgs });
8
+ browser.on("disconnected", () => {
9
+ browserInstance = null;
10
+ launchPromise = null;
11
+ });
12
+ return browser;
13
+ };
14
+ /** Get a shared browser instance. Launches one if none exists. */
15
+ export const getBrowser = async () => {
16
+ if (browserInstance?.connected) {
17
+ return browserInstance;
18
+ }
19
+ // Prevent multiple concurrent launches
20
+ if (!launchPromise) {
21
+ launchPromise = launchBrowser().then((browser) => {
22
+ browserInstance = browser;
23
+ launchPromise = null;
24
+ return browser;
25
+ });
26
+ }
27
+ return launchPromise;
28
+ };
29
+ /** Close the shared browser instance. Call at the end of processing. */
30
+ export const closeBrowser = async () => {
31
+ if (launchPromise) {
32
+ await launchPromise;
33
+ }
34
+ if (browserInstance?.connected) {
35
+ await browserInstance.close();
36
+ }
37
+ browserInstance = null;
38
+ launchPromise = null;
39
+ };
@@ -69,6 +69,10 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
69
69
  } | {
70
70
  type: "imagePrompt";
71
71
  prompt: string;
72
+ canvasSize?: {
73
+ width: number;
74
+ height: number;
75
+ } | undefined;
72
76
  }> | undefined;
73
77
  backgroundImage?: string | {
74
78
  source: {
@@ -1725,6 +1729,7 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
1725
1729
  script?: string | string[] | undefined;
1726
1730
  animation?: true | {
1727
1731
  fps: number;
1732
+ movie?: boolean | undefined;
1728
1733
  } | undefined;
1729
1734
  } | {
1730
1735
  type: "beat";
@@ -2163,6 +2168,10 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
2163
2168
  } | {
2164
2169
  type: "imagePrompt";
2165
2170
  prompt: string;
2171
+ canvasSize?: {
2172
+ width: number;
2173
+ height: number;
2174
+ } | undefined;
2166
2175
  }> | undefined;
2167
2176
  backgroundImage?: string | {
2168
2177
  source: {
@@ -3819,6 +3828,7 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
3819
3828
  script?: string | string[] | undefined;
3820
3829
  animation?: true | {
3821
3830
  fps: number;
3831
+ movie?: boolean | undefined;
3822
3832
  } | undefined;
3823
3833
  } | {
3824
3834
  type: "beat";
@@ -4264,6 +4274,10 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
4264
4274
  } | {
4265
4275
  type: "imagePrompt";
4266
4276
  prompt: string;
4277
+ canvasSize?: {
4278
+ width: number;
4279
+ height: number;
4280
+ } | undefined;
4267
4281
  }> | undefined;
4268
4282
  backgroundImage?: string | {
4269
4283
  source: {
@@ -49,6 +49,7 @@ export declare const blankImagePath: () => string;
49
49
  export declare const blankVerticalImagePath: () => string;
50
50
  export declare const blankSquareImagePath: () => string;
51
51
  export declare const getHTMLFile: (filename: string) => string;
52
+ export declare const getJSFile: (filename: string) => string;
52
53
  export declare const getBaseDirPath: (basedir?: string) => string;
53
54
  export declare const getFullPath: (baseDirPath: string | undefined, file: string) => string;
54
55
  export declare const readScriptTemplateFile: (scriptTemplateFileName: string) => MulmoScript;
package/lib/utils/file.js CHANGED
@@ -158,6 +158,10 @@ export const getHTMLFile = (filename) => {
158
158
  const htmlPath = resolveAssetFile(`./assets/html/${filename}.html`, npmRoot);
159
159
  return fs.readFileSync(htmlPath, "utf-8");
160
160
  };
161
+ export const getJSFile = (filename) => {
162
+ const jsPath = resolveAssetFile(`./assets/html/js/${filename}.js`, npmRoot);
163
+ return fs.readFileSync(jsPath, "utf-8");
164
+ };
161
165
  // for cli
162
166
  export const getBaseDirPath = (basedir) => {
163
167
  if (!basedir) {
@@ -10,5 +10,11 @@ export declare const renderHTMLToImage: (html: string, outputPath: string, width
10
10
  * The user-defined render() function may be sync or async.
11
11
  */
12
12
  export declare const renderHTMLToFrames: (html: string, outputDir: string, width: number, height: number, totalFrames: number, fps: number) => Promise<string[]>;
13
+ /**
14
+ * Record an animated HTML page as video using Puppeteer's screencast API.
15
+ * The animation plays in real-time via requestAnimationFrame, and
16
+ * page.screencast() captures frames directly to an mp4 file.
17
+ */
18
+ export declare const renderHTMLToVideo: (html: string, videoPath: string, width: number, height: number, totalFrames: number, fps: number) => Promise<void>;
13
19
  export declare const renderMarkdownToImage: (markdown: string, style: string, outputPath: string, width: number, height: number) => Promise<void>;
14
20
  export declare const interpolate: (template: string, data: Record<string, string>) => string;
@@ -135,6 +135,39 @@ export const renderHTMLToFrames = async (html, outputDir, width, height, totalFr
135
135
  await browser.close();
136
136
  }
137
137
  };
138
+ /**
139
+ * Record an animated HTML page as video using Puppeteer's screencast API.
140
+ * The animation plays in real-time via requestAnimationFrame, and
141
+ * page.screencast() captures frames directly to an mp4 file.
142
+ */
143
+ export const renderHTMLToVideo = async (html, videoPath, width, height, totalFrames, fps) => {
144
+ const duration_ms = (totalFrames / fps) * 1000;
145
+ const browser = await puppeteer.launch({
146
+ args: isCI ? ["--no-sandbox", "--allow-file-access-from-files"] : ["--allow-file-access-from-files"],
147
+ });
148
+ try {
149
+ const page = await browser.newPage();
150
+ await loadHtmlIntoPage(page, html, 30000);
151
+ await page.setViewport({ width, height });
152
+ await page.addStyleTag({ content: "html{height:100%;margin:0;padding:0;overflow:hidden}" });
153
+ await scaleContentToFit(page, width, height);
154
+ const recorder = await page.screencast({
155
+ path: videoPath,
156
+ format: "mp4",
157
+ fps,
158
+ });
159
+ // Play animation in real-time and wait for completion
160
+ await page.evaluate(() => {
161
+ return window.playAnimation();
162
+ });
163
+ // Small buffer to ensure the last frame is captured
164
+ await new Promise((resolve) => setTimeout(resolve, Math.min(duration_ms * 0.1, 500)));
165
+ await recorder.stop();
166
+ }
167
+ finally {
168
+ await browser.close();
169
+ }
170
+ };
138
171
  export const renderMarkdownToImage = async (markdown, style, outputPath, width, height) => {
139
172
  const header = `<head><style>${style}</style></head>`;
140
173
  const body = await marked(markdown);