@amg555/frameforge-mcp 2.4.9 → 2.4.10
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/package.json +1 -1
- package/plugins/video-pipeline/skills/html-renderer/references/motion-craft.md +1 -0
- package/plugins/video-pipeline/skills/html-renderer/references/render-contract.md +1 -0
- package/scripts/deep-validate.js +2 -2
- package/src/harness/lib/verify-quality.mjs +1 -0
- package/src/mcp-server/server.js +6 -3
- package/src/video-renderer/renderer.js +7 -1
- package/src/video-renderer/scene-animator.js +143 -1
- package/src/workflows/assets.js +4 -0
- package/src/workflows/render.js +2 -0
- package/src/workflows/script.js +18 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amg555/frameforge-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.10",
|
|
4
4
|
"description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 23 MCP tools.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -89,3 +89,4 @@ Never deliver rendered video blind. The production harness must execute automate
|
|
|
89
89
|
2. **Luminance / Exposure**: Non-blown ($< 96\%$ overexposure).
|
|
90
90
|
3. **Compositional Distribution**: Not corner-confined ($> 12\%$ bbox coverage).
|
|
91
91
|
4. **Motion Delta Verification**: Test frame delta between $t = 30\%$ and $t = 70\%$ to guarantee continuous animation.
|
|
92
|
+
|
|
@@ -63,3 +63,4 @@ Output MP4s must comply with broadcast and streaming color standards:
|
|
|
63
63
|
- **Color Matrix**: `BT.709` tags:
|
|
64
64
|
`-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv`
|
|
65
65
|
- **Audio Synchronization**: Exact audio mux with 18dB voiceover sidechain ducking.
|
|
66
|
+
|
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.10', code.includes("version: '2.4.10'"));
|
|
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'));
|
|
@@ -303,7 +303,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
|
|
|
303
303
|
check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
|
|
304
304
|
check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
|
|
305
305
|
check('cf-worker returns 429 on limit', cfWorker.includes('429'));
|
|
306
|
-
check('cf-worker version 2.4.
|
|
306
|
+
check('cf-worker version 2.4.10', cfWorker.includes("'2.4.10'"));
|
|
307
307
|
check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
|
|
308
308
|
|
|
309
309
|
// ── plugins/skills present ────────────────────────────────────────────────
|
package/src/mcp-server/server.js
CHANGED
|
@@ -96,7 +96,7 @@ const TOOLS = {
|
|
|
96
96
|
claims: { type: 'array' },
|
|
97
97
|
topic: { type: 'string' },
|
|
98
98
|
style: { type: 'string', default: 'informative', description: 'e.g. "educational", "comedic", "documentary", "tutorial", "vlog"' },
|
|
99
|
-
persona: { type: 'string', default: '
|
|
99
|
+
persona: { type: 'string', default: 'narrator', description: 'Who the narrator is, e.g. "narrator", "travel blogger", "tech reviewer"' },
|
|
100
100
|
format: { type: 'string', default: 'YouTube video', description: 'e.g. "YouTube short", "Instagram reel", "long-form tutorial"' },
|
|
101
101
|
language: { type: 'string', default: 'en', description: 'BCP-47 language code for the script' },
|
|
102
102
|
sceneCount: { type: 'number', default: 55, description: 'Target number of scenes' },
|
|
@@ -587,10 +587,11 @@ Example scene:
|
|
|
587
587
|
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)' },
|
|
588
588
|
theme: { type: 'string', description: 'Theme preset alias' },
|
|
589
589
|
showCaptions: { type: 'boolean', default: false, description: 'Display on-screen synchronized subtitles/captions. Default false (clean canvas without text unless explicitly requested in prompt).' },
|
|
590
|
+
showAnchor: { type: 'boolean', default: false, description: 'Display on-screen anchor/presenter character. Default false (no anchor unless explicitly requested in prompt).' },
|
|
590
591
|
productionId: { type: 'string', description: 'Optional — reuse an existing productionId' },
|
|
591
592
|
},
|
|
592
593
|
},
|
|
593
|
-
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 }) => {
|
|
594
|
+
handler: async ({ scenes, title = 'FrameForge Video', orientation = 'landscape', voice, language, skipTts = false, parallelism = 1, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', showCaptions = false, showAnchor = false, productionId }) => {
|
|
594
595
|
const { runAssets } = require('../workflows/assets');
|
|
595
596
|
const { runRender } = require('../workflows/render');
|
|
596
597
|
|
|
@@ -670,6 +671,7 @@ Example scene:
|
|
|
670
671
|
title,
|
|
671
672
|
language,
|
|
672
673
|
showCaptions,
|
|
674
|
+
showAnchor,
|
|
673
675
|
});
|
|
674
676
|
const renderResult = await runRender({
|
|
675
677
|
productionId: pid,
|
|
@@ -684,6 +686,7 @@ Example scene:
|
|
|
684
686
|
title,
|
|
685
687
|
language,
|
|
686
688
|
showCaptions,
|
|
689
|
+
showAnchor,
|
|
687
690
|
});
|
|
688
691
|
|
|
689
692
|
if (!renderResult.passed) {
|
|
@@ -1861,7 +1864,7 @@ Analyzes input clips, segments narrative beats, calculates cut in/out timestamps
|
|
|
1861
1864
|
|
|
1862
1865
|
const SERVER_INFO = {
|
|
1863
1866
|
name: 'video-pipeline',
|
|
1864
|
-
version: '2.4.
|
|
1867
|
+
version: '2.4.10',
|
|
1865
1868
|
};
|
|
1866
1869
|
|
|
1867
1870
|
const CAPABILITIES = {
|
|
@@ -174,11 +174,16 @@ class VideoRenderer {
|
|
|
174
174
|
clipVolume = 0,
|
|
175
175
|
colorGrade = null,
|
|
176
176
|
showCaptions = false,
|
|
177
|
+
showAnchor = false,
|
|
177
178
|
} = {}) {
|
|
178
179
|
const effectiveShowCaptions = Boolean(
|
|
179
180
|
showCaptions ||
|
|
180
181
|
process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
|
|
181
182
|
);
|
|
183
|
+
const effectiveShowAnchor = Boolean(
|
|
184
|
+
showAnchor ||
|
|
185
|
+
process.env.FRAMEFORGE_SHOW_ANCHOR === '1'
|
|
186
|
+
);
|
|
182
187
|
const outputPath = path.join(this.vdir, `scene_${sceneIndex}_out.mp4`);
|
|
183
188
|
|
|
184
189
|
// Idempotency: skip if already rendered and manifest says so
|
|
@@ -419,7 +424,8 @@ class VideoRenderer {
|
|
|
419
424
|
height: renderHeight,
|
|
420
425
|
sceneIndex,
|
|
421
426
|
musicMood,
|
|
422
|
-
showCaptions,
|
|
427
|
+
showCaptions: effectiveShowCaptions,
|
|
428
|
+
showAnchor: effectiveShowAnchor,
|
|
423
429
|
camera: effectiveCam,
|
|
424
430
|
cameraHint: effectiveCam,
|
|
425
431
|
});
|
|
@@ -279,15 +279,147 @@ function detectAesthetic({
|
|
|
279
279
|
return 'cyber_tech';
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
// ── Anchor Requested Detector ───────────────────────────────────────────────
|
|
283
|
+
/** Returns true ONLY if caller explicitly asks for an on-screen anchor, presenter, host, avatar, or talking head in prompt. */
|
|
284
|
+
function detectAnchorRequested(text = '') {
|
|
285
|
+
if (!text || typeof text !== 'string') return false;
|
|
286
|
+
return /(?:with|show|display|include|enable|add)\s+(?:an?\s+)?(?:anchor|presenter|host|avatar|spokesperson|talking\s+head|narrator\s+character)|(?:anchor|presenter|host)\s*[:=]\s*(?:true|yes|1|on|always)/i.test(text);
|
|
287
|
+
}
|
|
288
|
+
|
|
282
289
|
// ── Component Theme Detection ──────────────────────────────────────────────────
|
|
283
290
|
/**
|
|
284
291
|
* Detect the graphic entity to render on the full canvas for a given scene.
|
|
285
292
|
*/
|
|
286
|
-
function detectComponentTheme({
|
|
293
|
+
function detectComponentTheme({
|
|
294
|
+
visualDescription = '',
|
|
295
|
+
visual_description = '',
|
|
296
|
+
prompt = '',
|
|
297
|
+
caption = '',
|
|
298
|
+
narration = '',
|
|
299
|
+
topic = '',
|
|
300
|
+
imagePath = null,
|
|
301
|
+
aesthetic = 'cyber_tech',
|
|
302
|
+
anchorWanted = true,
|
|
303
|
+
} = {}) {
|
|
287
304
|
if (imagePath) return 'image';
|
|
288
305
|
const visDesc = visualDescription || visual_description || prompt || '';
|
|
289
306
|
const text = `${visDesc} ${caption} ${narration} ${topic}`.toLowerCase();
|
|
290
307
|
|
|
308
|
+
// If anchor is NOT requested, suppress all humanoid doodle anchor characters
|
|
309
|
+
// and route to non-anchor procedural visual archetypes
|
|
310
|
+
if (!anchorWanted) {
|
|
311
|
+
// 1. Versus / Comparison / Split Screen
|
|
312
|
+
if (/(?:^|[^\w])(versus|vs|comparison|compare|pros and cons|pros vs cons|difference between|dichotomy|contrast|side-by-side|before and after|before vs after|split-screen|split screen|modern vs ancient|modern vs prehistoric|then and now|evolution of human)(?:[^\w]|$)/i.test(text)
|
|
313
|
+
|| /വ്യത്യാസം|താരതമ്യം|എതിര്|പോരാട്ടം|തമ്മിൽ|തുലനം|താരതമ്യപ്പെടുത്തൽ/.test(text)) {
|
|
314
|
+
return 'versus_comparison';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 2. Magnification / Microscope / Fiber / Macro Inspection / Lice
|
|
318
|
+
if (/(?:^|[^\w])(magnify|magnification|zoom|microscopic|microscope|macro|close up|closeup|inspection|loupe|examine|scrutiny|detail|focal point|focus in|specimen|fiber|weave|parasite|lice|louse)(?:[^\w]|$)/i.test(text)
|
|
319
|
+
|| /സൂക്ഷ്മ|പരിശോധന|വലുതാക്കി കാണുക|സൂക്ഷ്മമായി|നിരീക്ഷണം|മൈക്രോസ്കോപ്പ്|പേൻ/.test(text)) {
|
|
320
|
+
return 'magnification_focus';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 3. Cognitive Insight / Psychology / Brain / Eureka / Thought / Future Planning / Shame Reflection
|
|
324
|
+
if (/(?:^|[^\w])(mind|psychology|cognitive|brain|insight|revelation|eureka|intuition|subconscious|thinking|thought|contemplation|intelligence|mental|awareness|neuroscience|concept|idea|epiphany|planning for tomorrow|future planning|foresight|shame|blush|embarrass|mirror|reflection|theory of mind|self-aware|identity)(?:[^\w]|$)/i.test(text)
|
|
325
|
+
|| /മനസ്സ്|ബുദ്ധി|ചിന്ത|മനഃശാസ്ത്രം|ജ്ഞാനം|മസ്തിഷ്കം|ബോധം|കണ്ണാടി|പ്രതിഫലനം|നാണം/.test(text)) {
|
|
326
|
+
return 'cognitive_insight';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 4. Mechanism / Physics / Machinery / Engine / Needle / Stitching / Cold
|
|
330
|
+
if (/(?:^|[^\w])(engine|gear|gears|motor|physics|aerodynamics|thrust|turbine|propulsion|piston|hydraulic|kinetic|machine|machinery|circuit|mechanics|thermodynamics|levers|pulley|needle|bone needle|sew|sewing|stitch|stitching|shiver|shivering|cold|freeze|freezing|ice|winter|protection flaw)(?:[^\w]|$)/i.test(text)
|
|
331
|
+
|| /യന്ത്രം|എഞ്ചിൻ|ഭൗതികശാസ്ത്രം|പ്രവർത്തനം|ചലനം|മെക്കാനിസം|ഭൗതികം|സൂചി|തുന്ന|വിറയ്ക്കുക|തണുപ്പ്/.test(text)) {
|
|
332
|
+
return 'mechanism_physics';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 5. Process / Pipeline / Stages / Assembly / Tribe / Community / Hierarchy / Royal Status
|
|
336
|
+
if (/(?:^|[^\w])(pipeline|workflow|step by step|steps|stages|phase|phases|production line|assembly|manufacturing|lifecycle|funnel|conveyor|sequence|how it works|how it is made|procedure|tribe|tribal|clan|stranger|meeting strangers|social language|status in society|hierarchy|royal robe|royal robes|crown|sceptre|ornaments|status and power|wealth authority)(?:[^\w]|$)/i.test(text)
|
|
337
|
+
|| /ഘട്ടങ്ങൾ|പ്രക്രിയ|ഘട്ടം|ഉൽപ്പാദനം|വഴികൾ|പ്രവർത്തനരീതി|കൂട്ടമായി|സമൂഹം|ഗോത്രം|സാമൂഹിക സ്ഥാനം/.test(text)) {
|
|
338
|
+
return 'process_pipeline';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 6. Radar Globe / World Map / Migration
|
|
342
|
+
if (/(?:^|[^\w])(globe|map|world|geography|radar|international|continent|territory|country|border|location|migration|migrating|migrate|africa highlighted|colder regions|out of africa)(?:[^\w]|$)/i.test(text)
|
|
343
|
+
|| /ഭൂപടം|ലോകം|ഭൂഖണ്ഡം|ഭൂപട|നക്ശാ|ക്ലോബ്|കുടിയേറ്റം|മൈഗ്രേഷൻ/.test(text)) {
|
|
344
|
+
return 'radar_globe';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 7. Celestial / Orbit
|
|
348
|
+
if (/(?:^|[^\w])(orbit|satellite|satellites|gps|planet|earth|mars|moon|galaxy|universe|solar|astronomy|gravity|space|telescope|interstellar)(?:[^\w]|$)/i.test(text)
|
|
349
|
+
|| /ബഹിരാകാശ|ഗ്രഹം|ഉപഗ്രഹ|ഭൂമി|നക്ഷത്ര|പ്രപഞ്ച|अंतरिक्ष|ग्रह|तारा|വിൺവെളി/.test(text)) {
|
|
350
|
+
return 'celestial_orbit';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 8. DNA / Biology
|
|
354
|
+
if (/(?:^|[^\w])(dna|helix|gene|cell|molecule|biology|organism|protein|virus|bacteria|medical|genetic|chromosome)(?:[^\w]|$)/i.test(text)
|
|
355
|
+
|| /ഡിഎൻഎ|കോശം|തന്മാത്ര|ജീവശാസ്ത്ര|डीएनए|कोशिका/.test(text)) {
|
|
356
|
+
return 'dna_helix';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 9. Code / Programming
|
|
360
|
+
if (/(?:^|[^\w])(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct|cli|terminal|bash|shell|npm|git|compiler|linux|rust)(?:[^\w]|$)/.test(text)
|
|
361
|
+
|| /കോഡിംഗ്|സോഫ്റ്റ്വെയർ|പ്രോഗ്രാമിംഗ്|कोड|सॉफ्टवेयर/.test(text)) {
|
|
362
|
+
return 'code';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 10. Chart / Finance
|
|
366
|
+
if (/(?:^|[^\w])(chart|data|analytics|growth|graph|statistic|bars|volume|distribution|stock|crypto|bitcoin|market|invest|trading|inflation|economy|gdp|finance|gain|loss|bull|bear|candlestick|price)(?:[^\w]|$)/.test(text)
|
|
367
|
+
|| /നിക്ഷേപ|ഓഹരി|വിപണി|പണം|ചാർട്ട്|शेयर|बाज़ार/.test(text)) {
|
|
368
|
+
return 'chart';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 11. Nodes / Neural Net
|
|
372
|
+
if (/(?:^|[^\w])(network|nodes|neural network|neural net|distributed system|server mesh|deep learning)(?:[^\w]|$)/.test(text)
|
|
373
|
+
|| /നെറ്റ്വർക്ക്|ശൃംഖല|नेटवर्क/.test(text)) {
|
|
374
|
+
return 'nodes';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 12. 3D Isometric Geometry
|
|
378
|
+
if (/(?:^|[^\w])(cube|geometry|3d|wireframe|isometric|matrix|crystal|structure|polyhedron|dimension|architect|blueprint|math)(?:[^\w]|$)/.test(text)
|
|
379
|
+
|| /ജ്യാമിതി|രൂപം|ത്രിമാന|संरचना/.test(text)) {
|
|
380
|
+
return 'isometric_geometry';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 13. Speed Action / Hunter
|
|
384
|
+
if (/(?:^|[^\w])(speed|sprint|velocity|fast|run|race|motion|hunter|hunting|spear|savannah|blazing sun|mosquito)(?:[^\w]|$)/.test(text)
|
|
385
|
+
|| /വേഗത|ഓട്ടം|കുന്തം|വേഗം/.test(text)) {
|
|
386
|
+
return 'speed_action';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 14. Quote / Metric
|
|
390
|
+
if (/(?:^|[^\w])(quote|saying|proverb|statement|speech|wisdom)(?:[^\w]|$)/.test(text)) return 'quote';
|
|
391
|
+
if (/(?:^|[^\w])(metric|kpi|target|counter|percentage|ratio|score)(?:[^\w]|$)/.test(text)) return 'metric';
|
|
392
|
+
|
|
393
|
+
// 15. Timeline (strictly explicit roadmap/milestone chart requests only)
|
|
394
|
+
if (/(?:^|[^\w])(timeline|roadmap|milestone chart|milestone roadmap)(?:[^\w]|$)/i.test(text)
|
|
395
|
+
|| /ടൈംലൈൻ|റോഡ്മാപ്പ്/.test(text)) {
|
|
396
|
+
return 'timeline';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 16. Explainer Presentation (General visual lecture, whiteboard, tutorial, clothing topic, subscribe)
|
|
400
|
+
if (/(?:^|[^\w])(explain|explainer|presentation|lecture|teach|tutorial|whiteboard|blackboard|breakdown|guide|overview|theory|principles|fundamentals|introduction|summary|lesson|masterclass|subscribe|notification bell|outro|dress|dressing|wearing clothes|shirt|wardrobe|closet|hanger|outfit|wedding dress)(?:[^\w]|$)/i.test(text)
|
|
401
|
+
|| /വിവരണം|പഠിപ്പിക്കുക|ക്ലാസ്|പാഠം|തത്വം|വിവരിക്കുക|സബ്സ്ക്രൈബ്|വസ്ത്രധാരണം/.test(text)) {
|
|
402
|
+
return 'explainer_presentation';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 17. Waveform
|
|
406
|
+
if (/(?:^|[^\w])(waveform|audio|sound|equalizer|frequency|acoustics|hz|decibel|music spectrum|voice waveform)(?:[^\w]|$)/i.test(text)
|
|
407
|
+
|| /ശബ്ദതരംഗം|തരംഗം|സംഗീതം|ധ്വനി/.test(text)) {
|
|
408
|
+
return 'waveform';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Clean aesthetic fallbacks — strictly non-anchor
|
|
412
|
+
if (aesthetic === 'cosmic_space') return 'celestial_orbit';
|
|
413
|
+
if (aesthetic === 'cinematic_documentary') return 'explainer_presentation';
|
|
414
|
+
if (aesthetic === 'finance_executive') return 'chart';
|
|
415
|
+
if (aesthetic === 'nature_organic') return 'dna_helix';
|
|
416
|
+
if (aesthetic === 'breaking_news') return 'radar_globe';
|
|
417
|
+
if (aesthetic === 'cyber_tech') return 'nodes';
|
|
418
|
+
if (aesthetic === 'minimal_luxury') return 'explainer_presentation';
|
|
419
|
+
if (aesthetic === 'hype_sports') return 'speed_action';
|
|
420
|
+
return 'explainer_presentation';
|
|
421
|
+
}
|
|
422
|
+
|
|
291
423
|
// ── Doodled Character Actions (Human Stories, History, Evolution, Daily Life) ──
|
|
292
424
|
// 1. Microscope / Lice Theory / Parasite / Genetics (Scene 7)
|
|
293
425
|
if (/(?:^|[^\w])(lice|louse|microscope|fiber|weave|parasite|lice evolution|genetic biology proof)(?:[^\w]|$)/i.test(text)
|
|
@@ -616,12 +748,18 @@ function generateSceneHtml({
|
|
|
616
748
|
height = 1080,
|
|
617
749
|
sceneIndex = 0,
|
|
618
750
|
showCaptions = false,
|
|
751
|
+
showAnchor = false,
|
|
619
752
|
} = {}) {
|
|
620
753
|
const captionsWanted = Boolean(
|
|
621
754
|
showCaptions ||
|
|
622
755
|
detectCaptionsRequested(prompt || topic) ||
|
|
623
756
|
process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
|
|
624
757
|
);
|
|
758
|
+
const anchorWanted = Boolean(
|
|
759
|
+
showAnchor ||
|
|
760
|
+
detectAnchorRequested(prompt || topic) ||
|
|
761
|
+
process.env.FRAMEFORGE_SHOW_ANCHOR === '1'
|
|
762
|
+
);
|
|
625
763
|
const aestheticKey = detectAesthetic({
|
|
626
764
|
topic,
|
|
627
765
|
style,
|
|
@@ -642,6 +780,7 @@ function generateSceneHtml({
|
|
|
642
780
|
topic,
|
|
643
781
|
imagePath,
|
|
644
782
|
aesthetic: aestheticKey,
|
|
783
|
+
anchorWanted,
|
|
645
784
|
});
|
|
646
785
|
const clampedDur = Math.max(1, Number(duration) || 3);
|
|
647
786
|
|
|
@@ -734,6 +873,7 @@ function spring(t, delay = 0, tension = ${aesthetic.springTension}, friction = $
|
|
|
734
873
|
}
|
|
735
874
|
|
|
736
875
|
const SHOW_CAPTIONS = ${JSON.stringify(captionsWanted)};
|
|
876
|
+
const SHOW_ANCHOR = ${JSON.stringify(anchorWanted)};
|
|
737
877
|
const AESTHETIC = ${JSON.stringify(aestheticKey)};
|
|
738
878
|
const BG_TYPE = ${JSON.stringify(aesthetic.backgroundType)};
|
|
739
879
|
const COMP_THEME = ${JSON.stringify(compTheme)};
|
|
@@ -1006,6 +1146,7 @@ function drawMainScene(t) {
|
|
|
1006
1146
|
showSpecular = false,
|
|
1007
1147
|
seed = 100,
|
|
1008
1148
|
} = {}) {
|
|
1149
|
+
if (!SHOW_ANCHOR) return;
|
|
1009
1150
|
ctx.save();
|
|
1010
1151
|
ctx.translate(x, y);
|
|
1011
1152
|
ctx.scale(scale, scale);
|
|
@@ -4345,6 +4486,7 @@ module.exports = {
|
|
|
4345
4486
|
detectAesthetic,
|
|
4346
4487
|
detectComponentTheme,
|
|
4347
4488
|
detectCaptionsRequested,
|
|
4489
|
+
detectAnchorRequested,
|
|
4348
4490
|
getScriptFontStack,
|
|
4349
4491
|
AESTHETICS,
|
|
4350
4492
|
REGIONAL_FONTS,
|
package/src/workflows/assets.js
CHANGED
|
@@ -35,6 +35,8 @@ async function runAssets({
|
|
|
35
35
|
theme = '',
|
|
36
36
|
title = '',
|
|
37
37
|
language = '',
|
|
38
|
+
showCaptions = false,
|
|
39
|
+
showAnchor = false,
|
|
38
40
|
}) {
|
|
39
41
|
const ttsGen = new TTSProvider();
|
|
40
42
|
const sm = new StateMachine({ mode: 'file', stateDir: STATE_DIR });
|
|
@@ -132,6 +134,8 @@ async function runAssets({
|
|
|
132
134
|
speed: scene.speed != null ? scene.speed : 1.0,
|
|
133
135
|
clipVolume: scene.clip_volume != null ? scene.clip_volume : (scene.clipVolume != null ? scene.clipVolume : 0),
|
|
134
136
|
colorGrade: scene.color_grade || scene.colorGrade || null,
|
|
137
|
+
showCaptions: scene.show_captions !== undefined ? scene.show_captions : (scene.showCaptions !== undefined ? scene.showCaptions : showCaptions),
|
|
138
|
+
showAnchor: scene.show_anchor !== undefined ? scene.show_anchor : (scene.showAnchor !== undefined ? scene.showAnchor : showAnchor),
|
|
135
139
|
};
|
|
136
140
|
}));
|
|
137
141
|
results.push(...batchResults);
|
package/src/workflows/render.js
CHANGED
|
@@ -37,6 +37,7 @@ async function runRender({
|
|
|
37
37
|
title = '',
|
|
38
38
|
language = '',
|
|
39
39
|
showCaptions = false,
|
|
40
|
+
showAnchor = false,
|
|
40
41
|
}) {
|
|
41
42
|
const sm = new StateMachine({ mode: 'file', stateDir: STATE_DIR });
|
|
42
43
|
const retryPol = new RetryPolicy({ maxAttempts: 2, baseDelayMs: 15000 });
|
|
@@ -81,6 +82,7 @@ async function runRender({
|
|
|
81
82
|
clipVolume: asset.clipVolume,
|
|
82
83
|
colorGrade: asset.colorGrade,
|
|
83
84
|
showCaptions: asset.showCaptions !== undefined ? asset.showCaptions : showCaptions,
|
|
85
|
+
showAnchor: asset.showAnchor !== undefined ? asset.showAnchor : showAnchor,
|
|
84
86
|
renderWidth,
|
|
85
87
|
renderHeight,
|
|
86
88
|
});
|
package/src/workflows/script.js
CHANGED
|
@@ -15,6 +15,7 @@ const { LLMProvider } = require('../pipeline/providers');
|
|
|
15
15
|
const { StateMachine } = require('../pipeline/state-machine');
|
|
16
16
|
const { RetryPolicy } = require('../pipeline/retry-policy');
|
|
17
17
|
const { Telemetry } = require('../pipeline/telemetry');
|
|
18
|
+
const { detectAnchorRequested } = require('../video-renderer/scene-animator');
|
|
18
19
|
|
|
19
20
|
const os = require('os');
|
|
20
21
|
const FILES_DIR = process.env.N8N_FILES_DIR || require('path').join(os.homedir(), '.frameforge-files');
|
|
@@ -98,7 +99,7 @@ async function runScript({
|
|
|
98
99
|
claims,
|
|
99
100
|
topic,
|
|
100
101
|
style = 'informative',
|
|
101
|
-
persona = '
|
|
102
|
+
persona = 'narrator',
|
|
102
103
|
format = 'YouTube video',
|
|
103
104
|
language = 'en',
|
|
104
105
|
sceneCount = 55,
|
|
@@ -116,13 +117,16 @@ async function runScript({
|
|
|
116
117
|
|
|
117
118
|
const facts = claims.slice(0, 8).map(c => c.safe_wording || c.claim).join('; ');
|
|
118
119
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
const
|
|
120
|
+
// Detect whether caller explicitly requested an on-screen anchor / presenter character
|
|
121
|
+
const anchorWanted = detectAnchorRequested(topic);
|
|
122
|
+
const effectivePersona = persona !== 'video host' ? persona : (anchorWanted ? 'video host' : 'narrator');
|
|
123
|
+
|
|
124
|
+
// Fast visual pacing: ≈ 2.5-3.2s per scene cut (never monolithic drags)
|
|
125
|
+
const targetScenes = targetDuration ? Math.max(8, Math.ceil(targetDuration / 2.8)) : Math.max(sceneCount, 55);
|
|
122
126
|
|
|
123
127
|
const raw = await retryPol.executeStage('Script', async () => {
|
|
124
128
|
const system = [
|
|
125
|
-
`You are a ${
|
|
129
|
+
`You are a ${effectivePersona} writing a ${format} script.`,
|
|
126
130
|
`Style: ${style}. Language: ${language}.`,
|
|
127
131
|
`Visual aesthetic for image prompts: ${imageStyle}.`,
|
|
128
132
|
'Output ONLY raw JSON. No markdown. Start with { end with }.',
|
|
@@ -136,12 +140,16 @@ async function runScript({
|
|
|
136
140
|
`STYLE: ${style}`,
|
|
137
141
|
`LANGUAGE: ${language}`,
|
|
138
142
|
'',
|
|
139
|
-
`Write exactly ${targetScenes} scenes (schema_version "2.0.0").
|
|
143
|
+
`Write exactly ${targetScenes} scenes (schema_version "2.0.0"). Fast cinematic visual pacing: 2-3.5 seconds per scene (8-16 spoken words). Scene visual cuts must change frequently. Never write long monolithic scenes. Each scene duration matches the spoken voiceover audio.`,
|
|
144
|
+
anchorWanted
|
|
145
|
+
? 'Include an on-screen host/anchor character in the scenes where appropriate.'
|
|
146
|
+
: 'Do NOT include on-screen host or anchor characters unless asked. Set character to { character_id: "none", pose: "none", emotion: "none" }. Visuals must be dynamic cinematic b-roll, motion graphics, animated diagrams, comparisons, processes, and data visualizations.',
|
|
147
|
+
'Each scene must have:',
|
|
140
148
|
'scene_id (integer), schema_version "2.0.0",',
|
|
141
|
-
'narration (
|
|
142
|
-
'caption (
|
|
143
|
-
'visual_description (vivid description of
|
|
144
|
-
'character { character_id (
|
|
149
|
+
'narration (8-18 words in the specified language, spoken aloud),',
|
|
150
|
+
'caption (optional 2-4 uppercase words text overlay only if captions requested, otherwise empty string),',
|
|
151
|
+
'visual_description (vivid description of the visual motion graphics / b-roll / diagram / scene — used for code-driven animation),',
|
|
152
|
+
'character { character_id ("none" or identifier), pose, emotion, position {x,y,scale} },',
|
|
145
153
|
'focal_point {x,y,weight},',
|
|
146
154
|
'overlay { type: "prop"|"effect"|"text"|"none" },',
|
|
147
155
|
'sound_effects [ { effect_id, timing: "start"|"middle"|"end", volume } ],',
|