@amg555/frameforge-mcp 2.3.5 → 2.3.6

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/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.3.6] — 2026-11-09
8
+
9
+ ### Added
10
+ - **Full-Canvas Cinematic Procedural Entities** (`src/video-renderer/scene-animator.js`):
11
+ - Completely eliminated the rigid PPT card slideshow pattern. Every scene now renders a living, full-canvas animated universe.
12
+ - 3D Celestial Orbital Simulation: Earth with rotating longitude/latitude wireframes, tilted elliptical orbits, orbiting satellites with solar panels, expanding spherical radio wavefront pulses, and ground target beacon.
13
+ - 3D Rotating DNA Double-Helix: Sinusoidal backbones with perspective depth, nucleotide base-pair rungs, and molecular spore drift.
14
+ - Full-Canvas Financial Candlesticks: Realistic Japanese candlesticks with upper/lower wicks, glowing Exponential Moving Average (EMA) ribbon curve, volume bars, and price tickers.
15
+ - 3D Tactical Radar Globe: Rotating spherical grid with 360-degree radar sweep sector and pinging incident coordinates.
16
+ - 3D Wireframe Polyhedra: Pure mathematical 3D perspective projection (octahedron/cubes) with coordinate axes.
17
+ - Historical Milestone Roadmaps: Flowing chronological paths with golden era seals and progressive illumination pulses.
18
+ - Full-Canvas Neural Mesh: 4-tier neural network with glowing energy packets traveling along active synapses.
19
+ - **Direct Agent Code & HTML Execution** (`src/video-renderer/renderer.js`, `src/mcp-server/server.js`, `src/workflows/render.js`, `src/workflows/assets.js`):
20
+ - Added support for `html` (complete page conforming to `window.DURATION` and `window.seek(t)`) and `code` (canvas drawing instructions for `t`) across `create_video_from_script`, `render_from_scenes`, and workflow pipelines.
21
+ - Enables Claude Code, Opus 5.5, Cursor, and IBM Bob to write custom animation programs that FrameForge renders directly without template overrides.
22
+ - Added `wrapCodeInHarnessHtml()` helper to embed pure Canvas drawing code into a compliant virtual-clock harness page.
23
+ - **Cinematic Broadcast Lower-Third Overlays**:
24
+ - Replaced repetitive center-card text with glassmorphic lower-third broadcast banners leaving the upper 75% of the frame completely open for motion graphics.
25
+ - Accent color left tab, tag pills, high-contrast drop-shadow typography, and subtle bottom progress bar.
26
+ - **Multilingual Typography Integration**:
27
+ - Full native DirectWrite/HarfBuzz font support for Malayalam (`Nirmala UI`, `Noto Sans Malayalam`), Hindi/Devanagari, Tamil, Telugu, Arabic, and CJK with zero broken glyphs.
28
+
29
+ ---
30
+
7
31
  ## [2.3.5] — 2026-11-09
8
32
 
9
33
  ### Added
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm](https://img.shields.io/npm/v/@amg555/frameforge-mcp.svg?label=npm)](https://www.npmjs.com/package/@amg555/frameforge-mcp)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
11
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
12
- [![Version](https://img.shields.io/badge/version-2.3.5-blue.svg)](CHANGELOG.md)
12
+ [![Version](https://img.shields.io/badge/version-2.3.6-blue.svg)](CHANGELOG.md)
13
13
  [![MCP Spec](https://img.shields.io/badge/MCP-2024--11--05-blue.svg)](https://modelcontextprotocol.io/)
14
14
  [![Tests](https://img.shields.io/badge/tests-539%20passing-brightgreen.svg)](#testing)
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 17 MCP tools.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -137,21 +137,36 @@ npm run setup
137
137
 
138
138
  ## Workflow (step-by-step)
139
139
 
140
- ### Path 1 — Agent-First (FFmpeg + AI images)
140
+ ### Path 1 — Agent-First (Code-Driven Motion Graphics or Direct Custom Code)
141
141
 
142
- **You are already running an LLM — use it.** Write the scenes yourself and hand
143
- them to FrameForge for rendering. No `GROQ_API_KEY` or any other LLM key needed.
144
- FrameForge only runs FFmpeg, TTS, and image generation:
142
+ **You are already running an LLM (Claude Code / Opus 5.5 / Cursor / Bob) — use it.**
143
+ Write the scenes yourself and hand them to FrameForge for rendering. No external API key needed.
144
+ FrameForge renders full-canvas, broadcast-quality motion graphics (24fps pure function of `t`) with zero slideshow/card patterns:
145
145
 
146
- ```
146
+ - **Option A — Natural Description:** Provide `visual_description` (e.g. "Earth rotating with 4 satellite orbits and microwave beams intersecting"). FrameForge automatically synthesizes a full-canvas procedural 3D/2D animation entity (orbital mechanics, 3D DNA helix, candlestick charts, neural mesh, 3D wireframe polyhedra).
147
+ - **Option B — Direct Agent Code:** Provide `html` (complete page with `window.DURATION` & `window.seek(t)`) or `code` (canvas drawing instructions for `t`). FrameForge executes your program directly via headless Chromium.
148
+ - **Option C — Multilingual & Regional Scripts:** Pass `language: "ml"` (Malayalam), `"hi"` (Hindi), etc. Native DirectWrite/HarfBuzz font stacks (`Nirmala UI`, `Noto Sans Malayalam`) render conjuncts without broken glyphs.
149
+
150
+ ```js
147
151
  create_video_from_script({
152
+ topic: "GPS Satellites and Orbital Mechanics",
153
+ style: "cosmic_space",
154
+ language: "en",
148
155
  scenes: [
149
- { narration: "WiFi sends data as radio waves...", caption: "RADIO WAVES",
150
- visual_description: "router emitting waves, flat illustration" },
151
- { narration: "Your device picks up the signal...", caption: "YOUR DEVICE",
152
- visual_description: "laptop receiving signal, clean diagram" }
156
+ {
157
+ narration: "GPS uses trilateration from atomic clock satellites orbiting Earth.",
158
+ caption: "TRILATERATION",
159
+ visual_description: "3D Earth rotating with 4 satellite orbits and microwave beams converging",
160
+ // Optional: you can pass your own Canvas code or complete HTML:
161
+ // code: "ctx.fillStyle = '#0c1116'; ctx.fillRect(0,0,W,H); ..."
162
+ },
163
+ {
164
+ narration: "Relativistic time dilation requires nanosecond daily corrections.",
165
+ caption: "TIME DILATION",
166
+ visual_description: "Quantum atomic clock oscillation with compounding nanosecond curve"
167
+ }
153
168
  ],
154
- title: "How WiFi Works"
169
+ title: "How GPS Actually Works"
155
170
  })
156
171
  ```
157
172
 
@@ -160,8 +175,6 @@ Then deliver it:
160
175
  publish_video({ productionId, masterPath, channels: ["file"] })
161
176
  ```
162
177
 
163
- > **Image generation fallback:** If the configured image provider is unavailable, FrameForge automatically falls back to a dark-background placeholder per scene — the render completes. Switch to `PROVIDER_IMAGE=falai` for a more reliable free provider.
164
-
165
178
  ### Path 2 — Code-Driven Animated Video (Playwright + ffmpeg, no AI images)
166
179
 
167
180
  Use when: the user wants Remotion-style motion graphics, image gen is unavailable, or they ask for "animated video" or "make it look good". Every pixel is a pure function of time `t`.
@@ -20,7 +20,7 @@ check('batch handling', code.includes('Array.isArray(body)'));
20
20
  check('ping handler', code.includes("method === 'ping'"));
21
21
  check('notifications/init', code.includes("notifications/initialized"));
22
22
  check('body size limit', code.includes('MAX_BODY_BYTES'));
23
- check('SERVER_INFO version 2.3.5', code.includes("version: '2.3.5'"));
23
+ check('SERVER_INFO version 2.3.6', code.includes("version: '2.3.6'"));
24
24
  check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
25
25
  check('export_to_editor tool defined', code.includes('export_to_editor'));
26
26
  check('voxforge TTS provider', code.includes('voxforge'));
@@ -293,7 +293,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
293
293
  check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
294
294
  check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
295
295
  check('cf-worker returns 429 on limit', cfWorker.includes('429'));
296
- check('cf-worker version 2.3.5', cfWorker.includes("'2.3.5'"));
296
+ check('cf-worker version 2.3.6', cfWorker.includes("'2.3.6'"));
297
297
  check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
298
298
 
299
299
  // ── plugins/skills present ────────────────────────────────────────────────
package/src/cli/make.js CHANGED
@@ -410,6 +410,9 @@ async function runSingleVideo(args, topic, outDir, workflows) {
410
410
  skipImages,
411
411
  skipTts: args.noTts,
412
412
  enableAiImages,
413
+ topic,
414
+ title,
415
+ style: args.style || '',
413
416
  });
414
417
  log('Assets', `✅ ${assetsResult.sceneCount} scenes prepared`);
415
418
 
@@ -418,6 +421,9 @@ async function runSingleVideo(args, topic, outDir, workflows) {
418
421
  const renderResult = await runRender({
419
422
  productionId,
420
423
  sceneAssets: assetsResult.sceneAssets,
424
+ topic,
425
+ title,
426
+ style: args.style || '',
421
427
  });
422
428
  if (!renderResult.passed) {
423
429
  throw new Error(`Quality gate FAILED: ${JSON.stringify(renderResult.qcReport?.checks?.filter(c => !c.passed).map(c => c.name))}`);
@@ -349,9 +349,14 @@ const TOOLS = {
349
349
  skipTts: { type: 'boolean', default: false, description: 'Skip TTS synthesis (use audio_path in scenes)' },
350
350
  withMusic: { type: 'boolean', default: true },
351
351
  musicVolume: { type: 'number', default: 0.07 },
352
+ topic: { type: 'string', description: 'Overall topic/genre for visual theme and aesthetic selection' },
353
+ style: { type: 'string', description: 'Visual style/theme override (e.g. cinematic_documentary, finance_executive, cyber_tech, retro_synthwave, nature_organic, hype_sports, minimal_luxury, breaking_news, cosmic_space)' },
354
+ theme: { type: 'string', description: 'Theme preset alias' },
355
+ title: { type: 'string', description: 'Video title' },
356
+ language: { type: 'string', description: 'BCP-47 language hint (e.g. ml, hi, en, ta)' },
352
357
  },
353
358
  },
354
- handler: async ({ productionId, scenes, orientation = 'landscape', skipImages = false, skipTts = false, withMusic = true, musicVolume = 0.07 }) => {
359
+ handler: async ({ productionId, scenes, orientation = 'landscape', skipImages = false, skipTts = false, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', title = '', language = '' }) => {
355
360
  const syncFs = require('fs');
356
361
  const { runAssets } = require('../workflows/assets');
357
362
  const { runRender } = require('../workflows/render');
@@ -392,6 +397,11 @@ const TOOLS = {
392
397
  parallelism: 4,
393
398
  skipImages,
394
399
  skipTts,
400
+ topic,
401
+ style,
402
+ theme,
403
+ title,
404
+ language,
395
405
  imageWidth: renderWidth,
396
406
  imageHeight: renderHeight,
397
407
  });
@@ -404,6 +414,11 @@ const TOOLS = {
404
414
  musicVolume,
405
415
  renderWidth,
406
416
  renderHeight,
417
+ topic,
418
+ style,
419
+ theme,
420
+ title,
421
+ language,
407
422
  });
408
423
 
409
424
  if (!renderResult.passed) {
@@ -518,8 +533,9 @@ Named presets (set EXTERNAL_PLATFORM_NAME to one of these and omit platformUrl):
518
533
  description: `Agent-first video creation. YOU (the IDE agent) provide the complete scenes array — FrameForge renders it.
519
534
  Features:
520
535
  - Code-driven animation and kinetic typography by default (broadcast quality, zero AI artifacts).
536
+ - Supports direct agent/user code: pass "html" (complete page with window.seek(t)) or "code" (canvas drawing instructions for t).
537
+ - Procedural full-canvas cinematic animations (celestial orbits, DNA helices, candlestick charts, neural nets, 3D wireframe geometry).
521
538
  - Supports manual user clips (video_path) and manual images (image_path).
522
- - Optional AI diffusion generation via enableAiImages: true.
523
539
  - No internal LLM or research required. Primary path for Claude Code, IBM Bob, Cursor, and any agentic IDE.
524
540
 
525
541
  Example scene:
@@ -527,6 +543,8 @@ Example scene:
527
543
  "narration": "The text spoken aloud in this scene",
528
544
  "caption": "OVERLAY TEXT",
529
545
  "visual_description": "Description of the scene visual / animation",
546
+ "html": "<!doctype html>... window.seek = async (t) => { ... }",
547
+ "code": "/* canvas drawing code for t */",
530
548
  "camera_hint": "zoom_in",
531
549
  "music_mood": "energetic",
532
550
  "image_path": "/optional/your/own/image.jpg",
@@ -537,7 +555,7 @@ Example scene:
537
555
  type: 'object',
538
556
  required: ['scenes'],
539
557
  properties: {
540
- scenes: { type: 'array', description: 'Complete scenes array written by the agent. Each scene: { narration, caption, visual_description, camera_hint?, music_mood?, image_path?, video_path?, audio_path?, voice? }' },
558
+ scenes: { type: 'array', description: 'Complete scenes array written by the agent. Each scene: { narration, caption, visual_description, html?, code?, camera_hint?, music_mood?, image_path?, video_path?, audio_path?, voice? }' },
541
559
  title: { type: 'string', description: 'Video title' },
542
560
  orientation: { type: 'string', enum: ['landscape', 'portrait'], default: 'landscape', description: 'Output orientation. "landscape" = 1920×1080 (16:9, YouTube/desktop). "portrait" = 1080×1920 (9:16, TikTok/Reels/Shorts).' },
543
561
  voice: { type: 'string', description: 'edge-tts voice ID for all scenes, e.g. "ml-IN-MidhunNeural" (Malayalam male), "en-US-BrianNeural" (English). Per-scene voice field overrides this. Full list: edge-tts --list-voices' },
@@ -546,10 +564,13 @@ Example scene:
546
564
  parallelism: { type: 'number', default: 1, description: 'Concurrent TTS synthesis requests (default: 1).' },
547
565
  withMusic: { type: 'boolean', default: true },
548
566
  musicVolume: { type: 'number', default: 0.07 },
567
+ topic: { type: 'string', description: 'Overall topic/genre for visual aesthetic and background motion (e.g. "Kerala history", "crypto finance", "AI robotics")' },
568
+ style: { type: 'string', description: 'Visual style/theme override (e.g. cinematic_documentary, finance_executive, cyber_tech, retro_synthwave, nature_organic, hype_sports, minimal_luxury, breaking_news, cosmic_space)' },
569
+ theme: { type: 'string', description: 'Theme preset alias' },
549
570
  productionId: { type: 'string', description: 'Optional — reuse an existing productionId' },
550
571
  },
551
572
  },
552
- handler: async ({ scenes, title = 'FrameForge Video', orientation = 'landscape', voice, language, skipTts = false, parallelism = 1, withMusic = true, musicVolume = 0.07, productionId }) => {
573
+ handler: async ({ scenes, title = 'FrameForge Video', orientation = 'landscape', voice, language, skipTts = false, parallelism = 1, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', productionId }) => {
553
574
  const { runAssets } = require('../workflows/assets');
554
575
  const { runRender } = require('../workflows/render');
555
576
 
@@ -595,6 +616,12 @@ Example scene:
595
616
  video_path: s.video_path || null,
596
617
  audio_path: s.audio_path || null,
597
618
  voice: s.voice || null, // per-scene voice override
619
+ html: s.html || null,
620
+ code: s.code || s.animation_code || null,
621
+ topic: s.topic || topic || '',
622
+ style: s.style || style || '',
623
+ theme: s.theme || theme || '',
624
+ language: s.language || language || '',
598
625
  character: s.character || { character_id: 'none', pose: 'none', emotion: 'neutral', position: { x: 0.5, y: 0.8, scale: 1.0 } },
599
626
  focal_point: s.focal_point || { x: 0.5, y: 0.35, weight: 0.8 },
600
627
  overlay: s.overlay || { type: 'none' },
@@ -608,8 +635,31 @@ Example scene:
608
635
  const renderWidth = orientation === 'portrait' ? 1080 : 1920;
609
636
  const renderHeight = orientation === 'portrait' ? 1920 : 1080;
610
637
 
611
- const assetsResult = await runAssets({ productionId: pid, scenes: processedScenes, parallelism, skipTts, voice: resolvedVoice });
612
- const renderResult = await runRender({ productionId: pid, sceneAssets: assetsResult.sceneAssets, withMusic, musicVolume, renderWidth, renderHeight });
638
+ const assetsResult = await runAssets({
639
+ productionId: pid,
640
+ scenes: processedScenes,
641
+ parallelism,
642
+ skipTts,
643
+ voice: resolvedVoice,
644
+ topic,
645
+ style,
646
+ theme,
647
+ title,
648
+ language,
649
+ });
650
+ const renderResult = await runRender({
651
+ productionId: pid,
652
+ sceneAssets: assetsResult.sceneAssets,
653
+ withMusic,
654
+ musicVolume,
655
+ renderWidth,
656
+ renderHeight,
657
+ topic,
658
+ style,
659
+ theme,
660
+ title,
661
+ language,
662
+ });
613
663
 
614
664
  if (!renderResult.passed) {
615
665
  return { productionId: pid, passed: false, qcReport: renderResult.qcReport };
@@ -1372,7 +1422,7 @@ Operations (used with action="edit"):
1372
1422
 
1373
1423
  const SERVER_INFO = {
1374
1424
  name: 'video-pipeline',
1375
- version: '2.3.5',
1425
+ version: '2.3.6',
1376
1426
  };
1377
1427
 
1378
1428
  const CAPABILITIES = {
@@ -29,7 +29,7 @@ const DEFAULT_FILES_DIR = process.env.N8N_FILES_DIR || path.join(os.homedir(), '
29
29
 
30
30
  const { buildCameraFilter, selectCameraHint } = require('../animation/animation-system');
31
31
  const { AudioMixer } = require('../audio-mixer/mixer');
32
- const { generateSceneHtml } = require('./scene-animator');
32
+ const { generateSceneHtml, wrapCodeInHarnessHtml, detectAesthetic, AESTHETICS } = require('./scene-animator');
33
33
  const { findChromePath } = require('../harness/lib/browser.mjs');
34
34
 
35
35
  // ── Cross-platform Indic / Malayalam font resolver ────────────────────────────
@@ -117,10 +117,16 @@ class VideoRenderer {
117
117
  * narration?: string,
118
118
  * visualDescription?: string,
119
119
  * visualType?: string,
120
+ * topic?: string,
121
+ * style?: string,
122
+ * theme?: string,
123
+ * title?: string,
120
124
  * language?: string,
121
125
  * musicMood?: string,
122
126
  * camera?: string, // camera_hint from scene schema v1.0.0
123
127
  * focalPoint?: { x: number, y: number },
128
+ * html?: string, // direct agent/user pure-t HTML scene
129
+ * code?: string, // direct agent/user canvas seek(t) drawing code
124
130
  * manifest?: ArtifactManifest // optional — enables idempotency
125
131
  * renderWidth?: number, // default 1920
126
132
  * renderHeight?: number, // default 1080
@@ -137,10 +143,16 @@ class VideoRenderer {
137
143
  narration = '',
138
144
  visualDescription = '',
139
145
  visualType = 'code_animation',
146
+ topic = '',
147
+ style = '',
148
+ theme = '',
149
+ title = '',
140
150
  language = '',
141
151
  musicMood = 'neutral',
142
152
  camera,
143
153
  focalPoint,
154
+ html = null,
155
+ code = null,
144
156
  manifest,
145
157
  renderWidth = 1920,
146
158
  renderHeight = 1080,
@@ -212,6 +224,13 @@ class VideoRenderer {
212
224
  caption,
213
225
  narration,
214
226
  visualDescription,
227
+ topic,
228
+ style,
229
+ theme,
230
+ title,
231
+ language,
232
+ html,
233
+ code,
215
234
  clampedDur,
216
235
  renderWidth,
217
236
  renderHeight,
@@ -237,16 +256,13 @@ class VideoRenderer {
237
256
  const cameraFilter = buildCameraFilter(hint, { duration: clampedDur, w: renderWidth, h: renderHeight, ...fx });
238
257
  vf = [cameraFilter, captionFilter].filter(Boolean).join(',');
239
258
  } else {
240
- const PALETTES = [
241
- { bg: '0d1117', c1: '38bdf8' },
242
- { bg: '0f172a', c1: '818cf8' },
243
- { bg: '13111c', c1: 'c084fc' },
244
- { bg: '0c1a24', c1: '34d399' },
245
- { bg: '18181b', c1: 'fbbf24' },
246
- ];
247
- const pal = PALETTES[sceneIndex % PALETTES.length];
259
+ const aestheticKey = detectAesthetic({ topic, style, theme, title, narration, visualDescription, caption, language, musicMood });
260
+ const aesthetic = AESTHETICS[aestheticKey] || AESTHETICS.cyber_tech;
261
+ const pal = aesthetic.palettes[sceneIndex % aesthetic.palettes.length];
262
+ const c0 = (pal.bg0 || '#0d1117').replace('#', '');
263
+ const c1 = (pal.accent || '#38bdf8').replace('#', '');
248
264
  inputArgs = [
249
- '-f', 'lavfi', '-i', `gradients=s=${renderWidth}x${renderHeight}:r=30:c0=0x${pal.bg}:c1=0x${pal.c1}:speed=0.5:type=radial`,
265
+ '-f', 'lavfi', '-i', `gradients=s=${renderWidth}x${renderHeight}:r=30:c0=0x${c0}:c1=0x${c1}:speed=0.5:type=radial`,
250
266
  '-i', voicePath,
251
267
  ];
252
268
  const vfFilters = ['vignette=PI/4'];
@@ -289,6 +305,13 @@ class VideoRenderer {
289
305
  caption,
290
306
  narration,
291
307
  visualDescription,
308
+ topic = '',
309
+ style = '',
310
+ theme = '',
311
+ title = '',
312
+ language = '',
313
+ html = null,
314
+ code = null,
292
315
  clampedDur,
293
316
  renderWidth,
294
317
  renderHeight,
@@ -297,17 +320,40 @@ class VideoRenderer {
297
320
  manifest,
298
321
  musicMood,
299
322
  }) {
300
- const htmlContent = generateSceneHtml({
301
- caption,
302
- narration,
303
- visualDescription,
304
- imagePath,
305
- duration: clampedDur,
306
- width: renderWidth,
307
- height: renderHeight,
308
- sceneIndex,
309
- musicMood,
310
- });
323
+ let htmlContent;
324
+ if (html && typeof html === 'string' && html.trim().length > 0) {
325
+ htmlContent = html;
326
+ if (!htmlContent.includes('window.DURATION')) {
327
+ htmlContent = htmlContent.replace('<script>', `<script>\nwindow.DURATION = ${clampedDur};\n`);
328
+ }
329
+ } else if (code && typeof code === 'string' && code.trim().length > 0) {
330
+ htmlContent = wrapCodeInHarnessHtml({
331
+ code,
332
+ duration: clampedDur,
333
+ width: renderWidth,
334
+ height: renderHeight,
335
+ language,
336
+ style,
337
+ topic,
338
+ });
339
+ } else {
340
+ htmlContent = generateSceneHtml({
341
+ caption,
342
+ narration,
343
+ visualDescription,
344
+ imagePath,
345
+ topic,
346
+ style,
347
+ theme,
348
+ title,
349
+ language,
350
+ duration: clampedDur,
351
+ width: renderWidth,
352
+ height: renderHeight,
353
+ sceneIndex,
354
+ musicMood,
355
+ });
356
+ }
311
357
 
312
358
  const animHtmlPath = path.join(this.vdir, `scene_${sceneIndex}_anim.html`);
313
359
  const framesDir = path.join(this.vdir, `scene_${sceneIndex}_frames`);