@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
@@ -1,192 +1,215 @@
1
- /**
2
- * capture.mjs — Virtual Clock Injection & Deterministic Frame Stepper
3
- *
4
- * Replaces Date.now, performance.now, requestAnimationFrame, setTimeout/setInterval,
5
- * and CSS animations with a virtual clock. Supports both canvas/WebGL renderAt(t)
6
- * and DOM/canvas seek(t).
7
- */
8
-
9
- import fs from 'fs';
10
- import path from 'path';
11
- import { pathToFileURL } from 'url';
12
-
13
- export const VIRTUAL_CLOCK_INJECTION = `
14
- (function () {
15
- let _now = 0;
16
- let _rafQueue = new Map();
17
- let _rafId = 0;
18
- let _timerQueue = [];
19
- let _timerId = 0;
20
- const _startDate = Date.now();
21
-
22
- window.__playwright = true;
23
-
24
- // 1. Deterministic clocks
25
- performance.now = () => _now;
26
- const OriginalDate = window.Date;
27
- class VirtualDate extends OriginalDate {
28
- constructor(...args) {
29
- if (args.length === 0) {
30
- super(_startDate + _now);
31
- } else {
32
- super(...args);
33
- }
34
- }
35
- static now() {
36
- return Math.floor(_startDate + _now);
37
- }
38
- }
39
- window.Date = VirtualDate;
40
- Date.now = () => _startDate + _now;
41
-
42
- // 2. Deterministic requestAnimationFrame
43
- window.requestAnimationFrame = (cb) => {
44
- const id = ++_rafId;
45
- _rafQueue.set(id, cb);
46
- return id;
47
- };
48
- window.cancelAnimationFrame = (id) => {
49
- _rafQueue.delete(id);
50
- };
51
-
52
- // 3. Virtual timer queue
53
- window.setTimeout = (fn, delay = 0, ...args) => {
54
- const id = ++_timerId;
55
- _timerQueue.push({ id, runAt: _now + Math.max(0, delay), fn, args, repeat: 0 });
56
- _timerQueue.sort((a, b) => a.runAt - b.runAt);
57
- return id;
58
- };
59
- window.clearTimeout = (id) => {
60
- const idx = _timerQueue.findIndex(t => t.id === id);
61
- if (idx !== -1) _timerQueue.splice(idx, 1);
62
- };
63
- window.setInterval = (fn, interval = 0, ...args) => {
64
- const id = ++_timerId;
65
- const step = Math.max(16, interval);
66
- _timerQueue.push({ id, runAt: _now + step, fn, args, repeat: step });
67
- _timerQueue.sort((a, b) => a.runAt - b.runAt);
68
- return id;
69
- };
70
- window.clearInterval = (id) => {
71
- const idx = _timerQueue.findIndex(t => t.id === id);
72
- if (idx !== -1) _timerQueue.splice(idx, 1);
73
- };
74
-
75
- // 4. Deterministic Seeded PRNG fallback for unseeded Math.random()
76
- let _seed = 0x2F6E2B1;
77
- const resetSeed = (tSec) => {
78
- _seed = (0x2F6E2B1 ^ Math.imul(Math.floor(tSec * 1000), 0x9E3779B9)) | 0;
79
- };
80
- window.Math.random = () => {
81
- _seed = (_seed + 0x6D2B79F5) | 0;
82
- let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed);
83
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
84
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
85
- };
86
-
87
- // 5. Virtual time advance
88
- window.__advance = (ms) => {
89
- _now += ms;
90
- while (_timerQueue.length > 0 && _timerQueue[0].runAt <= _now) {
91
- const item = _timerQueue.shift();
92
- try { item.fn(...item.args); } catch (_) {}
93
- if (item.repeat > 0) {
94
- item.runAt = _now + item.repeat;
95
- _timerQueue.push(item);
96
- _timerQueue.sort((a, b) => a.runAt - b.runAt);
97
- }
98
- }
99
- const currentRafs = Array.from(_rafQueue.values());
100
- _rafQueue.clear();
101
- currentRafs.forEach(cb => { try { cb(_now); } catch (_) {} });
102
-
103
- try {
104
- if (document.getAnimations) {
105
- document.getAnimations().forEach(a => {
106
- if (a.playState !== 'paused') a.pause();
107
- a.currentTime = _now;
108
- });
109
- }
110
- } catch (_) {}
111
- };
112
-
113
- // 6. Absolute virtual seek
114
- window.__seek = async (t) => {
115
- _now = t * 1000;
116
- resetSeed(t);
117
-
118
- // Pause & seek CSS animations
119
- try {
120
- if (document.getAnimations) {
121
- document.getAnimations().forEach(a => {
122
- if (a.playState !== 'paused') a.pause();
123
- a.currentTime = _now;
124
- });
125
- }
126
- } catch (_) {}
127
-
128
- if (window.renderAt) {
129
- await window.renderAt(t);
130
- } else if (window.seek) {
131
- await window.seek(t);
132
- }
133
- while (_timerQueue.length > 0 && _timerQueue[0].runAt <= _now) {
134
- const item = _timerQueue.shift();
135
- try { item.fn(...item.args); } catch (_) {}
136
- }
137
- if (document.fonts && document.fonts.ready) {
138
- await document.fonts.ready;
139
- }
140
- };
141
- })();
142
- `;
143
-
144
- export async function openPageWithVirtualClock(browserOrOpts, htmlPathArg, optsArg = {}) {
145
- let browser, htmlPath, width, height;
146
- if (browserOrOpts && typeof browserOrOpts === 'object' && browserOrOpts.browser) {
147
- browser = browserOrOpts.browser;
148
- htmlPath = browserOrOpts.htmlPath;
149
- width = browserOrOpts.width || 1920;
150
- height = browserOrOpts.height || 1080;
151
- } else {
152
- browser = browserOrOpts;
153
- htmlPath = htmlPathArg;
154
- width = optsArg.width || 1920;
155
- height = optsArg.height || 1080;
156
- }
157
- const page = await browser.newPage();
158
- await page.addInitScript(VIRTUAL_CLOCK_INJECTION);
159
-
160
- const fileUrl = pathToFileURL(path.resolve(htmlPath)).href;
161
- await page.goto(fileUrl, { waitUntil: 'load' });
162
-
163
- // Await fonts before frame 0
164
- await page.evaluate(async () => {
165
- if (document.fonts && document.fonts.ready) {
166
- await document.fonts.ready;
167
- }
168
- });
169
-
170
- await page.setViewportSize({ width, height });
171
- return page;
172
- }
173
-
174
- export async function captureFrame(page, t, { type = 'image/png', quality = 0.94 } = {}) {
175
- const dataUrl = await page.evaluate(async ({ time, mime, q }) => {
176
- await window.__seek(time);
177
- if (window.renderAt) {
178
- return await window.renderAt(time, mime, q);
179
- }
180
- return null;
181
- }, { time: t, mime: type, q: quality });
182
-
183
- if (typeof dataUrl === 'string' && dataUrl.includes(',')) {
184
- return Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64');
185
- }
186
-
187
- const screenshotType = type === 'image/jpeg' ? 'jpeg' : 'png';
188
- return page.screenshot({
189
- type: screenshotType,
190
- quality: screenshotType === 'jpeg' ? Math.round(quality * 100) : undefined,
191
- });
192
- }
1
+ /**
2
+ * capture.mjs — Virtual Clock Injection & Deterministic Frame Stepper
3
+ *
4
+ * Replaces Date.now, performance.now, requestAnimationFrame, setTimeout/setInterval,
5
+ * and CSS animations with a virtual clock. Supports both canvas/WebGL renderAt(t)
6
+ * and DOM/canvas seek(t).
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { pathToFileURL } from 'url';
12
+
13
+ export const VIRTUAL_CLOCK_INJECTION = `
14
+ (function () {
15
+ let _now = 0;
16
+ let _rafQueue = new Map();
17
+ let _rafId = 0;
18
+ let _timerQueue = [];
19
+ let _timerId = 0;
20
+ const _realDateNow = Date.now;
21
+ const _realSetTimeout = window.setTimeout;
22
+ const _startDate = _realDateNow();
23
+ const _delayHandles = new Set();
24
+ let _delayId = 0;
25
+
26
+ window.delayRender = (label = 'asset') => {
27
+ const id = ++_delayId;
28
+ _delayHandles.add(id);
29
+ return id;
30
+ };
31
+ window.continueRender = (id) => {
32
+ _delayHandles.delete(id);
33
+ };
34
+ window.cancelRender = (err) => {
35
+ console.error("Render cancelled:", err);
36
+ };
37
+
38
+ window.__playwright = true;
39
+
40
+ // 1. Deterministic clocks
41
+ performance.now = () => _now;
42
+ const OriginalDate = window.Date;
43
+ class VirtualDate extends OriginalDate {
44
+ constructor(...args) {
45
+ if (args.length === 0) {
46
+ super(_startDate + _now);
47
+ } else {
48
+ super(...args);
49
+ }
50
+ }
51
+ static now() {
52
+ return Math.floor(_startDate + _now);
53
+ }
54
+ }
55
+ window.Date = VirtualDate;
56
+ Date.now = () => _startDate + _now;
57
+
58
+ // 2. Deterministic requestAnimationFrame
59
+ window.requestAnimationFrame = (cb) => {
60
+ const id = ++_rafId;
61
+ _rafQueue.set(id, cb);
62
+ return id;
63
+ };
64
+ window.cancelAnimationFrame = (id) => {
65
+ _rafQueue.delete(id);
66
+ };
67
+
68
+ // 3. Virtual timer queue
69
+ window.setTimeout = (fn, delay = 0, ...args) => {
70
+ const id = ++_timerId;
71
+ _timerQueue.push({ id, runAt: _now + Math.max(0, delay), fn, args, repeat: 0 });
72
+ _timerQueue.sort((a, b) => a.runAt - b.runAt);
73
+ return id;
74
+ };
75
+ window.clearTimeout = (id) => {
76
+ const idx = _timerQueue.findIndex(t => t.id === id);
77
+ if (idx !== -1) _timerQueue.splice(idx, 1);
78
+ };
79
+ window.setInterval = (fn, interval = 0, ...args) => {
80
+ const id = ++_timerId;
81
+ const step = Math.max(16, interval);
82
+ _timerQueue.push({ id, runAt: _now + step, fn, args, repeat: step });
83
+ _timerQueue.sort((a, b) => a.runAt - b.runAt);
84
+ return id;
85
+ };
86
+ window.clearInterval = (id) => {
87
+ const idx = _timerQueue.findIndex(t => t.id === id);
88
+ if (idx !== -1) _timerQueue.splice(idx, 1);
89
+ };
90
+
91
+ // 4. Deterministic Seeded PRNG fallback for unseeded Math.random()
92
+ let _seed = 0x2F6E2B1;
93
+ const resetSeed = (tSec) => {
94
+ _seed = (0x2F6E2B1 ^ Math.imul(Math.floor(tSec * 1000), 0x9E3779B9)) | 0;
95
+ };
96
+ window.Math.random = () => {
97
+ _seed = (_seed + 0x6D2B79F5) | 0;
98
+ let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed);
99
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
100
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
101
+ };
102
+
103
+ // 5. Virtual time advance
104
+ window.__advance = (ms) => {
105
+ _now += ms;
106
+ while (_timerQueue.length > 0 && _timerQueue[0].runAt <= _now) {
107
+ const item = _timerQueue.shift();
108
+ try { item.fn(...item.args); } catch (_) {}
109
+ if (item.repeat > 0) {
110
+ item.runAt = _now + item.repeat;
111
+ _timerQueue.push(item);
112
+ _timerQueue.sort((a, b) => a.runAt - b.runAt);
113
+ }
114
+ }
115
+ const currentRafs = Array.from(_rafQueue.values());
116
+ _rafQueue.clear();
117
+ currentRafs.forEach(cb => { try { cb(_now); } catch (_) {} });
118
+
119
+ try {
120
+ if (document.getAnimations) {
121
+ document.getAnimations().forEach(a => {
122
+ if (a.playState !== 'paused') a.pause();
123
+ a.currentTime = _now;
124
+ });
125
+ }
126
+ } catch (_) {}
127
+ };
128
+
129
+ // 6. Absolute virtual seek
130
+ window.__seek = async (t) => {
131
+ _now = t * 1000;
132
+ resetSeed(t);
133
+
134
+ // Pause & seek CSS animations
135
+ try {
136
+ if (document.getAnimations) {
137
+ document.getAnimations().forEach(a => {
138
+ if (a.playState !== 'paused') a.pause();
139
+ a.currentTime = _now;
140
+ });
141
+ }
142
+ } catch (_) {}
143
+
144
+ if (window.renderAt) {
145
+ await window.renderAt(t);
146
+ } else if (window.seek) {
147
+ await window.seek(t);
148
+ }
149
+ while (_timerQueue.length > 0 && _timerQueue[0].runAt <= _now) {
150
+ const item = _timerQueue.shift();
151
+ try { item.fn(...item.args); } catch (_) {}
152
+ }
153
+ if (document.fonts && document.fonts.ready) {
154
+ await document.fonts.ready;
155
+ }
156
+
157
+ if (_delayHandles.size > 0) {
158
+ const startW = _realDateNow();
159
+ while (_delayHandles.size > 0 && _realDateNow() - startW < 10000) {
160
+ await new Promise(r => _realSetTimeout(r, 10));
161
+ }
162
+ }
163
+ };
164
+ })();
165
+ `;
166
+
167
+ export async function openPageWithVirtualClock(browserOrOpts, htmlPathArg, optsArg = {}) {
168
+ let browser, htmlPath, width, height;
169
+ if (browserOrOpts && typeof browserOrOpts === 'object' && browserOrOpts.browser) {
170
+ browser = browserOrOpts.browser;
171
+ htmlPath = browserOrOpts.htmlPath;
172
+ width = browserOrOpts.width || 1920;
173
+ height = browserOrOpts.height || 1080;
174
+ } else {
175
+ browser = browserOrOpts;
176
+ htmlPath = htmlPathArg;
177
+ width = optsArg.width || 1920;
178
+ height = optsArg.height || 1080;
179
+ }
180
+ const page = await browser.newPage();
181
+ await page.addInitScript(VIRTUAL_CLOCK_INJECTION);
182
+
183
+ const fileUrl = pathToFileURL(path.resolve(htmlPath)).href;
184
+ await page.goto(fileUrl, { waitUntil: 'load' });
185
+
186
+ // Await fonts before frame 0
187
+ await page.evaluate(async () => {
188
+ if (document.fonts && document.fonts.ready) {
189
+ await document.fonts.ready;
190
+ }
191
+ });
192
+
193
+ await page.setViewportSize({ width, height });
194
+ return page;
195
+ }
196
+
197
+ export async function captureFrame(page, t, { type = 'image/png', quality = 0.94 } = {}) {
198
+ const dataUrl = await page.evaluate(async ({ time, mime, q }) => {
199
+ await window.__seek(time);
200
+ if (window.renderAt) {
201
+ return await window.renderAt(time, mime, q);
202
+ }
203
+ return null;
204
+ }, { time: t, mime: type, q: quality });
205
+
206
+ if (typeof dataUrl === 'string' && dataUrl.includes(',')) {
207
+ return Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64');
208
+ }
209
+
210
+ const screenshotType = type === 'image/jpeg' ? 'jpeg' : 'png';
211
+ return page.screenshot({
212
+ type: screenshotType,
213
+ quality: screenshotType === 'jpeg' ? Math.round(quality * 100) : undefined,
214
+ });
215
+ }
@@ -204,18 +204,58 @@ export async function verifySceneQuality({
204
204
  });
205
205
  }
206
206
 
207
- // Motion continuity delta: compare hashes of t=30% vs t=70%
208
- // A static/stuck PPT slide will have hash(t1) === hash(t2)
209
- const hashEarly = createHash('sha256').update(rawBuffers[1] || rawBuffers[0]).digest('hex');
210
- const hashLate = createHash('sha256').update(rawBuffers[3] || rawBuffers[rawBuffers.length - 1]).digest('hex');
211
- const isStuckFrozen = (hashEarly === hashLate);
207
+ // Motion continuity delta: pixel-luminance MAD between t=30% and t=70% frames.
208
+ // A pure hash comparison misses slow-drift scenes (e.g. barely-visible zoom) that
209
+ // look frozen to a human viewer but differ by ≥1 pixel. We require at least 0.5%
210
+ // of pixels to change by >10 luminance units — this filters lens-flare flickers too.
211
+ const earlyBuf = rawBuffers[1] || rawBuffers[0];
212
+ const lateBuf = rawBuffers[3] || rawBuffers[rawBuffers.length - 1];
213
+
214
+ const hashEarly = createHash('sha256').update(earlyBuf).digest('hex');
215
+ const hashLate = createHash('sha256').update(lateBuf).digest('hex');
216
+ const identicalHash = (hashEarly === hashLate);
217
+
212
218
  let motionDelta = 1.0;
219
+ let isStuckFrozen = identicalHash;
220
+
221
+ if (!identicalHash) {
222
+ // Pixel-level MAD using Playwright canvas read (already done above, reuse page)
223
+ const pixelMotion = await page.evaluate(async (times) => {
224
+ const canvas = document.querySelector('canvas') || document.getElementById('stage');
225
+ if (!canvas) return null;
226
+ const ctx = canvas.getContext('2d');
227
+ const w = canvas.width;
228
+ const h = canvas.height;
229
+
230
+ await window.__seek(times[0]);
231
+ const d1 = ctx.getImageData(0, 0, w, h).data;
232
+
233
+ await window.__seek(times[1]);
234
+ const d2 = ctx.getImageData(0, 0, w, h).data;
235
+
236
+ const total = w * h;
237
+ let changedPixels = 0;
238
+ for (let i = 0; i < total; i++) {
239
+ const idx = i * 4;
240
+ const lum1 = 0.2126 * d1[idx] + 0.7152 * d1[idx + 1] + 0.0722 * d1[idx + 2];
241
+ const lum2 = 0.2126 * d2[idx] + 0.7152 * d2[idx + 1] + 0.0722 * d2[idx + 2];
242
+ if (Math.abs(lum1 - lum2) > 10) changedPixels++;
243
+ }
244
+ return changedPixels / total;
245
+ }, [sampleTimes[1] ?? sampleTimes[0], sampleTimes[3] ?? sampleTimes[sampleTimes.length - 1]]);
246
+
247
+ if (pixelMotion !== null) {
248
+ motionDelta = pixelMotion;
249
+ // Require at least 0.5% of pixels to have changed by >10 luminance units
250
+ isStuckFrozen = pixelMotion < 0.005;
251
+ }
252
+ }
213
253
 
214
254
  if (isStuckFrozen) {
215
- motionDelta = 0.0;
255
+ motionDelta = identicalHash ? 0.0 : motionDelta;
216
256
  sampledFrames.forEach(f => {
217
257
  f.score = Math.max(1, f.score - 4);
218
- f.issues.push('Stuck motion: zero visual delta between early and late frames');
258
+ f.issues.push(`Stuck motion: < 0.5% pixel luminance change between t=30% and t=70% (delta=${(motionDelta * 100).toFixed(2)}%)`);
219
259
  });
220
260
  }
221
261
 
@@ -249,3 +289,4 @@ export async function verifySceneQuality({
249
289
  sheetPath: renderedSheetPath,
250
290
  };
251
291
  }
292
+