@amg555/frameforge-mcp 2.4.3 → 2.4.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,62 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.4.5] — 2026-11-10
8
+
9
+ ### Fixed
10
+ - **Browser Code Animation Dispatch in `src/video-renderer/renderer.js`**:
11
+ - Fixed `ReferenceError: camera is not defined` inside `_renderCodeAnimationScene`. Previously, missing parameter destructuring caused the browser animation engine to throw on every scene, silently falling back to static FFmpeg image slideshows.
12
+ - Added full pass-through of `camera`, `visualType`, and `focalPoint` into `_renderCodeAnimationScene`.
13
+ - **Manifest Ingest Preservation (`src/workflows/ingest.js`, `src/workflows/assets.js`)**:
14
+ - `normaliseIngestScene` now retains `prompt`, `style`, and `theme`, and falls back to `raw.prompt` for `visual_description`.
15
+ - Added `--theme` and `--aesthetic` CLI options to `src/cli/make.js`.
16
+
17
+ ### Enhanced
18
+ - **Audio-Driven Continuous Camera Director (`src/video-renderer/scene-animator.js`)**:
19
+ - Replaced hardcoded timer modulo cuts with continuous trajectory over `p = t / window.DURATION`.
20
+ - Camera movements (`slow_zoom_in`, `slow_zoom_out`, `pan_left`, `pan_right`, `tilt_up`, `tilt_down`, `ken_burns`, `static`, and `auto`) now smoothly scale to exact narration length.
21
+ - **Max-Quality Organic Doodle Anatomy (`src/video-renderer/scene-animator.js`)**:
22
+ - Overhauled `drawDoodleCharacter`: replaced straight vertical spine line with contoured human torso (shoulders, ribcage, waist, hips), clavicle accents, articulated knees, and planted feet.
23
+ - Removed forced ground drop shadows, forced rosy cheek blush, and forced specular eye highlights.
24
+ - **Light Parchment Hand-Drawn Inking Aesthetic (`hand_drawn_doodle`)**:
25
+ - Warm parchment paper backgrounds (`#faf7f2`, `#f6f1e8`, `#fdfbf7`) with dark charcoal inking (`#1c1917`), subtle line grain, and organic 12fps line boil.
26
+ - Removed all unwanted text stamps and banners (`NOT FOR COLD`, `THEORY OF MIND`, etc.) from narrative doodle themes.
27
+ - Added `doodle_modern_vs_prehistoric` split-screen comparison theme.
28
+
29
+ ---
30
+
31
+ ## [2.4.4] — 2026-11-09
32
+
33
+ ### Added
34
+ - **Optional Captions via Prompt / Flag (`src/video-renderer/scene-animator.js`, `src/video-renderer/renderer.js`, `src/cli/make.js`)**:
35
+ - Captions, subtitle pills, and on-screen text are now disabled by default (`SHOW_CAPTIONS = false`).
36
+ - Added `detectCaptionsRequested(text)` to automatically enable captions when explicitly asked in prompt (e.g. `with captions`, `show captions`, `add subtitles`, `subtitles: true`).
37
+ - Added CLI flags `--captions`, `--subtitles`, `--with-captions` to `src/cli/make.js`.
38
+ - Added `showCaptions` option to MCP tools `create_video_from_script` and `publish_video`.
39
+ - Guarded all FFmpeg video-clip subtitle burn-in filters with `effectiveShowCaptions`.
40
+
41
+ - **Multi-Shot Dynamic Camera Director (`src/video-renderer/scene-animator.js`)**:
42
+ - Scenes longer than 6.5s automatically trigger dynamic multi-shot camera cuts every 5.5s (`shotDur = 5.5`).
43
+ - Progresses cleanly through 4 cinematic angles: Wide establishing $\to$ Medium push-in $\to$ Action close-up $\to$ Dynamic high angle using spring transition interpolations (`shotCutSpring`), eliminating static camera stagnation.
44
+
45
+ - **Pacing & Duration Scaling (`src/workflows/script.js`)**:
46
+ - Upgraded target scene count formula to `targetDuration ? Math.max(8, Math.ceil(targetDuration / 4.5)) : Math.max(sceneCount, 55)` to guarantee tight 4-6 second scene pacing matching high-retention video essay benchmarks.
47
+ - LLM instructions explicitly require 12-25 words per scene to prevent monolithic 30-50s static scenes.
48
+
49
+ ### Enhanced
50
+ - **Visual Rendering Quality Polish (`src/video-renderer/scene-animator.js`)**:
51
+ - Ground contact drop shadows (`ellipse(0, legLength, 34, 7)`) anchor all characters to the ground plane.
52
+ - Specular eye highlights and warm rosy cheek blush (`rgba(251, 113, 133, 0.28)`) enhance character expressiveness.
53
+ - Round line caps (`ctx.lineCap = 'round'`, `ctx.lineJoin = 'round'`) for clean, hand-crafted doodle aesthetics.
54
+
55
+ - **Near-Lossless FFmpeg Encoding (`src/video-renderer/renderer.js`)**:
56
+ - Upgraded CRF from 20/22 to `17` (visually lossless).
57
+ - Upgraded encoding preset from `veryfast` to `fast` for superior inter-frame motion estimation.
58
+ - Added `-x264-params aq-mode=3:deblock=-1,-1` to eliminate gradient banding on dark animated canvas backgrounds while preserving razor-sharp vector line art.
59
+ - Upgraded audio output stream to 320 kbps AAC stereo.
60
+
61
+ ---
62
+
7
63
  ## [2.4.3] — 2026-11-09
8
64
 
9
65
  ### Enhanced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 21 MCP tools.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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.4.3', code.includes("version: '2.4.3'"));
23
+ check('SERVER_INFO version 2.4.5', code.includes("version: '2.4.5'"));
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('edit_video tool defined', code.includes('edit_video'));
@@ -299,7 +299,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
299
299
  check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
300
300
  check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
301
301
  check('cf-worker returns 429 on limit', cfWorker.includes('429'));
302
- check('cf-worker version 2.4.3', cfWorker.includes("'2.4.3'"));
302
+ check('cf-worker version 2.4.5', cfWorker.includes("'2.4.5'"));
303
303
  check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
304
304
 
305
305
  // ── plugins/skills present ────────────────────────────────────────────────
package/src/cli/make.js CHANGED
@@ -98,7 +98,9 @@ function parseArgs(argv) {
98
98
  mode: 'full-auto',
99
99
  topic: '', topicsFile: '', language: 'en',
100
100
  style: 'educational comedy', channels: ['file'],
101
+ theme: '',
101
102
  orientation: 'landscape',
103
+ captions: false, subtitles: false,
102
104
  scenesFile: '', scriptFile: '', mediaDir: '',
103
105
  shorts: true, dryRun: false,
104
106
  noResearch: false, noScript: false, noImages: false, noTts: false, noPublish: false,
@@ -115,7 +117,12 @@ function parseArgs(argv) {
115
117
  case '--language': args.language = (argv[++i] || 'en').toLowerCase(); break;
116
118
  case '--orientation': args.orientation = (argv[++i] || 'landscape').toLowerCase(); break;
117
119
  case '--portrait': args.orientation = 'portrait'; break;
120
+ case '--captions':
121
+ case '--subtitles':
122
+ case '--with-captions': args.captions = true; break;
118
123
  case '--style': args.style = argv[++i] || args.style; break;
124
+ case '--theme':
125
+ case '--aesthetic': args.theme = argv[++i] || ''; break;
119
126
  case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
120
127
  case '--no-research': args.noResearch = true; break;
121
128
  case '--no-script': args.noScript = true; break;
@@ -419,6 +426,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
419
426
  topic,
420
427
  title,
421
428
  style: args.style || '',
429
+ theme: args.theme || '',
422
430
  language: args.language,
423
431
  voice: resolvedVoice,
424
432
  });
@@ -435,7 +443,9 @@ async function runSingleVideo(args, topic, outDir, workflows) {
435
443
  topic,
436
444
  title,
437
445
  style: args.style || '',
446
+ theme: args.theme || '',
438
447
  language: args.language,
448
+ showCaptions: Boolean(args.captions || args.subtitles),
439
449
  renderWidth,
440
450
  renderHeight,
441
451
  });
@@ -420,6 +420,7 @@ const TOOLS = {
420
420
  theme,
421
421
  title,
422
422
  language,
423
+ showCaptions,
423
424
  });
424
425
 
425
426
  if (!renderResult.passed) {
@@ -584,10 +585,11 @@ Example scene:
584
585
  topic: { type: 'string', description: 'Overall topic/genre for visual aesthetic and background motion (e.g. "Kerala history", "crypto finance", "AI robotics")' },
585
586
  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)' },
586
587
  theme: { type: 'string', description: 'Theme preset alias' },
588
+ showCaptions: { type: 'boolean', default: false, description: 'Display on-screen synchronized subtitles/captions. Default false (clean canvas without text unless explicitly requested in prompt).' },
587
589
  productionId: { type: 'string', description: 'Optional — reuse an existing productionId' },
588
590
  },
589
591
  },
590
- handler: async ({ scenes, title = 'FrameForge Video', orientation = 'landscape', voice, language, skipTts = false, parallelism = 1, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', productionId }) => {
592
+ handler: async ({ scenes, title = 'FrameForge Video', orientation = 'landscape', voice, language, skipTts = false, parallelism = 1, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', showCaptions = false, productionId }) => {
591
593
  const { runAssets } = require('../workflows/assets');
592
594
  const { runRender } = require('../workflows/render');
593
595
 
@@ -663,6 +665,7 @@ Example scene:
663
665
  theme,
664
666
  title,
665
667
  language,
668
+ showCaptions,
666
669
  });
667
670
  const renderResult = await runRender({
668
671
  productionId: pid,
@@ -676,6 +679,7 @@ Example scene:
676
679
  theme,
677
680
  title,
678
681
  language,
682
+ showCaptions,
679
683
  });
680
684
 
681
685
  if (!renderResult.passed) {
@@ -1633,7 +1637,7 @@ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output
1633
1637
 
1634
1638
  const SERVER_INFO = {
1635
1639
  name: 'video-pipeline',
1636
- version: '2.4.3',
1640
+ version: '2.4.5',
1637
1641
  };
1638
1642
 
1639
1643
  const CAPABILITIES = {
@@ -171,7 +171,14 @@ class VideoRenderer {
171
171
  speed = 1.0,
172
172
  clipVolume = 0,
173
173
  colorGrade = null,
174
+ showCaptions = false,
174
175
  } = {}) {
176
+ const { detectCaptionsRequested } = require('./scene-animator');
177
+ const effectiveShowCaptions = Boolean(
178
+ showCaptions ||
179
+ detectCaptionsRequested(`${caption} ${narration} ${topic} ${style} ${visualDescription}`) ||
180
+ process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
181
+ );
175
182
  const outputPath = path.join(this.vdir, `scene_${sceneIndex}_out.mp4`);
176
183
 
177
184
  // Idempotency: skip if already rendered and manifest says so
@@ -197,7 +204,7 @@ class VideoRenderer {
197
204
  const startSec = parseTimestamp(trimStart != null ? trimStart : startTime);
198
205
  const sp = buildSpeedFilters(speed);
199
206
 
200
- const captionFilter = this._captionFilter(caption);
207
+ const captionFilter = effectiveShowCaptions ? this._captionFilter(caption) : '';
201
208
  const scaleFilter = `scale=${renderWidth}:${renderHeight}:force_original_aspect_ratio=increase:flags=lanczos,crop=${renderWidth}:${renderHeight}`;
202
209
  const colorFilter = buildColorFilter(colorGrade);
203
210
 
@@ -271,11 +278,14 @@ class VideoRenderer {
271
278
  caption,
272
279
  narration,
273
280
  visualDescription,
281
+ visualType,
274
282
  topic,
275
283
  style,
276
284
  theme,
277
285
  title,
278
286
  language,
287
+ camera,
288
+ focalPoint,
279
289
  html,
280
290
  code,
281
291
  clampedDur,
@@ -285,6 +295,7 @@ class VideoRenderer {
285
295
  outputPath,
286
296
  manifest,
287
297
  musicMood,
298
+ showCaptions: effectiveShowCaptions,
288
299
  });
289
300
  } catch (err) {
290
301
  console.warn(`[Renderer] Code-driven browser scene render failed (${err.message}), falling back to procedural animation`);
@@ -292,7 +303,7 @@ class VideoRenderer {
292
303
  }
293
304
 
294
305
  // Priority 3: Procedural dynamic animated fallback (dynamic moving gradients / camera drift)
295
- const captionFilter = this._captionFilter(caption);
306
+ const captionFilter = effectiveShowCaptions ? this._captionFilter(caption) : '';
296
307
  let inputArgs = [];
297
308
  let vf = '';
298
309
 
@@ -324,12 +335,12 @@ class VideoRenderer {
324
335
  '-af', `apad=whole_dur=${clampedDur}`,
325
336
  '-t', String(clampedDur),
326
337
  '-map', '0:v:0', '-map', '1:a:0',
327
- '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '22',
338
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '17',
328
339
  '-profile:v', 'high', '-level', '4.1',
329
340
  '-color_range', 'tv', '-colorspace', 'bt709',
330
341
  '-color_primaries', 'bt709', '-color_trc', 'bt709',
331
- '-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709',
332
- '-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
342
+ '-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709:aq-mode=3:deblock=-1,-1',
343
+ '-c:a', 'aac', '-b:a', '320k', '-ar', '48000', '-ac', '2',
333
344
  '-movflags', '+faststart',
334
345
  '-pix_fmt', 'yuv420p',
335
346
  outputPath,
@@ -352,11 +363,14 @@ class VideoRenderer {
352
363
  caption,
353
364
  narration,
354
365
  visualDescription,
366
+ visualType = 'code_animation',
355
367
  topic = '',
356
368
  style = '',
357
369
  theme = '',
358
370
  title = '',
359
371
  language = '',
372
+ camera = 'auto',
373
+ focalPoint = null,
360
374
  html = null,
361
375
  code = null,
362
376
  clampedDur,
@@ -366,6 +380,7 @@ class VideoRenderer {
366
380
  outputPath,
367
381
  manifest,
368
382
  musicMood,
383
+ showCaptions = false,
369
384
  }) {
370
385
  let htmlContent;
371
386
  if (html && typeof html === 'string' && html.trim().length > 0) {
@@ -384,6 +399,7 @@ class VideoRenderer {
384
399
  topic,
385
400
  });
386
401
  } else {
402
+ const effectiveCam = camera || selectCameraHint(sceneIndex, focalPoint || {});
387
403
  htmlContent = generateSceneHtml({
388
404
  caption,
389
405
  narration,
@@ -399,6 +415,9 @@ class VideoRenderer {
399
415
  height: renderHeight,
400
416
  sceneIndex,
401
417
  musicMood,
418
+ showCaptions,
419
+ camera: effectiveCam,
420
+ cameraHint: effectiveCam,
402
421
  });
403
422
  }
404
423
 
@@ -434,13 +453,13 @@ class VideoRenderer {
434
453
  '-i', framesPattern,
435
454
  '-i', voicePath,
436
455
  '-map', '0:v:0', '-map', '1:a:0',
437
- '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20',
456
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '17',
438
457
  '-profile:v', 'high', '-level', '4.1',
439
458
  '-color_range', 'tv', '-colorspace', 'bt709',
440
459
  '-color_primaries', 'bt709', '-color_trc', 'bt709',
441
- '-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709',
460
+ '-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709:aq-mode=3:deblock=-1,-1',
442
461
  '-pix_fmt', 'yuv420p',
443
- '-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
462
+ '-c:a', 'aac', '-b:a', '320k', '-ar', '48000', '-ac', '2',
444
463
  '-af', `apad=whole_dur=${clampedDur}`,
445
464
  '-t', String(clampedDur),
446
465
  '-movflags', '+faststart',