@amg555/frameforge-mcp 2.4.2 → 2.4.4
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 +60 -0
- package/out/frames/f000048.jpg +0 -0
- package/out/frames/f000049.jpg +0 -0
- package/out/frames/f000050.jpg +0 -0
- package/out/frames/f000051.jpg +0 -0
- package/out/frames/f000052.jpg +0 -0
- package/out/frames/f000053.jpg +0 -0
- package/out/frames/f000054.jpg +0 -0
- package/out/frames/f000055.jpg +0 -0
- package/out/frames/f000056.jpg +0 -0
- package/out/frames/f000057.jpg +0 -0
- package/out/frames/f000058.jpg +0 -0
- package/out/frames/f000059.jpg +0 -0
- package/package.json +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +5 -0
- package/src/mcp-server/server.js +6 -2
- package/src/video-renderer/renderer.js +18 -8
- package/src/video-renderer/scene-animator.js +486 -286
- package/src/workflows/render.js +2 -0
- package/src/workflows/script.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,66 @@ All notable changes to FrameForge are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.4.4] — 2026-11-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Optional Captions via Prompt / Flag (`src/video-renderer/scene-animator.js`, `src/video-renderer/renderer.js`, `src/cli/make.js`)**:
|
|
11
|
+
- Captions, subtitle pills, and on-screen text are now disabled by default (`SHOW_CAPTIONS = false`).
|
|
12
|
+
- Added `detectCaptionsRequested(text)` to automatically enable captions when explicitly asked in prompt (e.g. `with captions`, `show captions`, `add subtitles`, `subtitles: true`).
|
|
13
|
+
- Added CLI flags `--captions`, `--subtitles`, `--with-captions` to `src/cli/make.js`.
|
|
14
|
+
- Added `showCaptions` option to MCP tools `create_video_from_script` and `publish_video`.
|
|
15
|
+
- Guarded all FFmpeg video-clip subtitle burn-in filters with `effectiveShowCaptions`.
|
|
16
|
+
|
|
17
|
+
- **Multi-Shot Dynamic Camera Director (`src/video-renderer/scene-animator.js`)**:
|
|
18
|
+
- Scenes longer than 6.5s automatically trigger dynamic multi-shot camera cuts every 5.5s (`shotDur = 5.5`).
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
- **Pacing & Duration Scaling (`src/workflows/script.js`)**:
|
|
22
|
+
- 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.
|
|
23
|
+
- LLM instructions explicitly require 12-25 words per scene to prevent monolithic 30-50s static scenes.
|
|
24
|
+
|
|
25
|
+
### Enhanced
|
|
26
|
+
- **Visual Rendering Quality Polish (`src/video-renderer/scene-animator.js`)**:
|
|
27
|
+
- Ground contact drop shadows (`ellipse(0, legLength, 34, 7)`) anchor all characters to the ground plane.
|
|
28
|
+
- Specular eye highlights and warm rosy cheek blush (`rgba(251, 113, 133, 0.28)`) enhance character expressiveness.
|
|
29
|
+
- Round line caps (`ctx.lineCap = 'round'`, `ctx.lineJoin = 'round'`) for clean, hand-crafted doodle aesthetics.
|
|
30
|
+
|
|
31
|
+
- **Near-Lossless FFmpeg Encoding (`src/video-renderer/renderer.js`)**:
|
|
32
|
+
- Upgraded CRF from 20/22 to `17` (visually lossless).
|
|
33
|
+
- Upgraded encoding preset from `veryfast` to `fast` for superior inter-frame motion estimation.
|
|
34
|
+
- 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.
|
|
35
|
+
- Upgraded audio output stream to 320 kbps AAC stereo.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## [2.4.3] — 2026-11-09
|
|
40
|
+
|
|
41
|
+
### Enhanced
|
|
42
|
+
- **Sentence-Synchronized Subtitles (`src/video-renderer/scene-animator.js`)**:
|
|
43
|
+
- Replaced static slide titles, topic banners, and category tags (`// CHRONICLE`, `// HERITAGE`, top headlines) with sentence-synchronized broadcast subtitles centered at bottom with subtle frosted glass backing pills.
|
|
44
|
+
- Automatically slices narration scripts into sentence beats keyed to duration (`DURATION / sentenceCount`) with smooth spring entrance transitions (`spring(localT, 0.04, 210, 16)`).
|
|
45
|
+
- Word wrapping for long sentences to prevent clipping.
|
|
46
|
+
|
|
47
|
+
- **3-Beat Progressive Choreography & Dynamic Camera (`src/video-renderer/scene-animator.js`)**:
|
|
48
|
+
- Added multi-beat progression across all 9 doodle themes:
|
|
49
|
+
- `doodle_dressing`: Beat 1 browsing wardrobe $\to$ Beat 2 trying on shirt $\to$ Beat 3 smiling & waving in glowing amber mirror.
|
|
50
|
+
- `doodle_shiver_cold`: Beat 1 shivering alone $\to$ Beat 2 "NOT FOR COLD" stamp $\to$ Beat 3 "NOT FOR PROTECTION" stamp with character contemplating.
|
|
51
|
+
- `doodle_hunter_savannah`: Character walking with spear across savannah grass swatting mosquitoes under sun.
|
|
52
|
+
- `doodle_sewing_needle`: Campfire $\to$ loupe zoom into bone needle stitching animal hide $\to$ archaeological timeline badge.
|
|
53
|
+
- `doodle_shame_reflection`: Looking down $\to$ water reflection ripples clear $\to$ blush & Theory of Mind halo.
|
|
54
|
+
- `doodle_tribe_meeting`: Two clans approach $\to$ peace greeting $\to$ social language connection arc.
|
|
55
|
+
- `doodle_thinking_future`: Campfire thinker $\to$ thought cloud $\to$ planning for tomorrow tools & sunrise.
|
|
56
|
+
- `doodle_microscope_lice`: Character scratching itching pelt $\to$ microscope slide $\to$ head vs body lice divergence badge.
|
|
57
|
+
- Added subtle cinematic camera drift and breathing (`camZoom`, `camPanX`, `camPanY`) via progress $p = t / DURATION$.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- **Regex Parsing Bug in Template Literal (`src/video-renderer/scene-animator.js`)**:
|
|
61
|
+
- Fixed syntax error caused by unescaped newline in regex literal inside template literal string.
|
|
62
|
+
- **Theme Detection Priority (`src/video-renderer/scene-animator.js`)**:
|
|
63
|
+
- Prioritized specific narrative themes (`doodle_microscope_lice`, `doodle_shiver_cold`, `doodle_hunter_savannah`, `doodle_tribe_meeting`, `doodle_thinking_future`, `doodle_sewing_needle`, `doodle_shame_reflection`) ahead of general wardrobe dressing to guarantee every scene gets its exact script-matched visual doodle.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
7
67
|
## [2.4.2] — 2026-11-09
|
|
8
68
|
|
|
9
69
|
### Added
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amg555/frameforge-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
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.4', code.includes("version: '2.4.4'"));
|
|
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.4', cfWorker.includes("'2.4.4'"));
|
|
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
|
@@ -99,6 +99,7 @@ function parseArgs(argv) {
|
|
|
99
99
|
topic: '', topicsFile: '', language: 'en',
|
|
100
100
|
style: 'educational comedy', channels: ['file'],
|
|
101
101
|
orientation: 'landscape',
|
|
102
|
+
captions: false, subtitles: false,
|
|
102
103
|
scenesFile: '', scriptFile: '', mediaDir: '',
|
|
103
104
|
shorts: true, dryRun: false,
|
|
104
105
|
noResearch: false, noScript: false, noImages: false, noTts: false, noPublish: false,
|
|
@@ -115,6 +116,9 @@ function parseArgs(argv) {
|
|
|
115
116
|
case '--language': args.language = (argv[++i] || 'en').toLowerCase(); break;
|
|
116
117
|
case '--orientation': args.orientation = (argv[++i] || 'landscape').toLowerCase(); break;
|
|
117
118
|
case '--portrait': args.orientation = 'portrait'; break;
|
|
119
|
+
case '--captions':
|
|
120
|
+
case '--subtitles':
|
|
121
|
+
case '--with-captions': args.captions = true; break;
|
|
118
122
|
case '--style': args.style = argv[++i] || args.style; break;
|
|
119
123
|
case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
|
|
120
124
|
case '--no-research': args.noResearch = true; break;
|
|
@@ -436,6 +440,7 @@ async function runSingleVideo(args, topic, outDir, workflows) {
|
|
|
436
440
|
title,
|
|
437
441
|
style: args.style || '',
|
|
438
442
|
language: args.language,
|
|
443
|
+
showCaptions: Boolean(args.captions || args.subtitles),
|
|
439
444
|
renderWidth,
|
|
440
445
|
renderHeight,
|
|
441
446
|
});
|
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.4',
|
|
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
|
|
|
@@ -285,6 +292,7 @@ class VideoRenderer {
|
|
|
285
292
|
outputPath,
|
|
286
293
|
manifest,
|
|
287
294
|
musicMood,
|
|
295
|
+
showCaptions: effectiveShowCaptions,
|
|
288
296
|
});
|
|
289
297
|
} catch (err) {
|
|
290
298
|
console.warn(`[Renderer] Code-driven browser scene render failed (${err.message}), falling back to procedural animation`);
|
|
@@ -292,7 +300,7 @@ class VideoRenderer {
|
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
// Priority 3: Procedural dynamic animated fallback (dynamic moving gradients / camera drift)
|
|
295
|
-
const captionFilter = this._captionFilter(caption);
|
|
303
|
+
const captionFilter = effectiveShowCaptions ? this._captionFilter(caption) : '';
|
|
296
304
|
let inputArgs = [];
|
|
297
305
|
let vf = '';
|
|
298
306
|
|
|
@@ -324,12 +332,12 @@ class VideoRenderer {
|
|
|
324
332
|
'-af', `apad=whole_dur=${clampedDur}`,
|
|
325
333
|
'-t', String(clampedDur),
|
|
326
334
|
'-map', '0:v:0', '-map', '1:a:0',
|
|
327
|
-
'-c:v', 'libx264', '-preset', '
|
|
335
|
+
'-c:v', 'libx264', '-preset', 'fast', '-crf', '17',
|
|
328
336
|
'-profile:v', 'high', '-level', '4.1',
|
|
329
337
|
'-color_range', 'tv', '-colorspace', 'bt709',
|
|
330
338
|
'-color_primaries', 'bt709', '-color_trc', 'bt709',
|
|
331
|
-
'-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709',
|
|
332
|
-
'-c:a', 'aac', '-b:a', '
|
|
339
|
+
'-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709:aq-mode=3:deblock=-1,-1',
|
|
340
|
+
'-c:a', 'aac', '-b:a', '320k', '-ar', '48000', '-ac', '2',
|
|
333
341
|
'-movflags', '+faststart',
|
|
334
342
|
'-pix_fmt', 'yuv420p',
|
|
335
343
|
outputPath,
|
|
@@ -366,6 +374,7 @@ class VideoRenderer {
|
|
|
366
374
|
outputPath,
|
|
367
375
|
manifest,
|
|
368
376
|
musicMood,
|
|
377
|
+
showCaptions = false,
|
|
369
378
|
}) {
|
|
370
379
|
let htmlContent;
|
|
371
380
|
if (html && typeof html === 'string' && html.trim().length > 0) {
|
|
@@ -399,6 +408,7 @@ class VideoRenderer {
|
|
|
399
408
|
height: renderHeight,
|
|
400
409
|
sceneIndex,
|
|
401
410
|
musicMood,
|
|
411
|
+
showCaptions,
|
|
402
412
|
});
|
|
403
413
|
}
|
|
404
414
|
|
|
@@ -434,13 +444,13 @@ class VideoRenderer {
|
|
|
434
444
|
'-i', framesPattern,
|
|
435
445
|
'-i', voicePath,
|
|
436
446
|
'-map', '0:v:0', '-map', '1:a:0',
|
|
437
|
-
'-c:v', 'libx264', '-preset', '
|
|
447
|
+
'-c:v', 'libx264', '-preset', 'fast', '-crf', '17',
|
|
438
448
|
'-profile:v', 'high', '-level', '4.1',
|
|
439
449
|
'-color_range', 'tv', '-colorspace', 'bt709',
|
|
440
450
|
'-color_primaries', 'bt709', '-color_trc', 'bt709',
|
|
441
|
-
'-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709',
|
|
451
|
+
'-x264-params', 'colorprim=bt709:transfer=bt709:colormatrix=bt709:aq-mode=3:deblock=-1,-1',
|
|
442
452
|
'-pix_fmt', 'yuv420p',
|
|
443
|
-
'-c:a', 'aac', '-b:a', '
|
|
453
|
+
'-c:a', 'aac', '-b:a', '320k', '-ar', '48000', '-ac', '2',
|
|
444
454
|
'-af', `apad=whole_dur=${clampedDur}`,
|
|
445
455
|
'-t', String(clampedDur),
|
|
446
456
|
'-movflags', '+faststart',
|