@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.9",
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
+
@@ -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.9', code.includes("version: '2.4.9'"));
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.9', cfWorker.includes("'2.4.9'"));
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 ────────────────────────────────────────────────
@@ -249,3 +249,4 @@ export async function verifySceneQuality({
249
249
  sheetPath: renderedSheetPath,
250
250
  };
251
251
  }
252
+
@@ -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: 'video host', description: 'Who the narrator is, e.g. "travel blogger", "tech reviewer"' },
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.9',
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({ visualDescription = '', visual_description = '', prompt = '', caption = '', narration = '', topic = '', imagePath = null, aesthetic = 'cyber_tech' } = {}) {
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,
@@ -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);
@@ -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
  });
@@ -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 = 'video host',
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
- // If targetDuration is given, adjust scene count guidance (≈ 4.5-5s per scene for high retention).
120
- // No ceiling — long-form documentaries, lectures, and podcasts scale dynamically.
121
- const targetScenes = targetDuration ? Math.max(8, Math.ceil(targetDuration / 4.5)) : Math.max(sceneCount, 55);
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 ${persona} writing a ${format} script.`,
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"). High visual pacing: 4-6 seconds per scene (12-25 spoken words). Never write monolithic 30-50s static scenes. Each scene must have:`,
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 (15-35 words in the specified language, spoken aloud),',
142
- 'caption (3-6 words UPPERCASE, text overlay),',
143
- 'visual_description (vivid description of what the scene looks like — used as image prompt),',
144
- 'character { character_id (free text or "none"), pose, emotion, position {x,y,scale} },',
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 } ],',