@amg555/frameforge-mcp 2.3.9 → 2.4.1

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 CHANGED
@@ -4,6 +4,43 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.4.1] — 2026-11-09
8
+
9
+ ### Fixed
10
+ - **CLI & npx Invocation Handling (`src/mcp-server/stdio.js`, `package.json`)**:
11
+ - Registered `frameforge-make` binary alias in `package.json` so `npx --package=@amg555/frameforge-mcp frameforge-make` works immediately.
12
+ - Added CLI dispatch interception in `stdio.js` when flags or subcommands (`make`, `--topic`, `doctor`, `setup`, `harness`) are passed, preventing npx CLI invocations from hanging on stdio JSON-RPC stdin.
13
+ - **Language & Orientation Forwarding in CLI (`src/cli/make.js`)**:
14
+ - Added `--orientation <landscape|portrait>` and `--portrait` flags.
15
+ - Forwarded `language` and auto-selected `voice` (`ml-IN-MidhunNeural`, `hi-IN-MadhurNeural`, etc.) to `runAssets` and `runRender`.
16
+ - Forwarded `renderWidth` and `renderHeight` to `runRender` so canvas dimensions match requested orientation.
17
+
18
+ ---
19
+
20
+ ## [2.4.0] — 2026-11-09
21
+
22
+ ### Added
23
+ - **MCP Tool `generate_scene_code` (`src/mcp-server/server.js`, `cloudflare-mcp/src/index.js`)**:
24
+ - Synthesizes pure-function-of-time canvas drawing code for `window.seek(t)` matching the Claude Opus 5.5 / `ClaudeAnimationBase` contract.
25
+ - LLM-assisted scene code synthesis with automatic procedural fallback (supporting celestial orbits, candlestick graphs, neural nets, 3D wireframe polyhedra).
26
+ - Automatically wraps generated code with `wrapCodeInHarnessHtml` for headless Chromium execution.
27
+ - **MCP Tool `preview_scene` (`src/mcp-server/server.js`, `cloudflare-mcp/src/index.js`)**:
28
+ - Renders still frame previews (PNGs) of any scene at specified timestamps (defaults to `[0.1, duration/2, duration - 0.1]`).
29
+ - Implements the Layer 3 inspection / feedback loop, letting IDE agents (Claude Code, Cursor, Windsurf) visually inspect keyframes before encoding.
30
+ - **Automatic Scene Code Synthesis (`src/workflows/assets.js`)**:
31
+ - Automatically invokes `generateSceneCodeWithLLM` when scenes lack pre-rendered media, ensuring all agent-generated videos are true code animations rather than static slideshows.
32
+
33
+ ### Enhanced
34
+ - **Full-Canvas Layout & Dynamic Scaling (`src/video-renderer/scene-animator.js`)**:
35
+ - Eliminated bottom glassmorphic rectangle card that caused PowerPoint-style slideshow appearances.
36
+ - Replaced with floating cinematic kinetic typography (subtle top title, bottom caption at `0.92H`, minimal progress line).
37
+ - Centered visual animations at `0.50H` with 1.4×–1.5× scale expansion.
38
+ - **Playwright Dependencies for Production (`package.json`)**:
39
+ - Moved `playwright` and `playwright-core` from `devDependencies` to production `dependencies` so `npx` and standard npm installs run headless Chromium out of the box without falling back to FFmpeg image loops.
40
+ - Expanded `findChromePath` in `src/harness/lib/browser.mjs` and `scripts/render.mjs` to auto-detect Microsoft Edge (`msedge.exe`), Brave, and system Chromium on Windows, macOS, and Linux.
41
+
42
+ ---
43
+
7
44
  ## [2.3.9] — 2026-11-09
8
45
 
9
46
  ### Enhanced
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.3.9",
4
- "description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 19 MCP tools.",
3
+ "version": "2.4.1",
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"
7
7
  },
8
8
  "main": "src/mcp-server/server.js",
9
9
  "bin": {
10
10
  "frameforge": "src/cli/make.js",
11
+ "frameforge-make": "src/cli/make.js",
11
12
  "frameforge-mcp": "src/mcp-server/stdio.js",
12
13
  "frameforge-setup": "src/cli/setup-keys.js",
13
14
  "frameforge-doctor": "src/cli/doctor.js",
@@ -40,8 +41,9 @@
40
41
  "engines": {
41
42
  "node": ">=18.0.0"
42
43
  },
43
- "devDependencies": {
44
- "playwright": "^1.64.1"
44
+ "dependencies": {
45
+ "playwright": "^1.64.1",
46
+ "playwright-core": "^1.64.1"
45
47
  },
46
48
  "keywords": [
47
49
  "frameforge",
@@ -26,7 +26,8 @@ IDE Agent (IBM Bob / Codex / opencode / Cursor / Windsurf / Claude Code)
26
26
  │
27
27
  ▼ Cloudflare Remote MCP (recommended) ─────────────────────────────
28
28
  │ https://video-mcp.YOUR_SUBDOMAIN.workers.dev/mcp
29
- │ 19 tools: create_video_from_script · edit_video · render_from_scenes
29
+ │ 21 tools: create_video_from_script · edit_video · render_from_scenes
30
+ │ generate_scene_code · preview_scene
30
31
  │ ingest_media · edit_scenes · render_tts · multi_voice_tts
31
32
  │ generate_scene_image · assemble_video · publish_video
32
33
  │ research_topic · write_script · external_platform_call
@@ -145,7 +146,8 @@ FrameForge renders full-canvas, broadcast-quality motion graphics (24fps pure fu
145
146
 
146
147
  - **Option A — Natural Description:** Provide `visual_description` (e.g. "Earth rotating with 4 satellite orbits and microwave beams intersecting"). FrameForge automatically synthesizes a full-canvas procedural 3D/2D animation entity (orbital mechanics, 3D DNA helix, candlestick charts, neural mesh, 3D wireframe polyhedra).
147
148
  - **Option B — Direct Agent Code:** Provide `html` (complete page with `window.DURATION` & `window.seek(t)`) or `code` (canvas drawing instructions for `t`). FrameForge executes your program directly via headless Chromium.
148
- - **Option C — Multilingual & Regional Scripts:** Pass `language: "ml"` (Malayalam), `"hi"` (Hindi), etc. Native DirectWrite/HarfBuzz font stacks (`Nirmala UI`, `Noto Sans Malayalam`) render conjuncts without broken glyphs.
149
+ - **Option C — Generate & Inspect (Layer 3 Loop):** Call `generate_scene_code({ visual_description, style })` to synthesize canvas drawing code. Then call `preview_scene({ html, timestamps: [0.1, 1.5, 2.9] })` to render PNG stills to disk, visually verify the scene in the IDE, tweak if needed, and pass into `create_video_from_script`.
150
+ - **Option D — Multilingual & Regional Scripts:** Pass `language: "ml"` (Malayalam), `"hi"` (Hindi), etc. Native DirectWrite/HarfBuzz font stacks (`Nirmala UI`, `Noto Sans Malayalam`) render conjuncts without broken glyphs.
149
151
 
150
152
  ```js
151
153
  create_video_from_script({
@@ -317,10 +319,11 @@ marks that production FAILED and moves to the next topic.
317
319
  | LLM | openai | `OPENAI_API_KEY` | GPT-4o / o1 |
318
320
  | LLM | ollama | _(none)_ | Local, OLLAMA_BASE_URL |
319
321
  | TTS | edge-tts | _(none)_ | Free, offline, 300+ voices |
322
+ | TTS | voxforge | `VOXFORGE_URL` + `_KEY` | Local neural TTS & cloning (Rust) |
320
323
  | TTS | elevenlabs | `ELEVENLABS_API_KEY` | Voice cloning |
321
324
  | TTS | azure | `AZURE_SPEECH_KEY` | Enterprise |
322
325
  | TTS | openai | `OPENAI_API_KEY` | High quality |
323
- | Visual| code-driven | _(none)_ | Default — kinetic cards & typography |
326
+ | Visual| code-driven | _(none)_ | Default — pure-t canvas animation & kinetic text |
324
327
  | Visual| user-media | _(none)_ | Caller `image_path` / `video_path` |
325
328
 
326
329
  ---
@@ -346,9 +349,9 @@ marks that production FAILED and moves to the next topic.
346
349
  ## Key files
347
350
 
348
351
  ```
349
- src/mcp-server/server.js MCP HTTP server (17 tools, port 3210)
350
- src/mcp-server/stdio.js MCP stdio server (17 tools)
351
- cloudflare-mcp/src/index.js Cloudflare Workers Remote MCP proxy
352
+ src/mcp-server/server.js MCP HTTP server (21 tools, port 3210)
353
+ src/mcp-server/stdio.js MCP stdio server (21 tools)
354
+ cloudflare-mcp/src/index.js Cloudflare Workers Remote MCP proxy (21 tools)
352
355
  src/pipeline/providers.js LLM / TTS / Image abstraction (env-var-switchable providers)
353
356
  src/pipeline/state-machine.js File + PostgreSQL production state
354
357
  src/pipeline/retry-policy.js Exponential backoff, stage timeouts
@@ -20,12 +20,16 @@ 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.3.9', code.includes("version: '2.3.9'"));
23
+ check('SERVER_INFO version 2.4.1', code.includes("version: '2.4.1'"));
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'));
27
+ check('generate_scene_code tool defined', code.includes('generate_scene_code'));
28
+ check('preview_scene tool defined', code.includes('preview_scene'));
27
29
  check('video-editor exists', fs.existsSync('./src/video-editor/editor.js'));
28
30
  check('voxforge TTS provider', code.includes('voxforge'));
31
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
32
+ check('playwright in dependencies', Boolean(pkg.dependencies && pkg.dependencies.playwright));
29
33
 
30
34
  // ── No execSync with shell strings in renderer or extractor ───────────────
31
35
  console.log('\n=== Shell injection check ===');
@@ -295,7 +299,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
295
299
  check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
296
300
  check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
297
301
  check('cf-worker returns 429 on limit', cfWorker.includes('429'));
298
- check('cf-worker version 2.3.9', cfWorker.includes("'2.3.9'"));
302
+ check('cf-worker version 2.4.1', cfWorker.includes("'2.4.1'"));
299
303
  check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
300
304
 
301
305
  // ── plugins/skills present ────────────────────────────────────────────────
package/src/cli/make.js CHANGED
@@ -98,6 +98,7 @@ function parseArgs(argv) {
98
98
  mode: 'full-auto',
99
99
  topic: '', topicsFile: '', language: 'en',
100
100
  style: 'educational comedy', channels: ['file'],
101
+ orientation: 'landscape',
101
102
  scenesFile: '', scriptFile: '', mediaDir: '',
102
103
  shorts: true, dryRun: false,
103
104
  noResearch: false, noScript: false, noImages: false, noTts: false, noPublish: false,
@@ -112,6 +113,8 @@ function parseArgs(argv) {
112
113
  case '--script-file': args.scriptFile = argv[++i] || ''; break;
113
114
  case '--media-dir': args.mediaDir = argv[++i] || ''; break;
114
115
  case '--language': args.language = (argv[++i] || 'en').toLowerCase(); break;
116
+ case '--orientation': args.orientation = (argv[++i] || 'landscape').toLowerCase(); break;
117
+ case '--portrait': args.orientation = 'portrait'; break;
115
118
  case '--style': args.style = argv[++i] || args.style; break;
116
119
  case '--channels': args.channels = (argv[++i] || 'file').split(',').map(s => s.trim()); break;
117
120
  case '--no-research': args.noResearch = true; break;
@@ -399,10 +402,13 @@ async function runSingleVideo(args, topic, outDir, workflows) {
399
402
  // Pass stage-skip flags into scenes so runAssets can honour them
400
403
  const preparedScenes = scenes.map(s => ({
401
404
  ...s,
405
+ language: s.language || args.language,
402
406
  _skipImageGen: skipImages,
403
407
  _skipTts: args.noTts,
404
408
  }));
405
409
 
410
+ const resolvedVoice = LANGUAGE_VOICES[args.language] || null;
411
+
406
412
  const assetsResult = await runAssets({
407
413
  productionId,
408
414
  scenes: preparedScenes,
@@ -413,17 +419,25 @@ async function runSingleVideo(args, topic, outDir, workflows) {
413
419
  topic,
414
420
  title,
415
421
  style: args.style || '',
422
+ language: args.language,
423
+ voice: resolvedVoice,
416
424
  });
417
425
  log('Assets', `✅ ${assetsResult.sceneCount} scenes prepared`);
418
426
 
419
427
  // ── Stage D: Render ────────────────────────────────────────────────────────
420
428
  logSection('Stage D: Video Render (FFmpeg)');
429
+ const renderWidth = args.orientation === 'portrait' ? 1080 : 1920;
430
+ const renderHeight = args.orientation === 'portrait' ? 1920 : 1080;
431
+
421
432
  const renderResult = await runRender({
422
433
  productionId,
423
434
  sceneAssets: assetsResult.sceneAssets,
424
435
  topic,
425
436
  title,
426
437
  style: args.style || '',
438
+ language: args.language,
439
+ renderWidth,
440
+ renderHeight,
427
441
  });
428
442
  if (!renderResult.passed) {
429
443
  throw new Error(`Quality gate FAILED: ${JSON.stringify(renderResult.qcReport?.checks?.filter(c => !c.passed).map(c => c.name))}`);
@@ -14,10 +14,23 @@ export function findChromePath(customPath = null) {
14
14
  process.env.CHROME_PATH,
15
15
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
16
16
  'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
17
+ 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
18
+ 'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
19
+ 'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
20
+ 'C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe',
17
21
  process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe') : null,
22
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Microsoft/Edge/Application/msedge.exe') : null,
23
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'BraveSoftware/Brave-Browser/Application/brave.exe') : null,
18
24
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
25
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
26
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
27
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
19
28
  '/usr/bin/google-chrome',
20
29
  '/usr/bin/google-chrome-stable',
30
+ '/usr/bin/microsoft-edge',
31
+ '/usr/bin/microsoft-edge-stable',
32
+ '/usr/bin/brave-browser',
33
+ '/usr/bin/brave',
21
34
  '/usr/bin/chromium',
22
35
  '/usr/bin/chromium-browser',
23
36
  '/snap/bin/chromium',
@@ -78,7 +91,11 @@ export async function launchHarnessBrowser(opts = {}) {
78
91
  try {
79
92
  ({ chromium } = await import('playwright'));
80
93
  } catch (err) {
81
- throw new Error('Playwright is required. Run: npm install playwright && npx playwright install chromium');
94
+ try {
95
+ ({ chromium } = await import('playwright-core'));
96
+ } catch (errCore) {
97
+ throw new Error('Playwright is required. Run: npm install playwright && npx playwright install chromium');
98
+ }
82
99
  }
83
100
 
84
101
  const execPath = findChromePath(opts.chromePath);
@@ -1518,13 +1518,122 @@ Features:
1518
1518
  },
1519
1519
  },
1520
1520
 
1521
+ // ── Tool 20: Generate Scene Code ──────────────────────────────────────────
1522
+ // Writes pure-function-of-t Canvas drawing code using LLM or procedural synthesis
1523
+ generate_scene_code: {
1524
+ description: `Generate pure-function-of-t JavaScript HTML5 Canvas drawing code for a scene.
1525
+ Matches the Claude Opus 5.5 / ClaudeAnimationBase contract where every frame is a pure mathematical function of time t.
1526
+ Returns the raw code and complete wrapped HTML ready for headless browser execution.`,
1527
+ inputSchema: {
1528
+ type: 'object',
1529
+ required: ['visual_description'],
1530
+ properties: {
1531
+ visual_description: { type: 'string', description: 'Visual direction and motion description of the scene' },
1532
+ caption: { type: 'string', description: 'Headline / key concept' },
1533
+ narration: { type: 'string', description: 'Voiceover narration text' },
1534
+ topic: { type: 'string', description: 'Overall video topic' },
1535
+ style: { type: 'string', description: 'Aesthetic / genre (cosmic_space, cyber_tech, cinematic_documentary, finance_executive, retro_synthwave, nature_organic, hype_sports, minimal_luxury, breaking_news, warm_creative)' },
1536
+ language: { type: 'string', default: 'en', description: 'Language code (e.g. en, ml, hi, ta)' },
1537
+ duration: { type: 'number', default: 3, description: 'Duration in seconds' },
1538
+ width: { type: 'number', default: 1920 },
1539
+ height: { type: 'number', default: 1080 },
1540
+ },
1541
+ },
1542
+ handler: async ({ visual_description, caption = '', narration = '', topic = '', style = '', language = 'en', duration = 3, width = 1920, height = 1080 }) => {
1543
+ const { generateSceneCodeWithLLM, wrapCodeInHarnessHtml, generateSceneHtml } = require('../video-renderer/scene-animator');
1544
+ let code = await generateSceneCodeWithLLM({ visualDescription: visual_description, caption, narration, topic, style, language, duration, width, height });
1545
+ let html;
1546
+ if (code) {
1547
+ html = wrapCodeInHarnessHtml({ code, duration, width, height, language, style, topic });
1548
+ } else {
1549
+ html = generateSceneHtml({ visualDescription: visual_description, caption, narration, topic, style, language, duration, width, height });
1550
+ }
1551
+ return { code: code || '// Procedural animation used', html, duration, width, height, success: true };
1552
+ },
1553
+ },
1554
+
1555
+ // ── Tool 21: Preview Scene ───────────────────────────────────────────────
1556
+ // Renders still preview frames at specified timestamps so the agent can inspect them
1557
+ preview_scene: {
1558
+ description: `Render still frame previews (PNGs) of an animated scene at specified timestamps (default: 0.1s, duration/2, duration - 0.1s).
1559
+ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output, verify fonts, colors, and compositions, and patch the code before encoding. Satisfies Layer 3 verification loop.`,
1560
+ inputSchema: {
1561
+ type: 'object',
1562
+ properties: {
1563
+ code: { type: 'string', description: 'Raw JavaScript canvas drawing code for window.seek(t)' },
1564
+ html: { type: 'string', description: 'Complete self-contained HTML page conforming to window.seek(t)' },
1565
+ visual_description: { type: 'string', description: 'Visual description (used to generate scene if code/html omitted)' },
1566
+ caption: { type: 'string' },
1567
+ narration: { type: 'string' },
1568
+ topic: { type: 'string' },
1569
+ style: { type: 'string' },
1570
+ language: { type: 'string', default: 'en' },
1571
+ duration: { type: 'number', default: 3 },
1572
+ timestamps: { type: 'array', items: { type: 'number' }, description: 'Timestamps in seconds to capture (default: [0.1, duration/2, duration - 0.1])' },
1573
+ width: { type: 'number', default: 1920 },
1574
+ height: { type: 'number', default: 1080 },
1575
+ },
1576
+ },
1577
+ handler: async ({ code, html, visual_description = '', caption = '', narration = '', topic = '', style = '', language = 'en', duration = 3, timestamps, width = 1920, height = 1080 }) => {
1578
+ const { wrapCodeInHarnessHtml, generateSceneHtml } = require('../video-renderer/scene-animator');
1579
+ const { launchHarnessBrowser } = require('../harness/lib/browser.mjs');
1580
+ const { openPageWithVirtualClock } = require('../harness/lib/capture.mjs');
1581
+ const syncFs = require('fs');
1582
+ const pathMod = require('path');
1583
+
1584
+ let htmlContent = html;
1585
+ if (!htmlContent) {
1586
+ if (code) {
1587
+ htmlContent = wrapCodeInHarnessHtml({ code, duration, width, height, language, style, topic });
1588
+ } else {
1589
+ htmlContent = generateSceneHtml({ visualDescription: visual_description, caption, narration, topic, style, language, duration, width, height });
1590
+ }
1591
+ }
1592
+
1593
+ const previewDir = pathMod.join(FILES_DIR, 'previews');
1594
+ syncFs.mkdirSync(previewDir, { recursive: true });
1595
+ const tempHtmlPath = pathMod.join(previewDir, `preview_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.html`);
1596
+ syncFs.writeFileSync(tempHtmlPath, htmlContent, 'utf8');
1597
+
1598
+ const tsList = Array.isArray(timestamps) && timestamps.length > 0
1599
+ ? timestamps
1600
+ : [0.1, Number((duration / 2).toFixed(2)), Number((duration - 0.1).toFixed(2))];
1601
+
1602
+ const { browser } = await launchHarnessBrowser();
1603
+ const captures = [];
1604
+
1605
+ try {
1606
+ const page = await openPageWithVirtualClock(browser, tempHtmlPath, { width, height });
1607
+ const { captureFrame } = require('../harness/lib/capture.mjs');
1608
+ for (const t of tsList) {
1609
+ const buf = await captureFrame(page, t, { type: 'image/png' });
1610
+ const outPng = pathMod.join(previewDir, `still_t${t.toString().replace('.', '_')}_${Date.now()}.png`);
1611
+ syncFs.writeFileSync(outPng, buf);
1612
+ captures.push({ timestamp: t, path: outPng, sizeBytes: buf.length });
1613
+ }
1614
+ } finally {
1615
+ await browser.close();
1616
+ try { syncFs.unlinkSync(tempHtmlPath); } catch (_) {}
1617
+ }
1618
+
1619
+ return {
1620
+ previews: captures,
1621
+ count: captures.length,
1622
+ width,
1623
+ height,
1624
+ duration,
1625
+ message: 'Preview stills captured. You can read the image files from disk to inspect the visual composition.'
1626
+ };
1627
+ },
1628
+ },
1629
+
1521
1630
  };
1522
1631
 
1523
1632
  // ── MCP protocol handler (MCP 2024-11-05 spec) ───────────────────────────
1524
1633
 
1525
1634
  const SERVER_INFO = {
1526
1635
  name: 'video-pipeline',
1527
- version: '2.3.9',
1636
+ version: '2.4.1',
1528
1637
  };
1529
1638
 
1530
1639
  const CAPABILITIES = {
@@ -34,6 +34,34 @@
34
34
  // Load config BEFORE requiring server.js so env vars are set when providers initialise
35
35
  require('../config/load-config').loadConfig();
36
36
 
37
+ // ── CLI Dispatch Fallback ──────────────────────────────────────────────────
38
+ // When run via `npx @amg555/frameforge-mcp` with CLI arguments (e.g. `make`, `--topic`,
39
+ // `doctor`, `setup`, `harness`, or `--help`), dispatch to the CLI tool instead of
40
+ // hanging on stdin JSON-RPC.
41
+ const firstArg = process.argv[2];
42
+ if (firstArg && (firstArg === 'make' || firstArg.startsWith('-') || firstArg === 'doctor' || firstArg === 'setup' || firstArg === 'harness')) {
43
+ if (firstArg === 'doctor') {
44
+ require('../cli/doctor');
45
+ return;
46
+ }
47
+ if (firstArg === 'setup') {
48
+ require('../cli/setup-keys');
49
+ return;
50
+ }
51
+ if (firstArg === 'harness') {
52
+ const harnessBin = require('path').resolve(__dirname, '../harness/bin/harness.mjs');
53
+ const { spawnSync } = require('child_process');
54
+ const res = spawnSync(process.execPath, [harnessBin, ...process.argv.slice(3)], { stdio: 'inherit' });
55
+ process.exit(res.status ?? 0);
56
+ }
57
+ // Otherwise dispatch to make.js (stripping 'make' if present)
58
+ if (firstArg === 'make') {
59
+ process.argv.splice(2, 1);
60
+ }
61
+ require('../cli/make');
62
+ return;
63
+ }
64
+
37
65
  const readline = require('readline');
38
66
  const { handleMCPRequest } = require('./server');
39
67
 
@@ -685,18 +685,18 @@ function drawBackground(t) {
685
685
  // ── Full-Canvas Cinematic Visual Directors ─────────────────────────────────────
686
686
  function drawMainScene(t) {
687
687
  ctx.save();
688
- const enterSpring = spring(t, 0.05);
689
- const sceneScale = Math.min(1.02, Math.max(0.9, enterSpring));
690
- ctx.translate(W / 2, H * 0.40);
688
+ const enterSpring = spring(t, 0.05, 170, 13);
689
+ const sceneScale = Math.min(1.04, Math.max(0.88, enterSpring));
690
+ ctx.translate(W / 2, H * 0.50);
691
691
  ctx.scale(sceneScale, sceneScale);
692
- ctx.translate(-W / 2, -H * 0.40);
692
+ ctx.translate(-W / 2, -H * 0.50);
693
693
 
694
694
  const cx = W / 2;
695
- const cy = H * 0.40;
695
+ const cy = H * 0.50;
696
696
 
697
697
  if (COMP_THEME === 'celestial_orbit') {
698
698
  // 3D Celestial Body (Earth) with orbiting satellites & signal pulses
699
- const earthR = Math.min(W, H) * 0.15;
699
+ const earthR = Math.min(W, H) * 0.22;
700
700
 
701
701
  // Atmospheric halo
702
702
  const haloGrad = ctx.createRadialGradient(cx, cy, earthR * 0.9, cx, cy, earthR * 1.45);
@@ -1168,69 +1168,60 @@ function drawMainScene(t) {
1168
1168
  ctx.restore();
1169
1169
  }
1170
1170
 
1171
- // ── Cinematic Broadcast Lower-Third Overlay ────────────────────────────────────
1171
+ // ── Kinetic Typography & Broadcast Subtitle Overlay ─────────────────────────
1172
1172
  function drawCinematicOverlay(t) {
1173
1173
  ctx.save();
1174
- const textSpring = spring(t, 0.18);
1175
- const bannerY = H * 0.74 + (1 - textSpring) * 45;
1176
- const bannerH = H * 0.18;
1177
- const bannerW = W * 0.88;
1178
- const bannerX = (W - bannerW) / 2;
1179
-
1180
- ctx.globalAlpha = Math.min(1, Math.max(0, textSpring));
1181
-
1182
- // Glassmorphic Lower-Third Banner
1183
- ctx.fillStyle = 'rgba(8, 12, 22, 0.82)';
1184
- ctx.strokeStyle = CARD_BORDER;
1185
- ctx.lineWidth = 1.5;
1186
- ctx.beginPath();
1187
- ctx.roundRect(bannerX, bannerY, bannerW, bannerH, 16);
1188
- ctx.fill();
1189
- ctx.stroke();
1190
-
1191
- // Accent Left Bar
1192
- ctx.fillStyle = ACCENT;
1193
- ctx.beginPath();
1194
- ctx.roundRect(bannerX + 16, bannerY + 16, 6, bannerH - 32, 3);
1195
- ctx.fill();
1196
-
1197
- // Tag Pill
1198
- ctx.font = 'bold 13px monospace';
1199
- ctx.fillStyle = ACCENT;
1200
- ctx.textAlign = 'left';
1201
- ctx.fillText(TAG, bannerX + 38, bannerY + 36);
1174
+ const textSpring = spring(t, 0.12, 190, 15);
1175
+ const headlineAlpha = Math.min(1, Math.max(0, textSpring));
1176
+ ctx.globalAlpha = headlineAlpha;
1177
+
1178
+ // Kinetic Headline at top
1179
+ const capY = H * 0.12 + (1 - textSpring) * -20;
1180
+
1181
+ // Tag Pill - minimal floating badge
1182
+ if (TAG) {
1183
+ ctx.font = 'bold 16px monospace';
1184
+ ctx.fillStyle = ACCENT;
1185
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.85)';
1186
+ ctx.shadowBlur = 10;
1187
+ ctx.textAlign = 'left';
1188
+ ctx.fillText('// ' + TAG, 80, capY - 22);
1189
+ }
1202
1190
 
1203
- // Primary Caption Headline
1204
- ctx.font = FONT_WEIGHT + ' 44px ' + FONT_DISPLAY;
1191
+ // Primary Caption Headline - crisp, heroic typography without opaque cards
1192
+ ctx.font = FONT_WEIGHT + ' 50px ' + FONT_DISPLAY;
1205
1193
  ctx.fillStyle = '#ffffff';
1206
- ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
1207
- ctx.shadowBlur = 16;
1194
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
1195
+ ctx.shadowBlur = 24;
1208
1196
  ctx.shadowOffsetY = 4;
1209
1197
  ctx.textAlign = 'left';
1210
- const cleanCap = CAPTION.length > 55 ? CAPTION.slice(0, 55) + '...' : CAPTION;
1211
- ctx.fillText(cleanCap, bannerX + 38, bannerY + 84);
1198
+ const cleanCap = CAPTION.length > 60 ? CAPTION.slice(0, 60) + '...' : CAPTION;
1199
+ ctx.fillText(cleanCap, 80, capY + 30);
1212
1200
 
1213
- // Subtitle / Narration preview
1201
+ // Subtitle / Spoken Narration - bottom center clean broadcast subtitles
1214
1202
  if (NARRATION) {
1215
- ctx.font = '400 23px ' + FONT_BODY;
1216
- ctx.fillStyle = 'rgba(226, 232, 240, 0.88)';
1217
- ctx.shadowBlur = 8;
1218
- const cleanNarr = NARRATION.length > 95 ? NARRATION.slice(0, 95) + '...' : NARRATION;
1219
- ctx.fillText(cleanNarr, bannerX + 38, bannerY + 120);
1203
+ const subSpring = spring(t, 0.22, 180, 16);
1204
+ ctx.globalAlpha = Math.min(1, Math.max(0, subSpring));
1205
+ ctx.font = '500 28px ' + FONT_BODY;
1206
+ ctx.fillStyle = '#f8fafc';
1207
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.95)';
1208
+ ctx.shadowBlur = 18;
1209
+ ctx.shadowOffsetY = 3;
1210
+ ctx.textAlign = 'center';
1211
+ const cleanNarr = NARRATION.length > 115 ? NARRATION.slice(0, 115) + '...' : NARRATION;
1212
+ ctx.fillText(cleanNarr, W / 2, H * 0.92);
1220
1213
  }
1221
1214
  ctx.restore();
1222
1215
 
1223
- // Bottom Progress Scrubber
1216
+ // Bottom Progress Indicator (minimal line)
1224
1217
  ctx.save();
1225
1218
  const progress = Math.min(1, Math.max(0, t / window.DURATION));
1226
- ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
1227
- ctx.fillRect(0, H - 6, W, 6);
1228
-
1219
+ ctx.globalAlpha = 0.85;
1229
1220
  const progGrad = ctx.createLinearGradient(0, 0, W, 0);
1230
1221
  progGrad.addColorStop(0, ACCENT);
1231
1222
  progGrad.addColorStop(1, ACCENT2);
1232
1223
  ctx.fillStyle = progGrad;
1233
- ctx.fillRect(0, H - 6, W * progress, 6);
1224
+ ctx.fillRect(0, H - 4, W * progress, 4);
1234
1225
  ctx.restore();
1235
1226
  }
1236
1227
 
@@ -1245,9 +1236,68 @@ window.seek = async (t) => {
1245
1236
  </html>`;
1246
1237
  }
1247
1238
 
1239
+ // ── LLM Creative Code Generator ──────────────────────────────────────────────
1240
+ /**
1241
+ * Asks an LLM (Claude, Groq, Gemini, OpenAI, etc.) to write raw pure-function-of-t
1242
+ * HTML5 Canvas animation code for a given scene brief.
1243
+ * Returns raw JavaScript for window.seek = async (t) => { ... }, or null if unavailable.
1244
+ */
1245
+ async function generateSceneCodeWithLLM({
1246
+ visualDescription = '',
1247
+ caption = '',
1248
+ narration = '',
1249
+ topic = '',
1250
+ style = '',
1251
+ language = '',
1252
+ duration = 3,
1253
+ width = 1920,
1254
+ height = 1080,
1255
+ } = {}) {
1256
+ try {
1257
+ const { LLMProvider } = require('../pipeline/providers');
1258
+ const llm = new LLMProvider();
1259
+ const system = `You are an elite creative motion graphics developer writing high-end JavaScript Canvas animation code for video production.
1260
+ You write code where EVERY PIXEL IS A PURE FUNCTION OF TIME t (seconds).
1261
+ AVAILABLE VARIABLES:
1262
+ - canvas, ctx, W (${width}), H (${height}), DURATION (${duration}), FONT_DISPLAY, FONT_BODY
1263
+ AVAILABLE HELPERS:
1264
+ - spring(t, delay, tension, friction), lerp(t, a, b, t0, t1), clamp(val, min, max)
1265
+ - easeIn(t), easeOut(t), easeInOut(t), boilSeed(t, seed), hash(i)
1266
+ RULES:
1267
+ 1. Every element must animate smoothly over time t [0..DURATION].
1268
+ 2. Fill the canvas with rich visual geometry, vectors, animated nodes, characters, particles, or cinematic diagrams matching the prompt.
1269
+ 3. NEVER use Math.random(), setTimeout, setInterval, Date.now(), performance.now().
1270
+ 4. Output ONLY raw executable JavaScript code for the body of window.seek = async (t) => { ... }.
1271
+ 5. NO markdown backticks, NO explanations. Start directly with JavaScript.`;
1272
+
1273
+ const user = `TOPIC: ${topic || 'Visual Explainer'}
1274
+ VISUAL DIRECTION: ${visualDescription || caption || 'Dynamic motion graphics'}
1275
+ KEY PHRASE: ${caption}
1276
+ NARRATION: ${narration}
1277
+ STYLE: ${style || 'cinematic'}
1278
+ DURATION: ${duration}s
1279
+
1280
+ Write the canvas drawing code for time t.`;
1281
+
1282
+ const raw = await llm.chat({ system, user, temperature: 0.7, maxTokens: 4000 });
1283
+ if (!raw) return null;
1284
+ let cleanCode = raw.trim();
1285
+ if (cleanCode.startsWith('```')) {
1286
+ cleanCode = cleanCode.replace(/^```[a-z]*\n?/i, '').replace(/\n?```$/i, '').trim();
1287
+ }
1288
+ if (cleanCode.length > 20 && (cleanCode.includes('ctx.') || cleanCode.includes('fillRect'))) {
1289
+ return cleanCode;
1290
+ }
1291
+ return null;
1292
+ } catch (_) {
1293
+ return null;
1294
+ }
1295
+ }
1296
+
1248
1297
  module.exports = {
1249
1298
  generateSceneHtml,
1250
1299
  wrapCodeInHarnessHtml,
1300
+ generateSceneCodeWithLLM,
1251
1301
  detectAesthetic,
1252
1302
  detectComponentTheme,
1253
1303
  getScriptFontStack,
@@ -182,6 +182,14 @@ async function dispatchJob(job) {
182
182
  const editor = new VideoEditor({ filesDir: FILES_DIR });
183
183
  return editor.editAndAssemble(payload);
184
184
  }
185
+ case 'generate_scene_code': {
186
+ const { TOOLS } = require('../mcp-server/server');
187
+ return TOOLS.generate_scene_code.handler(payload);
188
+ }
189
+ case 'preview_scene': {
190
+ const { TOOLS } = require('../mcp-server/server');
191
+ return TOOLS.preview_scene.handler(payload);
192
+ }
185
193
  case 'full_pipeline': return runFullPipeline(payload, job.id);
186
194
  default: throw new Error(`Unknown job type: ${type}`);
187
195
  }
@@ -102,6 +102,16 @@ async function executeJob(job) {
102
102
  result = await editor.editAndAssemble(job.payload);
103
103
  break;
104
104
  }
105
+ case 'generate_scene_code': {
106
+ const { TOOLS } = require('../mcp-server/server');
107
+ result = await TOOLS.generate_scene_code.handler(job.payload);
108
+ break;
109
+ }
110
+ case 'preview_scene': {
111
+ const { TOOLS } = require('../mcp-server/server');
112
+ result = await TOOLS.preview_scene.handler(job.payload);
113
+ break;
114
+ }
105
115
  default: throw new Error(`Unknown job type: ${job.type}`);
106
116
  }
107
117
  job.status = 'SUCCEEDED';
@@ -73,6 +73,22 @@ async function runAssets({
73
73
  visualType = 'user_image';
74
74
  }
75
75
 
76
+ // Automatically synthesize custom programmatic Canvas animation code with LLM if available
77
+ let sceneCode = scene.code || scene.animation_code || null;
78
+ if (!scene.video_path && !scene.image_path && !scene.html && !sceneCode) {
79
+ try {
80
+ const { generateSceneCodeWithLLM } = require('../video-renderer/scene-animator');
81
+ sceneCode = await generateSceneCodeWithLLM({
82
+ visualDescription: scene.visual_description || scene.narration,
83
+ caption: scene.caption,
84
+ narration: scene.narration,
85
+ topic: scene.topic || topic,
86
+ style: scene.style || style,
87
+ language: scene.language || language,
88
+ });
89
+ } catch (_) {}
90
+ }
91
+
76
92
  // ── TTS / User Audio ───────────────────────────────────────────────
77
93
  // Priority: audio_path (pre-recorded) > TTS synthesis.
78
94
  // --no-tts / skipTts: skip synthesis (scene must have audio_path or silence is used).
@@ -108,7 +124,7 @@ async function runAssets({
108
124
  musicMood: scene.music_mood || 'neutral',
109
125
  language: scene.language || language || '',
110
126
  html: scene.html || null,
111
- code: scene.code || scene.animation_code || null,
127
+ code: sceneCode,
112
128
  startTime: scene.start_time != null ? scene.start_time : scene.trim_start,
113
129
  endTime: scene.end_time != null ? scene.end_time : scene.trim_end,
114
130
  speed: scene.speed != null ? scene.speed : 1.0,