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.
- package/assets/html/js/animation_runtime.js +371 -0
- package/assets/html/js/auto_render.js +64 -0
- package/assets/html/js/data_attribute_registration.js +178 -0
- package/assets/html/tailwind_animated.html +37 -256
- package/lib/actions/image_references.js +1 -1
- package/lib/methods/mulmo_beat.d.ts +2 -0
- package/lib/methods/mulmo_beat.js +5 -0
- package/lib/types/schema.d.ts +42 -0
- package/lib/types/schema.js +2 -0
- package/lib/utils/browser_pool.d.ts +5 -0
- package/lib/utils/browser_pool.js +39 -0
- package/lib/utils/context.d.ts +14 -0
- package/lib/utils/file.d.ts +1 -0
- package/lib/utils/file.js +4 -0
- package/lib/utils/html_render.d.ts +6 -0
- package/lib/utils/html_render.js +33 -0
- package/lib/utils/image_plugins/html_tailwind.js +22 -13
- package/package.json +10 -9
- package/scripts/test/images/qa_landscape.jpg +0 -0
- package/scripts/test/images/qa_portrait.png +0 -0
- package/scripts/test/test_animated.json +592 -0
- package/scripts/test/test_data_animation.json +149 -0
- package/scripts/test/test_html_cover_pan_zoom_landscape_canvas.json +245 -0
- package/scripts/test/test_html_cover_pan_zoom_portrait_canvas.json +207 -0
- package/scripts/test/test_reference_canvas_size.json +83 -0
|
@@ -1,259 +1,40 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en" class="h-full">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
258
|
-
</
|
|
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);
|
package/lib/types/schema.d.ts
CHANGED
|
@@ -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<{
|
package/lib/types/schema.js
CHANGED
|
@@ -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
|
+
};
|
package/lib/utils/context.d.ts
CHANGED
|
@@ -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: {
|
package/lib/utils/file.d.ts
CHANGED
|
@@ -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;
|
package/lib/utils/html_render.js
CHANGED
|
@@ -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);
|