@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
package/src/cli/doctor.js CHANGED
@@ -126,7 +126,27 @@ if (hasPlaywright && hasChromium) {
126
126
  ' (Path 1 via create_video_from_script does not need Playwright)');
127
127
  }
128
128
 
129
- // 6. System Font (for video subtitles/captions)
129
+ // 6. Media Downloader (yt-dlp)
130
+ const { detectYtDlp } = require('../utils/media-downloader');
131
+ if (detectYtDlp()) {
132
+ const ytdlpVer = spawnSync('yt-dlp', ['--version'], { encoding: 'utf8', timeout: 3000, shell: process.platform === 'win32' });
133
+ pass('Media Downloader (yt-dlp)', `available (${(ytdlpVer.stdout || '').trim()})`);
134
+ } else {
135
+ warn('Media Downloader (yt-dlp)', 'yt-dlp not found in PATH (optional for downloading web clips/audio)',
136
+ 'Run "pip install yt-dlp" or download binary from https://github.com/yt-dlp/yt-dlp');
137
+ }
138
+
139
+ // 7. Speech Transcription (Whisper)
140
+ const { detectWhisper } = require('../utils/transcribe');
141
+ const whisperStatus = detectWhisper();
142
+ if (whisperStatus.available) {
143
+ pass('Speech Transcription (Whisper)', `engine: ${whisperStatus.engine} (word-level timestamps & pause detection ready)`);
144
+ } else {
145
+ warn('Speech Transcription (Whisper)', 'Local whisper engine not detected (FFmpeg pause detection fallback active)',
146
+ 'Run "pip install openai-whisper" for local word-level transcription or set OPENAI_API_KEY in .env');
147
+ }
148
+
149
+ // 8. System Font (for video subtitles/captions)
130
150
  const FONT_CANDIDATES = [
131
151
  'C:\\Windows\\Fonts\\arialbd.ttf',
132
152
  'C:\\Windows\\Fonts\\calibrib.ttf',
@@ -188,7 +208,21 @@ if (hasLLMKey) {
188
208
  );
189
209
  }
190
210
 
191
- // 7. Output directory check
211
+ // 10. Upstream Frontier Skills & Repo Sync
212
+ const { getUpstreamSummary } = require('../skills/sync-upstream');
213
+ const upstream = getUpstreamSummary();
214
+ if (upstream.hasUpdates) {
215
+ pass('Upstream Skills', `${upstream.repoCount} frontier repos tracked (${upstream.pendingUpdates.length} updates applied)`);
216
+ for (const u of upstream.pendingUpdates) {
217
+ console.log(` ${COLORS.cyan}↳ [${u.category}] ${u.repo} (${u.userImpact})${COLORS.reset}`);
218
+ }
219
+ } else if (upstream.lastSync) {
220
+ pass('Upstream Skills', `${upstream.repoCount} frontier repos synced (HyperFrames, OneTake, LemoOpuscar, Remotion, Earshot, ClaudeAnimationBase)`);
221
+ } else {
222
+ warn('Upstream Skills', 'Not yet synchronized with upstream frontier repos', 'Run "frameforge sync" to check and apply latest animation and editing techniques');
223
+ }
224
+
225
+ // 11. Output directory check
192
226
  const baseDir = process.env.N8N_FILES_DIR || path.join(process.cwd(), '.frameforge-files');
193
227
  try {
194
228
  fs.mkdirSync(baseDir, { recursive: true });
package/src/cli/make.js CHANGED
@@ -83,16 +83,107 @@ const LANGUAGE_VOICES = {
83
83
  'id': 'id-ID-ArdiNeural', // Indonesian
84
84
  };
85
85
 
86
- // ── Argument parsing ──────────────────────────────────────────────────────────
86
+ // ── Subcommand Dispatcher ─────────────────────────────────────────────────────
87
87
 
88
- // Dispatch to code-driven animation harness if invoked as `frameforge harness ...`
89
- if (process.argv[2] === 'harness') {
88
+ const subCmd = (process.argv[2] || '').toLowerCase();
89
+
90
+ // 1. Dispatch to code-driven animation harness
91
+ if (subCmd === 'harness') {
90
92
  const harnessBin = path.resolve(__dirname, '../harness/bin/harness.mjs');
91
93
  const { spawnSync } = require('child_process');
92
94
  const res = spawnSync(process.execPath, [harnessBin, ...process.argv.slice(3)], { stdio: 'inherit' });
93
95
  process.exit(res.status ?? 0);
94
96
  }
95
97
 
98
+ // 2. Dispatch to media downloader (yt-dlp)
99
+ if (subCmd === 'download' || subCmd === 'ingest') {
100
+ const url = process.argv[3];
101
+ if (!url || url.startsWith('-')) {
102
+ console.error('Usage: frameforge download <url> [--audio] [--subtitles] [--out-dir <dir>] [--filename <name>]');
103
+ process.exit(1);
104
+ }
105
+ const isAudio = process.argv.includes('--audio') || process.argv.includes('-a');
106
+ const sub = process.argv.includes('--subtitles') || process.argv.includes('--subs');
107
+ let outDir, filename;
108
+ for (let i = 4; i < process.argv.length; i++) {
109
+ if (process.argv[i] === '--out-dir' || process.argv[i] === '-o') outDir = process.argv[++i];
110
+ if (process.argv[i] === '--filename' || process.argv[i] === '-f') filename = process.argv[++i];
111
+ }
112
+ const { downloadMediaWithYtDlp, detectYtDlp } = require('../utils/media-downloader');
113
+ if (!detectYtDlp()) {
114
+ console.error('✖ yt-dlp is not installed. Install with: pip install yt-dlp');
115
+ process.exit(1);
116
+ }
117
+ try {
118
+ console.log(`⬇️ Downloading with yt-dlp: ${url}`);
119
+ const res = downloadMediaWithYtDlp(url, { format: isAudio ? 'audio' : 'video', subtitles: sub, outputDir: outDir, filename });
120
+ console.log(`✔ Downloaded successfully to: ${res.localPath}`);
121
+ if (res.subtitlePath) console.log(`✔ Subtitles saved to: ${res.subtitlePath}`);
122
+ if (res.duration) console.log(` Duration: ${res.duration.toFixed(1)}s (${res.width || 0}x${res.height || 0})`);
123
+ process.exit(0);
124
+ } catch (err) {
125
+ console.error(`✖ Download failed: ${err.message}`);
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ // 3. Dispatch to speech transcription (Whisper)
131
+ if (subCmd === 'transcribe') {
132
+ const mediaPath = process.argv[3];
133
+ if (!mediaPath || mediaPath.startsWith('-')) {
134
+ console.error('Usage: frameforge transcribe <mediaPath> [--model <model>] [--language <lang>] [--format <ndjson|srt|json|all>] [--out <path>]');
135
+ process.exit(1);
136
+ }
137
+ let model = 'base', language, format = 'all', outputPath;
138
+ for (let i = 4; i < process.argv.length; i++) {
139
+ if (process.argv[i] === '--model' || process.argv[i] === '-m') model = process.argv[++i];
140
+ if (process.argv[i] === '--language' || process.argv[i] === '-l') language = process.argv[++i];
141
+ if (process.argv[i] === '--format' || process.argv[i] === '-f') format = process.argv[++i];
142
+ if (process.argv[i] === '--out' || process.argv[i] === '-o') outputPath = process.argv[++i];
143
+ }
144
+ const { transcribeAudio, detectWhisper } = require('../utils/transcribe');
145
+ const engine = detectWhisper();
146
+ console.log(`🎙️ Transcribing with Whisper (${engine.engine}, model: ${model}): ${mediaPath}`);
147
+ transcribeAudio(mediaPath, { model, language, format, outputPath })
148
+ .then(res => {
149
+ console.log(`✔ Transcription complete (${res.duration.toFixed(1)}s, ${res.words.length} words, ${res.segments.length} segments, ${res.pauses.length} pauses)`);
150
+ if (res.files) {
151
+ for (const [fmt, p] of Object.entries(res.files)) {
152
+ console.log(` ↳ [${fmt.toUpperCase()}] ${p}`);
153
+ }
154
+ }
155
+ process.exit(0);
156
+ })
157
+ .catch(err => {
158
+ console.error(`✖ Transcription failed: ${err.message}`);
159
+ process.exit(1);
160
+ });
161
+ }
162
+
163
+ // 4. Dispatch to upstream skills synchronizer
164
+ if (subCmd === 'sync' || subCmd === 'sync-skills' || subCmd === 'update') {
165
+ const isSilent = process.argv.includes('--silent');
166
+ const { syncSkills } = require('../skills/sync-upstream');
167
+ syncSkills({ silent: isSilent })
168
+ .then(() => process.exit(0))
169
+ .catch(err => {
170
+ console.error(`✖ Upstream sync failed: ${err.message}`);
171
+ process.exit(1);
172
+ });
173
+ }
174
+
175
+ // 5. Dispatch to doctor
176
+ if (subCmd === 'doctor') {
177
+ require('./doctor');
178
+ return;
179
+ }
180
+
181
+ // 6. Dispatch to setup
182
+ if (subCmd === 'setup') {
183
+ require('./setup-keys');
184
+ return;
185
+ }
186
+
96
187
  function parseArgs(argv) {
97
188
  const args = {
98
189
  mode: 'full-auto',
@@ -151,7 +242,17 @@ function parseArgs(argv) {
151
242
 
152
243
  function printHelp() {
153
244
  console.log(`
154
- FrameForge Make — Flexible Video Generator
245
+ FrameForge — Unified AI Video Production Engine
246
+
247
+ SUBCOMMANDS:
248
+ download <url> Download video/audio from web (YouTube, Twitter, Vimeo, etc.) via yt-dlp
249
+ Options: [--audio] [--subtitles] [--out-dir <dir>] [--filename <name>]
250
+ transcribe <file> Transcribe speech to word-level timestamps & subtitles via Whisper
251
+ Options: [--model <size>] [--language <code>] [--format srt|ndjson|json|all]
252
+ sync Synchronize skills from 9 upstream frontier repositories
253
+ doctor Pre-flight environment diagnostics (FFmpeg, Whisper, yt-dlp, Playwright)
254
+ setup Configure API keys (~/.frameforge/config.json)
255
+ harness Run HTML code-driven animation test harness (Playwright + Chromium)
155
256
 
156
257
  MODES:
157
258
  full-auto (default) Research → Script → AI Images → TTS → Render → Publish
@@ -674,8 +775,11 @@ function summarise(startTime) {
674
775
  console.log(`\n⏱️ Total time: ${mins > 0 ? `${mins}m ` : ''}${secs}s\n`);
675
776
  }
676
777
 
677
- main().catch(err => {
678
- console.error(`\n❌ Fatal error: ${err.message}`);
679
- if (process.env.DEBUG_PROVIDERS) console.error(err.stack);
680
- process.exit(1);
681
- });
778
+ const KNOWN_SUBCMDS = ['harness', 'download', 'ingest', 'transcribe', 'sync', 'sync-skills', 'update', 'doctor', 'setup'];
779
+ if (!KNOWN_SUBCMDS.includes(subCmd)) {
780
+ main().catch(err => {
781
+ console.error(`\n❌ Fatal error: ${err.message}`);
782
+ if (process.env.DEBUG_PROVIDERS) console.error(err.stack);
783
+ process.exit(1);
784
+ });
785
+ }
@@ -162,9 +162,20 @@ async function cmdSheet() {
162
162
  const page = await openPageWithVirtualClock({ browser, htmlPath: HTML_FILE });
163
163
  const outPath = flags.out ? path.resolve(CWD, flags.out) : path.join(CHECK_DIR, 'sheet.jpg');
164
164
 
165
- let times = [0.1, 0.5, 1.0, 1.5, 2.0];
165
+ let times;
166
166
  if (flags.times) {
167
167
  times = flags.times.split(',').map(Number).filter(n => !isNaN(n));
168
+ } else {
169
+ // Auto-derive timestamps from scene duration so long films get full coverage.
170
+ // A 60s film at the old [0.1, 0.5, 1.0, 1.5, 2.0] default covers only the first 3%.
171
+ const duration = await page.evaluate(() =>
172
+ window.DURATION || window.REEL?.duration || 5
173
+ );
174
+ const step = Math.max(0.5, duration / 20);
175
+ const count = Math.min(20, Math.ceil(duration / step));
176
+ times = Array.from({ length: count }, (_, i) =>
177
+ parseFloat(((i + 1) * step).toFixed(2))
178
+ );
168
179
  }
169
180
 
170
181
  console.log(`🖼 Generating Contact Sheet (${times.length} frames) → ${outPath}`);
@@ -307,12 +318,12 @@ async function cmdPixels() {
307
318
  }
308
319
 
309
320
  async function cmdQuality() {
310
- console.log(`🎯 Running Closed-Loop Visual QA Gate (1–10 Scoring) on: ${HTML_PATH}`);
321
+ console.log(`🎯 Running Closed-Loop Visual QA Gate (1–10 Scoring) on: ${HTML_FILE}`);
311
322
  const { browser } = await launchHarnessBrowser();
312
323
  const sheetTarget = path.join(CHECK_DIR, 'sheet.jpg');
313
324
  const res = await verifySceneQuality({
314
325
  browser,
315
- htmlPath: HTML_PATH,
326
+ htmlPath: HTML_FILE,
316
327
  sheetPath: sheetTarget,
317
328
  });
318
329
  await browser.close();
@@ -333,10 +344,32 @@ async function cmdQuality() {
333
344
  }
334
345
  }
335
346
 
347
+ async function cmdLook() {
348
+ console.log(`🔍 Running Stage & Asset Diagnostic Linter: ${HTML_FILE}`);
349
+ const lookCandidates = [
350
+ path.join(CWD, 'scripts', 'look.mjs'),
351
+ path.resolve(__dirname, '../../../plugins/video-pipeline/skills/video-pipeline/assets/project/scripts/look.mjs'),
352
+ path.join(__dirname, 'look.mjs'),
353
+ ];
354
+ const lookScript = lookCandidates.find(p => fs.existsSync(p));
355
+ if (lookScript) {
356
+ const lookArgs = [lookScript];
357
+ if (flags.strict) lookArgs.push('--strict');
358
+ const res = spawnSync('node', lookArgs, { stdio: 'inherit', cwd: CWD });
359
+ if (res.status !== 0) {
360
+ console.error('❌ Look diagnostics failed. Resolve asset or stage errors before rendering.');
361
+ process.exit(1);
362
+ }
363
+ } else {
364
+ console.log(' ⚠️ look.mjs not found in project scripts, skipping DOM linter.');
365
+ }
366
+ }
367
+
336
368
  async function cmdAll() {
337
- console.log('🚀 Running Complete Production Pipeline: Storyboard → Verify → Quality → Frames → Encode → Inspect');
369
+ console.log('🚀 Running Complete Production Pipeline: Doctor → Storyboard → Look → Sheet → Verify → Quality → Frames → Encode');
338
370
  await cmdDoctor();
339
371
  await cmdStoryboard();
372
+ await cmdLook();
340
373
  await cmdSheet();
341
374
  await cmdVerify();
342
375
  await cmdQuality();
@@ -345,10 +378,98 @@ async function cmdAll() {
345
378
  console.log('\n✨ Production complete! Output ready at out/film.mp4');
346
379
  }
347
380
 
381
+ /**
382
+ * Golden stills regression — save or check.
383
+ *
384
+ * harness golden --save Capture 4-keyframe storyboard and write hashes to
385
+ * out/check/golden.json. Run after a video passes QC.
386
+ * harness golden --check Re-capture the same 4 keyframes and compare hashes.
387
+ * Fails if any frame hash changed (refactor broke determinism).
388
+ * harness golden --out <path> Custom path for golden.json (default: out/check/golden.json)
389
+ */
390
+ async function cmdGolden() {
391
+ const goldenPath = flags.out
392
+ ? path.resolve(CWD, flags.out)
393
+ : path.join(CHECK_DIR, 'golden.json');
394
+
395
+ const { browser } = await launchHarnessBrowser({
396
+ softGl: flags['soft-gl'],
397
+ chromePath: flags.chrome,
398
+ });
399
+ const page = await openPageWithVirtualClock({ browser, htmlPath: HTML_FILE });
400
+
401
+ // Resolve duration
402
+ const duration = await page.evaluate(() =>
403
+ window.DURATION || window.REEL?.duration || 5
404
+ );
405
+
406
+ // 4 canonical keyframes (0%, 33%, 66%, 99%)
407
+ const goldenTimes = [
408
+ parseFloat((duration * 0.01).toFixed(3)),
409
+ parseFloat((duration * 0.33).toFixed(3)),
410
+ parseFloat((duration * 0.66).toFixed(3)),
411
+ parseFloat((duration * 0.99).toFixed(3)),
412
+ ];
413
+
414
+ async function captureHashes() {
415
+ const hashes = {};
416
+ for (const t of goldenTimes) {
417
+ const buf = await captureFrame(page, t, { type: 'image/png' });
418
+ const { createHash } = await import('crypto');
419
+ hashes[String(t)] = createHash('sha256').update(buf).digest('hex').slice(0, 16);
420
+ }
421
+ return hashes;
422
+ }
423
+
424
+ if (flags.save) {
425
+ console.log(`🟡 Golden: capturing ${goldenTimes.length} keyframes → ${goldenPath}`);
426
+ const hashes = await captureHashes();
427
+ fs.writeFileSync(goldenPath, JSON.stringify({ htmlFile: HTML_FILE, duration, times: goldenTimes, hashes }, null, 2), 'utf8');
428
+ await browser.close();
429
+ console.log(` ✅ Golden baseline saved: ${goldenPath}`);
430
+ for (const [t, h] of Object.entries(hashes)) {
431
+ console.log(` t=${t}s hash=${h}`);
432
+ }
433
+ } else if (flags.check) {
434
+ if (!fs.existsSync(goldenPath)) {
435
+ console.error(`❌ No golden baseline found at ${goldenPath}. Run: harness golden --save first.`);
436
+ process.exit(1);
437
+ }
438
+ const baseline = JSON.parse(fs.readFileSync(goldenPath, 'utf8'));
439
+ console.log(`🔎 Golden: comparing against baseline (${baseline.times.length} frames) → ${goldenPath}`);
440
+ const current = await captureHashes();
441
+ await browser.close();
442
+
443
+ let passed = true;
444
+ for (const t of baseline.times) {
445
+ const key = String(t);
446
+ const expected = baseline.hashes[key];
447
+ const actual = current[key];
448
+ if (expected !== actual) {
449
+ passed = false;
450
+ console.error(` ❌ REGRESSION at t=${t}s expected=${expected} got=${actual}`);
451
+ } else {
452
+ console.log(` ✅ t=${t}s hash=${actual} (matches)`);
453
+ }
454
+ }
455
+
456
+ if (!passed) {
457
+ console.error('\n❌ GOLDEN CHECK FAILED — refactor broke determinism. Investigate edit.js changes.');
458
+ process.exit(1);
459
+ } else {
460
+ console.log('\n✅ GOLDEN CHECK PASSED — all keyframe hashes match baseline.');
461
+ }
462
+ } else {
463
+ await browser.close();
464
+ console.log('Usage: harness golden --save (capture baseline)\n harness golden --check (verify against baseline)');
465
+ }
466
+ }
467
+
348
468
  try {
349
469
  switch (command) {
350
470
  case 'doctor': await cmdDoctor(); break;
351
471
  case 'storyboard': await cmdStoryboard(); break;
472
+ case 'look': await cmdLook(); break;
352
473
  case 'verify': await cmdVerify(); break;
353
474
  case 'quality': await cmdQuality(); break;
354
475
  case 'sheet': await cmdSheet(); break;
@@ -357,9 +478,10 @@ try {
357
478
  case 'frames': await cmdFrames(); break;
358
479
  case 'encode': await cmdEncode(); break;
359
480
  case 'pixels': await cmdPixels(); break;
481
+ case 'golden': await cmdGolden(); break;
360
482
  case 'all': await cmdAll(); break;
361
483
  default:
362
- console.log(`Usage: harness <doctor|storyboard|verify|quality|sheet|strip|crop|frames|encode|pixels|all> [options]`);
484
+ console.log(`Usage: harness <doctor|storyboard|look|verify|quality|sheet|strip|crop|frames|encode|pixels|golden|all> [options]`);
363
485
  break;
364
486
  }
365
487
  } catch (err) {