@amg555/frameforge-mcp 2.4.5 → 2.4.7

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,31 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.4.6] — 2026-11-10
8
+
9
+ ### Enhanced
10
+ - **High-Quality Volumetric 2D Character Anatomy (`src/video-renderer/scene-animator.js`)**:
11
+ - Replaced all single-stroke stick limbs with solid anatomical contoured polygons:
12
+ - Contoured muscular thighs (quadriceps curve), articulated knee joints, gastrocnemius calf curves, and realistic ankles.
13
+ - Solid contoured upper arms and forearms tapering to wrists; fully articulated hands with solid palms, opposable thumbs, and 4 individual fingers for each pose (`natural`, `dress_shirt`, `hold_spear`, `sewing`, `thinker`, `peace_gesture`, `shiver`, `cover_body`).
14
+ - Anatomical head contour with cranium dome, temples, cheekbones, sculpted jawline, chin, and ears with antihelix and lobe.
15
+ - Almond eyes with sclera, colored iris, pupil, specular catchlights, eyelid crease, and blinking animation over pure-$t$.
16
+ - Sculpted nose bridge and nostril curves; expressive lips with oral cavity and teeth line modulated by sine speech waves.
17
+ - Layered volumetric styled hair: modern textured pompadour/fade, or lush flowing prehistoric locks.
18
+ - Modern tailored suit jacket/blazer with notched lapels, shirt collar, silk tie, belt with polished buckle, trousers with center pressed crease, and leather oxford shoes.
19
+ - Prehistoric animal hide tunic with ragged deckle hem, rawhide stitching, and carved bone toggle buckle, or bare human body with natural skin contours and 5 distinct toes.
20
+ - **Production-Grade Environmental Backdrops**:
21
+ - `doodle_dressing`: Hardwood herringbone parquet floor with individual planks and specular reflection sheen, architectural wainscoting trim, luxury dark walnut wardrobe closet with warm internal LED downlight, upper storage shelf, and 4 styled garments on wooden hangers, alongside an arched full-length brass mirror with crystal reflection.
22
+ - `doodle_modern_vs_prehistoric`: Split-screen comparison featuring a modern penthouse with twilight city skyline and illuminated skyscrapers on the left, an African savannah sunset with volcanic mountain ridges and flat-canopy acacia trees on the right, joined by a central glowing timeline rift (`300,000 BCE ───▶ PRESENT`).
23
+ - `doodle_shiver_cold`: Frigid Ice Age glacial valley with jagged snow-covered crags, wind-carved snow dunes, howling blizzard streaks, breath condensation plumes, and volumetric golden dawn sunbeams.
24
+ - `doodle_hunter_savannah`: Wide African savannah horizon under glowing sun with swaying golden grass stalks and flaked obsidian/flint spear.
25
+ - `doodle_sewing_needle`: Prehistoric cavern shelter with stone cave walls, ancient cave paintings, glowing campfire hearth with red embers, and high-precision inspection loupe showing extreme close-up of drilled ivory bone needle and sinew stitching.
26
+ - `doodle_shame_reflection`: Tranquil forest pool with mossy river rocks, water ripples, mirrored reflection, and radiant cognitive awareness halo.
27
+ - `doodle_tribe_meeting`: Misty valley with monolithic standing stones, two tribal figures meeting in peace, and glowing golden connection arc.
28
+ - `doodle_thinking_future`: Primeval starlit night sky with crescent moon, granite boulder, and luminous thought constellation.
29
+
30
+ ---
31
+
7
32
  ## [2.4.5] — 2026-11-10
8
33
 
9
34
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.4.5",
3
+ "version": "2.4.7",
4
4
  "description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 21 MCP tools.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,7 +31,7 @@
31
31
  "start:local-render": "cross-env RENDER_MODE=full WORKER_PORT=3211 node src/worker/server-render-free.js",
32
32
  "db:migrate": "node scripts/db-migrate.js",
33
33
  "validate": "node scripts/deep-validate.js",
34
- "test": "node --test src/pipeline/retry-policy.test.js src/pipeline/telemetry.test.js src/pipeline/artifact-manifest.test.js src/pipeline/artifact-retention.test.js src/pipeline/external-adapter.test.js src/workflows/publishing.test.js src/workflows/script.test.js src/animation/animation-system.test.js src/fact-checker/review-gate.test.js src/fact-checker/checker.test.js src/utils/log-redaction.test.js src/worker/server-render-free.test.js src/worker/server.test.js src/mcp-server/server.test.js src/quality-gate/gate.test.js src/shorts-extractor/extractor.test.js src/audio-mixer/mixer.test.js src/video-renderer/scene-animator.test.js src/video-renderer/renderer.test.js src/video-editor/editor.test.js plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.test.js src/harness/lib/pixels.test.js src/harness/lib/verify-purity.test.js src/harness/harness.test.js src/integration.test.js",
34
+ "test": "node --test src/pipeline/retry-policy.test.js src/pipeline/telemetry.test.js src/pipeline/artifact-manifest.test.js src/pipeline/artifact-retention.test.js src/pipeline/external-adapter.test.js src/workflows/publishing.test.js src/workflows/script.test.js src/workflows/ingest.test.js src/animation/animation-system.test.js src/fact-checker/review-gate.test.js src/fact-checker/checker.test.js src/utils/log-redaction.test.js src/worker/server-render-free.test.js src/worker/server.test.js src/mcp-server/server.test.js src/quality-gate/gate.test.js src/shorts-extractor/extractor.test.js src/audio-mixer/mixer.test.js src/video-renderer/scene-animator.test.js src/video-renderer/renderer.test.js src/video-editor/editor.test.js plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.test.js src/harness/lib/pixels.test.js src/harness/lib/verify-purity.test.js src/harness/harness.test.js src/integration.test.js",
35
35
  "lint": "echo 'No linter configured — add eslint for production'",
36
36
  "check-credentials": "node src/utils/log-redaction.js",
37
37
  "generate-assets": "node scripts/generate-initial-assets.js",
@@ -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.5', code.includes("version: '2.4.5'"));
23
+ check('SERVER_INFO version 2.4.7', code.includes("version: '2.4.7'"));
24
24
  check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
25
25
  check('export_to_editor tool defined', code.includes('export_to_editor'));
26
26
  check('edit_video tool defined', code.includes('edit_video'));
@@ -299,7 +299,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
299
299
  check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
300
300
  check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
301
301
  check('cf-worker returns 429 on limit', cfWorker.includes('429'));
302
- check('cf-worker version 2.4.5', cfWorker.includes("'2.4.5'"));
302
+ check('cf-worker version 2.4.7', cfWorker.includes("'2.4.7'"));
303
303
  check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
304
304
 
305
305
  // ── plugins/skills present ────────────────────────────────────────────────
package/src/cli/make.js CHANGED
@@ -354,21 +354,36 @@ async function runSingleVideo(args, topic, outDir, workflows) {
354
354
  }
355
355
  }
356
356
 
357
- // ── Script file bypass (--script-file): skip research, load approved JSON ──
357
+ // ── Script file bypass (--script-file): skip research, load approved JSON or Markdown ──
358
358
  if (args.scriptFile && !scenes) {
359
359
  logSection('Loading Approved Script');
360
360
  const absScript = path.resolve(process.cwd(), args.scriptFile);
361
361
  if (!fs.existsSync(absScript)) throw new Error(`Script file not found: ${absScript}`);
362
- const raw = JSON.parse(fs.readFileSync(absScript, 'utf8'));
363
- const scriptData = raw.script || raw;
364
- scenes = scriptData.scenes;
365
- productionId = scriptData.productionId || raw.productionId || `script_${Date.now()}`;
366
- title = scriptData.title || topic || 'Video';
367
- description = scriptData.description || '';
368
- tags = scriptData.tags || [];
369
- thumbnailPrompt = scriptData.thumbnailPrompt || null;
370
- totalWords = scriptData.totalWords || scenes?.reduce((n, s) => n + (s.narration || '').split(/\s+/).filter(Boolean).length, 0) || 0;
371
- log('Script', `✅ Loaded ${scenes?.length} scenes from: ${absScript}`);
362
+ const content = fs.readFileSync(absScript, 'utf8');
363
+
364
+ if (/\.(md|markdown)$/i.test(absScript) || (!content.trim().startsWith('{') && !content.trim().startsWith('['))) {
365
+ const { parseMarkdownScript } = require('../workflows/ingest');
366
+ const parsedMd = parseMarkdownScript(content);
367
+ scenes = parsedMd.scenes;
368
+ productionId = `script_${Date.now()}`;
369
+ title = parsedMd.title || topic || 'Video';
370
+ description = '';
371
+ tags = [];
372
+ thumbnailPrompt = null;
373
+ totalWords = scenes?.reduce((n, s) => n + (s.narration || '').split(/\s+/).filter(Boolean).length, 0) || 0;
374
+ log('Script', `✅ Parsed ${scenes?.length} scenes from Markdown: ${absScript}`);
375
+ } else {
376
+ const raw = JSON.parse(content);
377
+ const scriptData = raw.script || raw;
378
+ scenes = scriptData.scenes;
379
+ productionId = scriptData.productionId || raw.productionId || `script_${Date.now()}`;
380
+ title = scriptData.title || topic || 'Video';
381
+ description = scriptData.description || '';
382
+ tags = scriptData.tags || [];
383
+ thumbnailPrompt = scriptData.thumbnailPrompt || null;
384
+ totalWords = scriptData.totalWords || scenes?.reduce((n, s) => n + (s.narration || '').split(/\s+/).filter(Boolean).length, 0) || 0;
385
+ log('Script', `✅ Loaded ${scenes?.length} scenes from: ${absScript}`);
386
+ }
372
387
 
373
388
  if (args.dryRun) {
374
389
  logSection('Dry Run Complete');
@@ -355,9 +355,10 @@ const TOOLS = {
355
355
  theme: { type: 'string', description: 'Theme preset alias' },
356
356
  title: { type: 'string', description: 'Video title' },
357
357
  language: { type: 'string', description: 'BCP-47 language hint (e.g. ml, hi, en, ta)' },
358
+ showCaptions: { type: 'boolean', default: false, description: 'Display on-screen synchronized subtitles/captions. Default false.' },
358
359
  },
359
360
  },
360
- handler: async ({ productionId, scenes, orientation = 'landscape', skipImages = false, skipTts = false, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', title = '', language = '' }) => {
361
+ handler: async ({ productionId, scenes, orientation = 'landscape', skipImages = false, skipTts = false, withMusic = true, musicVolume = 0.07, topic = '', style = '', theme = '', title = '', language = '', showCaptions = false }) => {
361
362
  const syncFs = require('fs');
362
363
  const { runAssets } = require('../workflows/assets');
363
364
  const { runRender } = require('../workflows/render');
@@ -641,6 +642,9 @@ Example scene:
641
642
  style: s.style || style || '',
642
643
  theme: s.theme || theme || '',
643
644
  language: s.language || language || '',
645
+ duration: s.duration != null ? Number(s.duration) : null,
646
+ start_time: s.start_time != null ? Number(s.start_time) : (s.startTime != null ? Number(s.startTime) : null),
647
+ end_time: s.end_time != null ? Number(s.end_time) : (s.endTime != null ? Number(s.endTime) : null),
644
648
  character: s.character || { character_id: 'none', pose: 'none', emotion: 'neutral', position: { x: 0.5, y: 0.8, scale: 1.0 } },
645
649
  focal_point: s.focal_point || { x: 0.5, y: 0.35, weight: 0.8 },
646
650
  overlay: s.overlay || { type: 'none' },
@@ -1637,7 +1641,7 @@ Allows the agent in Claude Code, Cursor, or any IDE to inspect the visual output
1637
1641
 
1638
1642
  const SERVER_INFO = {
1639
1643
  name: 'video-pipeline',
1640
- version: '2.4.5',
1644
+ version: '2.4.7',
1641
1645
  };
1642
1646
 
1643
1647
  const CAPABILITIES = {
@@ -167,16 +167,15 @@ class VideoRenderer {
167
167
  startTime = 0,
168
168
  trimStart,
169
169
  endTime,
170
+ duration = null,
170
171
  trimEnd,
171
172
  speed = 1.0,
172
173
  clipVolume = 0,
173
174
  colorGrade = null,
174
175
  showCaptions = false,
175
176
  } = {}) {
176
- const { detectCaptionsRequested } = require('./scene-animator');
177
177
  const effectiveShowCaptions = Boolean(
178
178
  showCaptions ||
179
- detectCaptionsRequested(`${caption} ${narration} ${topic} ${style} ${visualDescription}`) ||
180
179
  process.env.FRAMEFORGE_SHOW_CAPTIONS === '1'
181
180
  );
182
181
  const outputPath = path.join(this.vdir, `scene_${sceneIndex}_out.mp4`);
@@ -189,12 +188,13 @@ class VideoRenderer {
189
188
 
190
189
  fs.mkdirSync(this.vdir, { recursive: true });
191
190
 
192
- // Duration from voice file.
191
+ // Duration from voice file or explicit scene duration.
193
192
  if (!voicePath || !fs.existsSync(voicePath)) {
194
193
  throw new Error(`Scene ${sceneIndex} render failed: voicePath is missing or does not exist (${voicePath})`);
195
194
  }
196
- const dur = this._getAudioDuration(voicePath);
197
- const clampedDur = Math.max(2, Number.isFinite(dur) ? dur + 0.15 : 3.15);
195
+ const voiceDur = this._getAudioDuration(voicePath);
196
+ const dur = (typeof duration === 'number' && duration > 0) ? Math.max(duration, voiceDur) : voiceDur;
197
+ const clampedDur = Math.max(1, Number.isFinite(dur) ? dur + 0.15 : 3.15);
198
198
 
199
199
  const isVideoInput = Boolean(videoPath) || (videoPath && /\.(mp4|mov|webm|mkv|m4v)$/i.test(videoPath));
200
200