@amg555/frameforge-mcp 2.4.9 → 2.7.0

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +22 -4
  2. package/CHANGELOG.md +174 -0
  3. package/CLAUDE.md +192 -9
  4. package/README.md +47 -6
  5. package/output/harness_test_1794418727139/frames/f000000.jpg +1 -0
  6. package/package.json +4 -3
  7. package/plugins/video-pipeline/.claude-plugin/plugin.json +8 -2
  8. package/plugins/video-pipeline/skills/.upstream-skills.json +143 -0
  9. package/plugins/video-pipeline/skills/html-renderer/SKILL.md +59 -15
  10. package/plugins/video-pipeline/skills/html-renderer/references/animation-patterns.md +609 -0
  11. package/plugins/video-pipeline/skills/html-renderer/references/character-animation.md +388 -0
  12. package/plugins/video-pipeline/skills/html-renderer/references/collage-broll.md +41 -0
  13. package/plugins/video-pipeline/skills/html-renderer/references/design.md +217 -0
  14. package/plugins/video-pipeline/skills/html-renderer/references/directing.md +165 -0
  15. package/plugins/video-pipeline/skills/html-renderer/references/earshot-audio.md +201 -0
  16. package/plugins/video-pipeline/skills/html-renderer/references/engine-api.md +480 -0
  17. package/plugins/video-pipeline/skills/html-renderer/references/film-styles.md +95 -0
  18. package/plugins/video-pipeline/skills/html-renderer/references/hyperframes-motion.md +208 -0
  19. package/plugins/video-pipeline/skills/html-renderer/references/motion-craft.md +1 -0
  20. package/plugins/video-pipeline/skills/html-renderer/references/onetake-camera.md +59 -0
  21. package/plugins/video-pipeline/skills/html-renderer/references/procedural-sfx.md +135 -0
  22. package/plugins/video-pipeline/skills/html-renderer/references/remotion-patterns.md +159 -0
  23. package/plugins/video-pipeline/skills/html-renderer/references/render-contract.md +1 -0
  24. package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +19 -2
  25. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +15 -0
  26. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +21 -0
  27. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1293 -20
  28. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +36 -6
  29. package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +126 -2
  30. package/scripts/deep-validate.js +52 -2
  31. package/src/audio-mixer/mixer.js +594 -63
  32. package/src/cli/doctor.js +36 -2
  33. package/src/cli/make.js +113 -9
  34. package/src/harness/bin/harness.mjs +127 -5
  35. package/src/harness/lib/capture.mjs +215 -192
  36. package/src/harness/lib/verify-quality.mjs +48 -7
  37. package/src/mcp-server/server.js +1020 -61
  38. package/src/skills/sync-upstream.js +290 -0
  39. package/src/utils/media-downloader.js +162 -0
  40. package/src/utils/transcribe.js +341 -0
  41. package/src/video-editor/editor.js +96 -11
  42. package/src/video-renderer/renderer.js +7 -1
  43. package/src/video-renderer/scene-animator.js +143 -1
  44. package/src/workflows/assets.js +4 -0
  45. package/src/workflows/ingest.js +10 -0
  46. package/src/workflows/render.js +2 -0
  47. package/src/workflows/script.js +18 -10
@@ -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);
@@ -49,6 +49,7 @@ const fs = require('fs');
49
49
  const fsp = require('fs').promises;
50
50
  const path = require('path');
51
51
  const crypto = require('crypto');
52
+ const { isWebUrl, downloadMediaWithYtDlp } = require('../utils/media-downloader');
52
53
  // ── Constants ─────────────────────────────────────────────────────────────────
53
54
 
54
55
  const VIDEO_EXTS = new Set(['.mp4','.mov','.avi','.mkv','.webm','.m4v','.flv','.ts']);
@@ -213,6 +214,15 @@ function normaliseIngestScene(raw, index, baseDir = process.cwd()) {
213
214
 
214
215
  function resolveMedia(p) {
215
216
  if (!p) return null;
217
+ if (isWebUrl(p)) {
218
+ try {
219
+ const dl = downloadMediaWithYtDlp(p);
220
+ return dl.localPath;
221
+ } catch (err) {
222
+ console.warn(` ⚠️ Scene ${sceneId}: failed to download media URL ${p}: ${err.message}`);
223
+ return null;
224
+ }
225
+ }
216
226
  const resolved = path.isAbsolute(p) ? p : path.resolve(baseDir, p);
217
227
  if (!fs.existsSync(resolved)) {
218
228
  console.warn(` ⚠️ Scene ${sceneId}: media file not found: ${resolved}`);
@@ -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 } ],',