@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 +56 -0
- package/package.json +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +10 -0
- package/src/mcp-server/server.js +6 -2
- package/src/video-renderer/renderer.js +27 -8
- package/src/video-renderer/scene-animator.js +402 -203
- package/src/workflows/assets.js +2 -1
- package/src/workflows/ingest.js +4 -1
- package/src/workflows/render.js +2 -0
- package/src/workflows/script.js +4 -4
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
|
+
"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"
|
package/scripts/deep-validate.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
});
|
package/src/mcp-server/server.js
CHANGED
|
@@ -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.
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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',
|