@amg555/frameforge-mcp 2.3.3 → 2.3.5

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -2
  3. package/out/frames/f000000.jpg +0 -0
  4. package/out/frames/f000001.jpg +0 -0
  5. package/out/frames/f000002.jpg +0 -0
  6. package/out/frames/f000003.jpg +0 -0
  7. package/out/frames/f000004.jpg +0 -0
  8. package/out/frames/f000005.jpg +0 -0
  9. package/out/frames/f000006.jpg +0 -0
  10. package/out/frames/f000007.jpg +0 -0
  11. package/out/frames/f000008.jpg +0 -0
  12. package/out/frames/f000009.jpg +0 -0
  13. package/out/frames/f000010.jpg +0 -0
  14. package/out/frames/f000011.jpg +0 -0
  15. package/out/frames/f000012.jpg +0 -0
  16. package/out/frames/f000013.jpg +0 -0
  17. package/out/frames/f000014.jpg +0 -0
  18. package/out/frames/f000015.jpg +0 -0
  19. package/out/frames/f000016.jpg +0 -0
  20. package/out/frames/f000017.jpg +0 -0
  21. package/out/frames/f000018.jpg +0 -0
  22. package/out/frames/f000019.jpg +0 -0
  23. package/out/frames/f000020.jpg +0 -0
  24. package/out/frames/f000021.jpg +0 -0
  25. package/out/frames/f000022.jpg +0 -0
  26. package/out/frames/f000023.jpg +0 -0
  27. package/out/frames/f000024.jpg +0 -0
  28. package/out/frames/f000025.jpg +0 -0
  29. package/out/frames/f000026.jpg +0 -0
  30. package/out/frames/f000027.jpg +0 -0
  31. package/out/frames/f000028.jpg +0 -0
  32. package/out/frames/f000029.jpg +0 -0
  33. package/out/frames/f000030.jpg +0 -0
  34. package/out/frames/f000031.jpg +0 -0
  35. package/out/frames/f000032.jpg +0 -0
  36. package/out/frames/f000033.jpg +0 -0
  37. package/out/frames/f000034.jpg +0 -0
  38. package/out/frames/f000035.jpg +0 -0
  39. package/out/frames/f000036.jpg +0 -0
  40. package/out/frames/f000037.jpg +0 -0
  41. package/out/frames/f000038.jpg +0 -0
  42. package/out/frames/f000039.jpg +0 -0
  43. package/out/frames/f000040.jpg +0 -0
  44. package/out/frames/f000041.jpg +0 -0
  45. package/out/frames/f000042.jpg +0 -0
  46. package/out/frames/f000043.jpg +0 -0
  47. package/out/frames/f000044.jpg +0 -0
  48. package/out/frames/f000045.jpg +0 -0
  49. package/out/frames/f000046.jpg +0 -0
  50. package/out/frames/f000047.jpg +0 -0
  51. package/package.json +2 -2
  52. package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +1 -1
  53. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +2 -2
  54. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1 -1
  55. package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +11 -0
  56. package/scripts/deep-validate.js +2 -2
  57. package/scripts/html-renderer/new-project.mjs +2 -2
  58. package/src/cli/doctor.js +12 -5
  59. package/src/harness/bin/harness.mjs +20 -7
  60. package/src/mcp-server/server.js +1 -1
  61. package/src/video-renderer/renderer.js +222 -60
  62. package/src/video-renderer/scene-animator.js +459 -0
  63. package/src/workflows/assets.js +4 -0
  64. package/src/workflows/render.js +12 -7
@@ -0,0 +1,459 @@
1
+ 'use strict';
2
+ /**
3
+ * Scene Animator — Code-Driven HTML/Canvas Scene Generator
4
+ *
5
+ * Generates self-contained HTML/Canvas animation scenes conforming to the
6
+ * FrameForge frame-purity contract:
7
+ * - window.DURATION = seconds
8
+ * - window.seek = async (t) => draws frame at time t
9
+ * - Every frame is a pure function of t (no Date.now, no Math.random without seed)
10
+ * - Multilingual typography support: Malayalam, Indic, CJK, Arabic, Latin
11
+ *
12
+ * Supported component themes:
13
+ * - 'code': IDE/terminal window with typing code and cursor
14
+ * - 'chart': Animated bar chart, progress ring, and metric counter
15
+ * - 'nodes': Pulsing network nodes with traveling energy packets
16
+ * - 'diagram': Flowchart steps with directional energy arrows
17
+ * - 'image': Floating Ken-Burns card embedding user-provided image
18
+ * - 'kinetic': Audio spectrum visualizer + isometric geometry
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // Multilingual font family stack prioritizing Indic/Malayalam system fonts
25
+ const FONT_STACK = `'Nirmala UI', 'Noto Sans Malayalam', 'Malayalam Sangam MN', 'Kartika', 'Meera', 'Manjari', -apple-system, 'Segoe UI', system-ui, sans-serif`;
26
+
27
+ /**
28
+ * Detect the best visual theme from visual description, caption, and media.
29
+ */
30
+ function detectTheme({ visualDescription = '', caption = '', imagePath = null } = {}) {
31
+ if (imagePath) return 'image';
32
+ const text = `${visualDescription} ${caption}`.toLowerCase();
33
+ if (/\b(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct|cli|terminal|bash|shell|npm|git)\b/.test(text)) {
34
+ return 'code';
35
+ }
36
+ if (/\b(chart|data|metric|analytics|growth|graph|statistic|stats|revenue|percent|count|number)\b/.test(text)) {
37
+ return 'chart';
38
+ }
39
+ if (/\b(network|node|ai|model|neural|pipeline|architecture|cloud|server|system|mesh|connect)\b/.test(text)) {
40
+ return 'nodes';
41
+ }
42
+ if (/\b(diagram|flow|step|process|workflow|sequence|lifecycle|cycle)\b/.test(text)) {
43
+ return 'diagram';
44
+ }
45
+ return 'kinetic';
46
+ }
47
+
48
+ /**
49
+ * Generate a complete self-contained HTML scene string.
50
+ *
51
+ * @param {object} opts
52
+ * @param {string} opts.caption
53
+ * @param {string} [opts.narration]
54
+ * @param {string} [opts.visualDescription]
55
+ * @param {string} [opts.imagePath]
56
+ * @param {number} opts.duration
57
+ * @param {number} [opts.width]
58
+ * @param {number} [opts.height]
59
+ * @param {number} [opts.sceneIndex]
60
+ * @param {string} [opts.musicMood]
61
+ * @returns {string} HTML document
62
+ */
63
+ function generateSceneHtml({
64
+ caption = '',
65
+ narration = '',
66
+ visualDescription = '',
67
+ imagePath = null,
68
+ duration = 3,
69
+ width = 1920,
70
+ height = 1080,
71
+ sceneIndex = 0,
72
+ musicMood = 'neutral',
73
+ } = {}) {
74
+ const theme = detectTheme({ visualDescription, caption, imagePath });
75
+ const clampedDur = Math.max(1, Number(duration) || 3);
76
+
77
+ // Convert imagePath to data URL if local file exists
78
+ let imageDataUri = '';
79
+ if (imagePath && fs.existsSync(imagePath)) {
80
+ try {
81
+ const ext = path.extname(imagePath).toLowerCase().slice(1) || 'jpeg';
82
+ const mime = ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
83
+ const b64 = fs.readFileSync(imagePath).toString('base64');
84
+ imageDataUri = `data:${mime};base64,${b64}`;
85
+ } catch (_) {}
86
+ }
87
+
88
+ // Color palettes based on scene index and music mood
89
+ const PALETTES = [
90
+ { bg0: '#142036', bg1: '#0b111c', bg2: '#05080f', accent: '#38bdf8', accent2: '#818cf8', tag: 'CYBER TECH' },
91
+ { bg0: '#231436', bg1: '#110a1c', bg2: '#07040d', accent: '#c084fc', accent2: '#f43f5e', tag: 'DEEP DYNAMICS' },
92
+ { bg0: '#0e261f', bg1: '#071511', bg2: '#030a08', accent: '#34d399', accent2: '#38bdf8', tag: 'SYSTEM ACTIVE' },
93
+ { bg0: '#291d0e', bg1: '#150f07', bg2: '#0a0703', accent: '#fbbf24', accent2: '#f97316', tag: 'HIGH IMPACT' },
94
+ { bg0: '#161d2b', bg1: '#0d131d', bg2: '#06090e', accent: '#60a5fa', accent2: '#a78bfa', tag: 'INTELLIGENCE' },
95
+ ];
96
+ const pal = PALETTES[sceneIndex % PALETTES.length];
97
+
98
+ return `<!doctype html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="utf-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <style>
104
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
105
+ html, body {
106
+ width: 100%;
107
+ height: 100%;
108
+ overflow: hidden;
109
+ background: ${pal.bg2};
110
+ font-family: ${FONT_STACK};
111
+ -webkit-font-smoothing: antialiased;
112
+ }
113
+ canvas {
114
+ width: 100%;
115
+ height: 100%;
116
+ display: block;
117
+ }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <canvas id="stage" width="${width}" height="${height}"></canvas>
122
+ <script>
123
+ window.DURATION = ${clampedDur};
124
+ const canvas = document.getElementById('stage');
125
+ const ctx = canvas.getContext('2d');
126
+ const W = ${width};
127
+ const H = ${height};
128
+
129
+ // Seeded PRNG for frame purity (same t -> exact same frame)
130
+ function sfc32(a, b, c, d) {
131
+ return function() {
132
+ a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
133
+ let t = (a + b | 0) + d | 0;
134
+ d = d + 1 | 0;
135
+ a = b ^ b >>> 9;
136
+ b = c + (c << 3) | 0;
137
+ c = c << 21 | c >>> 11;
138
+ c = c + t | 0;
139
+ return (t >>> 0) / 4294967296;
140
+ };
141
+ }
142
+
143
+ const rng = sfc32(${42 + sceneIndex * 17}, ${1337 + sceneIndex * 31}, ${7 + sceneIndex * 11}, 99);
144
+ const PARTICLES = Array.from({ length: 54 }, () => ({
145
+ x: rng() * W,
146
+ y: rng() * H,
147
+ vx: (rng() - 0.5) * 50,
148
+ vy: (rng() - 0.5) * 35 - 15,
149
+ r: 1.5 + rng() * 3.5,
150
+ alpha: 0.3 + rng() * 0.5
151
+ }));
152
+
153
+ // Spring dynamics function
154
+ function spring(t, delay = 0, tension = 160, friction = 14) {
155
+ const dt = t - delay;
156
+ if (dt <= 0) return 0;
157
+ const omega = Math.sqrt(tension);
158
+ const zeta = friction / (2 * omega);
159
+ if (zeta < 1) {
160
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
161
+ return 1 - Math.exp(-zeta * omega * dt) * (Math.cos(omegaD * dt) + (zeta * omega / omegaD) * Math.sin(omegaD * dt));
162
+ }
163
+ return 1 - Math.exp(-omega * dt);
164
+ }
165
+
166
+ const THEME = ${JSON.stringify(theme)};
167
+ const CAPTION = ${JSON.stringify(caption)};
168
+ const NARRATION = ${JSON.stringify(narration)};
169
+ const TAG = ${JSON.stringify(pal.tag)};
170
+ const ACCENT = ${JSON.stringify(pal.accent)};
171
+ const ACCENT2 = ${JSON.stringify(pal.accent2)};
172
+ const BG0 = ${JSON.stringify(pal.bg0)};
173
+ const BG1 = ${JSON.stringify(pal.bg1)};
174
+ const BG2 = ${JSON.stringify(pal.bg2)};
175
+ const IMAGE_SRC = ${JSON.stringify(imageDataUri)};
176
+
177
+ let cachedImage = null;
178
+ if (IMAGE_SRC) {
179
+ cachedImage = new Image();
180
+ cachedImage.src = IMAGE_SRC;
181
+ }
182
+
183
+ window.seek = async (t) => {
184
+ // ── 1. Dynamic Moving Gradient Background ────────────────────────────────
185
+ const cx = W / 2 + Math.sin(t * 0.7) * 160;
186
+ const cy = H / 2 + Math.cos(t * 0.5) * 90;
187
+ const bgGrad = ctx.createRadialGradient(cx, cy, 60, W / 2, H / 2, W * 0.85);
188
+ bgGrad.addColorStop(0, BG0);
189
+ bgGrad.addColorStop(0.55, BG1);
190
+ bgGrad.addColorStop(1, BG2);
191
+ ctx.fillStyle = bgGrad;
192
+ ctx.fillRect(0, 0, W, H);
193
+
194
+ // ── 2. Subtle Moving Perspective Grid ───────────────────────────────────
195
+ ctx.save();
196
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.04)';
197
+ ctx.lineWidth = 1;
198
+ const gridShift = (t * 30) % 60;
199
+ for (let x = 0; x <= W; x += 60) {
200
+ ctx.beginPath();
201
+ ctx.moveTo(x, 0);
202
+ ctx.lineTo(x, H);
203
+ ctx.stroke();
204
+ }
205
+ for (let y = gridShift; y <= H; y += 60) {
206
+ ctx.beginPath();
207
+ ctx.moveTo(0, y);
208
+ ctx.lineTo(W, y);
209
+ ctx.stroke();
210
+ }
211
+ ctx.restore();
212
+
213
+ // ── 3. Floating Dust & Energy Particles ──────────────────────────────────
214
+ ctx.save();
215
+ for (let i = 0; i < PARTICLES.length; i++) {
216
+ const p = PARTICLES[i];
217
+ const px = (p.x + p.vx * t + W * 10) % W;
218
+ const py = (p.y + p.vy * t + H * 10) % H;
219
+ const pulse = 0.4 + 0.6 * Math.sin(t * 3.2 + i * 0.5);
220
+ ctx.fillStyle = ACCENT;
221
+ ctx.globalAlpha = p.alpha * pulse * 0.7;
222
+ ctx.beginPath();
223
+ ctx.arc(px, py, p.r, 0, Math.PI * 2);
224
+ ctx.fill();
225
+ }
226
+ ctx.restore();
227
+
228
+ // ── 4. Central Graphic Card matching Theme ──────────────────────────────
229
+ ctx.save();
230
+ const cardSpring = spring(t, 0.08, 140, 13);
231
+ const cardScale = Math.min(1.04, Math.max(0, cardSpring));
232
+ const cardY = H * 0.40 + (1 - cardSpring) * 50;
233
+ ctx.translate(W / 2, cardY);
234
+ ctx.scale(cardScale, cardScale);
235
+
236
+ const cardW = Math.round(W * 0.55);
237
+ const cardH = Math.round(H * 0.38);
238
+
239
+ // Card backdrop with glassmorphic border
240
+ ctx.fillStyle = 'rgba(11, 17, 28, 0.82)';
241
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
242
+ ctx.lineWidth = 2;
243
+ ctx.beginPath();
244
+ ctx.roundRect(-cardW / 2, -cardH / 2, cardW, cardH, 20);
245
+ ctx.fill();
246
+ ctx.stroke();
247
+
248
+ // Accent edge highlight
249
+ ctx.strokeStyle = ACCENT;
250
+ ctx.lineWidth = 2;
251
+ ctx.beginPath();
252
+ ctx.moveTo(-cardW / 2 + 30, -cardH / 2);
253
+ ctx.lineTo(cardW / 2 - 30, -cardH / 2);
254
+ ctx.stroke();
255
+
256
+ if (THEME === 'code') {
257
+ // IDE Titlebar
258
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
259
+ ctx.beginPath();
260
+ ctx.roundRect(-cardW / 2, -cardH / 2, cardW, 40, [20, 20, 0, 0]);
261
+ ctx.fill();
262
+ // Window dots
263
+ ctx.fillStyle = '#ff5f56'; ctx.beginPath(); ctx.arc(-cardW / 2 + 25, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
264
+ ctx.fillStyle = '#ffbd2e'; ctx.beginPath(); ctx.arc(-cardW / 2 + 45, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
265
+ ctx.fillStyle = '#27c93f'; ctx.beginPath(); ctx.arc(-cardW / 2 + 65, -cardH / 2 + 20, 6, 0, Math.PI * 2); ctx.fill();
266
+ // Tab title
267
+ ctx.font = '14px monospace';
268
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
269
+ ctx.textAlign = 'left';
270
+ ctx.fillText('scene-pipeline.js', -cardW / 2 + 90, -cardH / 2 + 25);
271
+
272
+ // Code lines with typing effect
273
+ const codeLines = [
274
+ 'import { FrameForge } from "@amg555/frameforge";',
275
+ 'const engine = new FrameForge.AnimationEngine({ t });',
276
+ 'await engine.renderScene({',
277
+ ' status: "ACTIVE_24FPS",',
278
+ ' fidelity: "BROADCAST_BT709"',
279
+ '});'
280
+ ];
281
+ ctx.font = '18px "Fira Code", monospace';
282
+ const typeProg = Math.min(1, t / (window.DURATION * 0.75));
283
+ const totalChars = codeLines.join('\\n').length;
284
+ let charsToShow = Math.floor(totalChars * typeProg);
285
+
286
+ let lineY = -cardH / 2 + 80;
287
+ for (let l = 0; l < codeLines.length; l++) {
288
+ const line = codeLines[l];
289
+ const sliceLen = Math.min(line.length, charsToShow);
290
+ const textToDraw = line.slice(0, sliceLen);
291
+ charsToShow = Math.max(0, charsToShow - line.length - 1);
292
+
293
+ // Line number
294
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
295
+ ctx.fillText(String(l + 1).padStart(2, '0'), -cardW / 2 + 30, lineY);
296
+ // Syntax text
297
+ ctx.fillStyle = l === 0 ? ACCENT : l === 1 ? ACCENT2 : '#e2e8f0';
298
+ ctx.fillText(textToDraw, -cardW / 2 + 75, lineY);
299
+ lineY += 32;
300
+ }
301
+ } else if (THEME === 'chart') {
302
+ // Animated bar chart + metrics
303
+ const bars = 8;
304
+ const barW = (cardW - 140) / bars;
305
+ const startX = -cardW / 2 + 70;
306
+ const baseLine = cardH / 2 - 50;
307
+
308
+ for (let b = 0; b < bars; b++) {
309
+ const barSpring = spring(t, 0.15 + b * 0.05, 120, 12);
310
+ const targetH = 50 + ((b * 37) % 140);
311
+ const curH = targetH * Math.min(1.05, Math.max(0, barSpring));
312
+ const bx = startX + b * barW;
313
+ const by = baseLine - curH;
314
+
315
+ const barGrad = ctx.createLinearGradient(0, by, 0, baseLine);
316
+ barGrad.addColorStop(0, ACCENT);
317
+ barGrad.addColorStop(1, ACCENT2);
318
+ ctx.fillStyle = barGrad;
319
+ ctx.beginPath();
320
+ ctx.roundRect(bx, by, barW - 12, curH, 6);
321
+ ctx.fill();
322
+ }
323
+ // Animated Counter
324
+ const counterVal = Math.round(spring(t, 0.2, 100, 10) * 98.4);
325
+ ctx.font = 'bold 36px monospace';
326
+ ctx.fillStyle = '#ffffff';
327
+ ctx.textAlign = 'right';
328
+ ctx.fillText(counterVal + '% METRIC GAIN', cardW / 2 - 40, -cardH / 2 + 50);
329
+ } else if (THEME === 'nodes') {
330
+ // Interconnected pulsing network nodes
331
+ const nodeCoords = [
332
+ [-cardW * 0.35, -30], [-cardW * 0.15, 40], [0, -50],
333
+ [cardW * 0.15, 30], [cardW * 0.35, -20]
334
+ ];
335
+ // Connections
336
+ ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)';
337
+ ctx.lineWidth = 2;
338
+ for (let i = 0; i < nodeCoords.length - 1; i++) {
339
+ ctx.beginPath();
340
+ ctx.moveTo(nodeCoords[i][0], nodeCoords[i][1]);
341
+ ctx.lineTo(nodeCoords[i + 1][0], nodeCoords[i + 1][1]);
342
+ ctx.stroke();
343
+
344
+ // Energy packet along connection
345
+ const packetPos = (t * 0.8 + i * 0.25) % 1;
346
+ const ex = nodeCoords[i][0] + (nodeCoords[i + 1][0] - nodeCoords[i][0]) * packetPos;
347
+ const ey = nodeCoords[i][1] + (nodeCoords[i + 1][1] - nodeCoords[i][1]) * packetPos;
348
+ ctx.fillStyle = ACCENT;
349
+ ctx.beginPath();
350
+ ctx.arc(ex, ey, 5, 0, Math.PI * 2);
351
+ ctx.fill();
352
+ }
353
+ // Nodes
354
+ for (let i = 0; i < nodeCoords.length; i++) {
355
+ const [nx, ny] = nodeCoords[i];
356
+ const pulse = 1 + 0.25 * Math.sin(t * 4 + i);
357
+ ctx.fillStyle = ACCENT2;
358
+ ctx.beginPath();
359
+ ctx.arc(nx, ny, 14 * pulse, 0, Math.PI * 2);
360
+ ctx.fill();
361
+ ctx.fillStyle = '#ffffff';
362
+ ctx.beginPath();
363
+ ctx.arc(nx, ny, 6, 0, Math.PI * 2);
364
+ ctx.fill();
365
+ }
366
+ } else if (THEME === 'image' && cachedImage && cachedImage.complete && cachedImage.naturalWidth > 0) {
367
+ // Ken-Burns floating image
368
+ const zoom = 1.0 + (t / window.DURATION) * 0.07;
369
+ ctx.save();
370
+ ctx.beginPath();
371
+ ctx.roundRect(-cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24, 14);
372
+ ctx.clip();
373
+ ctx.scale(zoom, zoom);
374
+ ctx.drawImage(cachedImage, -cardW / 2 + 12, -cardH / 2 + 12, cardW - 24, cardH - 24);
375
+ ctx.restore();
376
+ } else {
377
+ // Kinetic Audio Spectrum Bars
378
+ const bars = 28;
379
+ const barW = (cardW - 80) / bars;
380
+ const startX = -cardW / 2 + 40;
381
+ for (let b = 0; b < bars; b++) {
382
+ const phase = t * 4.5 + b * 0.3;
383
+ const hVal = 25 + 95 * Math.abs(Math.sin(phase) * Math.cos(phase * 0.8));
384
+ const bx = startX + b * barW;
385
+ const by = 20 - hVal / 2;
386
+
387
+ const barGrad = ctx.createLinearGradient(0, by, 0, by + hVal);
388
+ barGrad.addColorStop(0, ACCENT);
389
+ barGrad.addColorStop(1, ACCENT2);
390
+ ctx.fillStyle = barGrad;
391
+ ctx.beginPath();
392
+ ctx.roundRect(bx, by, barW - 6, hVal, 4);
393
+ ctx.fill();
394
+ }
395
+ }
396
+
397
+ // Tag Badge
398
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
399
+ ctx.beginPath();
400
+ ctx.roundRect(-cardW / 2 + 25, -cardH / 2 + 20, 140, 30, 15);
401
+ ctx.fill();
402
+ ctx.font = 'bold 13px system-ui, sans-serif';
403
+ ctx.fillStyle = ACCENT;
404
+ ctx.textAlign = 'center';
405
+ ctx.fillText(TAG, -cardW / 2 + 95, -cardH / 2 + 40);
406
+
407
+ ctx.restore();
408
+
409
+ // ── 5. Kinetic Typography with Multilingual / Malayalam Support ─────────
410
+ ctx.save();
411
+ const textSpring = spring(t, 0.22, 150, 13);
412
+ const textY = H * 0.77 + (1 - textSpring) * 45;
413
+ const textAlpha = Math.min(1, Math.max(0, textSpring));
414
+
415
+ ctx.globalAlpha = textAlpha;
416
+ ctx.textAlign = 'center';
417
+
418
+ // Primary Caption / Headline
419
+ ctx.font = 'bold 54px ' + ${JSON.stringify(FONT_STACK)};
420
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
421
+ ctx.shadowBlur = 20;
422
+ ctx.shadowOffsetY = 6;
423
+ ctx.fillStyle = '#ffffff';
424
+ ctx.fillText(CAPTION, W / 2, textY);
425
+
426
+ // Subtitle / Narration preview
427
+ if (NARRATION) {
428
+ ctx.font = '400 28px ' + ${JSON.stringify(FONT_STACK)};
429
+ ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
430
+ ctx.shadowBlur = 12;
431
+ const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
432
+ ctx.fillText(cleanNarr, W / 2, textY + 62);
433
+ }
434
+ ctx.restore();
435
+
436
+ // ── 6. Progress Scrubber ────────────────────────────────────────────────
437
+ ctx.save();
438
+ const progress = Math.min(1, Math.max(0, t / window.DURATION));
439
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
440
+ ctx.fillRect(0, H - 8, W, 8);
441
+
442
+ const progGrad = ctx.createLinearGradient(0, 0, W, 0);
443
+ progGrad.addColorStop(0, ACCENT);
444
+ progGrad.addColorStop(1, ACCENT2);
445
+ ctx.fillStyle = progGrad;
446
+ ctx.fillRect(0, H - 8, W * progress, 8);
447
+ ctx.restore();
448
+ };
449
+ window.ready = true;
450
+ </script>
451
+ </body>
452
+ </html>`;
453
+ }
454
+
455
+ module.exports = {
456
+ generateSceneHtml,
457
+ detectTheme,
458
+ FONT_STACK,
459
+ };
@@ -88,6 +88,10 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipTts = fals
88
88
  caption: scene.caption,
89
89
  camera: scene.camera_hint || scene.camera || 'static',
90
90
  focalPoint: scene.focal_point || null,
91
+ narration: scene.narration || '',
92
+ visualDescription: scene.visual_description || scene.narration || '',
93
+ musicMood: scene.music_mood || 'neutral',
94
+ language: scene.language || '',
91
95
  };
92
96
  }));
93
97
  results.push(...batchResults);
@@ -42,13 +42,18 @@ async function runRender({ productionId, sceneAssets, withMusic = true, musicVol
42
42
  const idx = asset.sceneId - 1;
43
43
  try {
44
44
  const scenePath = renderer.renderScene({
45
- sceneIndex: idx,
46
- imagePath: asset.imagePath,
47
- videoPath: asset.videoPath,
48
- voicePath: asset.voicePath,
49
- caption: asset.caption || '',
50
- camera: asset.camera || 'static',
51
- focalPoint: asset.focalPoint || null,
45
+ sceneIndex: idx,
46
+ imagePath: asset.imagePath,
47
+ videoPath: asset.videoPath,
48
+ voicePath: asset.voicePath,
49
+ caption: asset.caption || '',
50
+ narration: asset.narration || '',
51
+ visualDescription: asset.visualDescription || '',
52
+ visualType: asset.visualType || 'code_animation',
53
+ musicMood: asset.musicMood || 'neutral',
54
+ language: asset.language || '',
55
+ camera: asset.camera || 'static',
56
+ focalPoint: asset.focalPoint || null,
52
57
  renderWidth,
53
58
  renderHeight,
54
59
  });