@amg555/frameforge-mcp 2.3.1 → 2.3.3
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/.mcp.json +2 -4
- package/CHANGELOG.md +20 -1
- package/CLAUDE.md +154 -18
- package/README.md +16 -12
- package/SECURITY.md +1 -3
- package/install.sh +1 -1
- package/package.json +7 -3
- package/plugins/video-pipeline/.claude-plugin/plugin.json +19 -0
- package/plugins/video-pipeline/skills/html-renderer/SKILL.md +292 -0
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +308 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +257 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/STORYBOARD.md +77 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/index.html +80 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +43 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +56 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +42 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +140 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +870 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +138 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +167 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +163 -0
- package/scripts/db-migrate.js +112 -0
- package/scripts/deep-validate.js +326 -0
- package/scripts/generate-initial-assets.js +342 -0
- package/scripts/html-renderer/new-project.mjs +408 -0
- package/scripts/html-renderer/transcribe.py +294 -0
- package/src/asset-library/README.md +2 -2
- package/src/asset-library/asset-library.js +1 -1
- package/src/cli/doctor.js +31 -1
- package/src/cli/make.js +52 -71
- package/src/cli/setup-keys.js +24 -19
- package/src/fact-checker/review-gate.js +5 -1
- package/src/harness/bin/harness.mjs +324 -0
- package/src/harness/lib/browser.mjs +137 -0
- package/src/harness/lib/capture.mjs +161 -0
- package/src/harness/lib/encode.mjs +89 -0
- package/src/harness/lib/jobs.mjs +135 -0
- package/src/harness/lib/pixels.mjs +137 -0
- package/src/harness/lib/sheets.mjs +164 -0
- package/src/harness/lib/verify-purity.mjs +119 -0
- package/src/harness/test/fixtures/broken-scene.html +27 -0
- package/src/harness/test/fixtures/reference-scene.html +23 -0
- package/src/harness/test/fixtures/sample-script.json +18 -0
- package/src/mcp-server/server.js +318 -66
- package/src/pipeline/external-adapter.js +55 -15
- package/src/pipeline/providers.js +46 -0
- package/src/pipeline/telemetry.js +1 -0
- package/src/utils/log-redaction.js +5 -3
- package/src/video-renderer/renderer.js +12 -4
- package/src/worker/server-render-free.js +2 -3
- package/src/workflows/assets.js +20 -43
- package/src/workflows/ingest.js +5 -5
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* new-project.mjs — Scaffold an HTML-renderer project from a FrameForge script JSON.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/html-renderer/new-project.mjs \
|
|
7
|
+
* --script output/<productionId>-script.json \
|
|
8
|
+
* --out output/<productionId>-html/
|
|
9
|
+
*
|
|
10
|
+
* What it produces:
|
|
11
|
+
* <out>/
|
|
12
|
+
* index.html compositor page (loads reel/engine.js)
|
|
13
|
+
* edit.js starter edit with one layer() per FrameForge scene
|
|
14
|
+
* reel/ engine runtime (copied from assets/project/reel/)
|
|
15
|
+
* scripts/ render, mix_audio, verify, stock, capture (copied from assets/project/scripts/)
|
|
16
|
+
* source/ empty — user drops video clips here
|
|
17
|
+
* audio/ empty — TTS outputs from FrameForge go here
|
|
18
|
+
* out/ empty — render outputs land here
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const args = {};
|
|
30
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
31
|
+
if (process.argv[i] === '--script') args.script = process.argv[++i];
|
|
32
|
+
if (process.argv[i] === '--out') args.out = process.argv[++i];
|
|
33
|
+
if (process.argv[i] === '--fps') args.fps = parseInt(process.argv[++i], 10) || 30;
|
|
34
|
+
if (process.argv[i] === '--width') args.width = parseInt(process.argv[++i], 10) || 1920;
|
|
35
|
+
if (process.argv[i] === '--height') args.height = parseInt(process.argv[++i], 10) || 1080;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!args.script || !args.out) {
|
|
39
|
+
console.error('Usage: node new-project.mjs --script <script.json> --out <project-dir>');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Load script JSON ──────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const scriptPath = path.resolve(process.cwd(), args.script);
|
|
46
|
+
if (!fs.existsSync(scriptPath)) {
|
|
47
|
+
console.error(`Script not found: ${scriptPath}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const raw = JSON.parse(fs.readFileSync(scriptPath, 'utf8'));
|
|
52
|
+
// Support both {script:{scenes:[]}} (make.js dry-run format) and {scenes:[]} (direct)
|
|
53
|
+
const script = raw.script || raw;
|
|
54
|
+
const scenes = script.scenes || [];
|
|
55
|
+
|
|
56
|
+
if (scenes.length === 0) {
|
|
57
|
+
console.error('No scenes found in script JSON. Run with --dry-run first.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const title = script.title || 'FrameForge Video';
|
|
62
|
+
const description = script.description || '';
|
|
63
|
+
const fps = args.fps || 30;
|
|
64
|
+
const width = args.width || 1920;
|
|
65
|
+
const height = args.height || 1080;
|
|
66
|
+
|
|
67
|
+
// ── Create project directory structure ───────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const outDir = path.resolve(process.cwd(), args.out);
|
|
70
|
+
const assetsDir = path.resolve(__dirname, '../../plugins/video-pipeline/skills/video-pipeline/assets/project');
|
|
71
|
+
for (const dir of ['', 'reel', 'scripts', 'source', 'audio', 'out', 'assets']) {
|
|
72
|
+
fs.mkdirSync(path.join(outDir, dir), { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Estimate scene durations from word count ──────────────────────────────────
|
|
76
|
+
// 2.5 words/second is a typical narrator pace; scenes start sequentially.
|
|
77
|
+
|
|
78
|
+
const WORDS_PER_SEC = 2.5;
|
|
79
|
+
let cumulativeTime = 0;
|
|
80
|
+
const sceneMeta = scenes.map((scene, i) => {
|
|
81
|
+
const words = (scene.narration || '').split(/\s+/).filter(Boolean).length;
|
|
82
|
+
const duration = Math.max(2.5, words / WORDS_PER_SEC);
|
|
83
|
+
const start = cumulativeTime;
|
|
84
|
+
cumulativeTime += duration;
|
|
85
|
+
return { ...scene, _start: start, _duration: duration, _index: i };
|
|
86
|
+
});
|
|
87
|
+
const totalDuration = cumulativeTime;
|
|
88
|
+
|
|
89
|
+
// ── Generate index.html ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const indexHtml = `<!DOCTYPE html>
|
|
92
|
+
<html lang="en">
|
|
93
|
+
<head>
|
|
94
|
+
<meta charset="UTF-8">
|
|
95
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
96
|
+
<title>${escapeHtml(title)}</title>
|
|
97
|
+
<!-- Preload primary typeface — must resolve before frame 0 (fonts.ready gate) -->
|
|
98
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
99
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
100
|
+
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
101
|
+
<link rel="stylesheet" href="reel/engine.css">
|
|
102
|
+
<style>
|
|
103
|
+
/* Project-level overrides — edit here */
|
|
104
|
+
:root {
|
|
105
|
+
--primary: #ffffff;
|
|
106
|
+
--accent: #3b82f6;
|
|
107
|
+
--bg: #0d1117;
|
|
108
|
+
--font: 'Archivo', system-ui, sans-serif;
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<div id="stage" style="width:${width}px;height:${height}px;position:relative;overflow:hidden;background:var(--bg)">
|
|
114
|
+
</div>
|
|
115
|
+
<script type="module" src="edit.js"></script>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
|
118
|
+
`;
|
|
119
|
+
fs.writeFileSync(path.join(outDir, 'index.html'), indexHtml, 'utf8');
|
|
120
|
+
|
|
121
|
+
// ── Generate edit.js ──────────────────────────────────────────────────────────
|
|
122
|
+
// One layer() per FrameForge scene with narration as captions and
|
|
123
|
+
// visual_description as a wallCard placeholder.
|
|
124
|
+
|
|
125
|
+
const editLines = [
|
|
126
|
+
`/**`,
|
|
127
|
+
` * edit.js — Generated by FrameForge html-renderer scaffold`,
|
|
128
|
+
` * Source: ${path.basename(scriptPath)}`,
|
|
129
|
+
` * Title: ${title}`,
|
|
130
|
+
` *`,
|
|
131
|
+
` * IRON RULE: Every style must be a pure function of t (seconds).`,
|
|
132
|
+
` * No CSS transitions, no setTimeout, no state that depends on previous frames.`,
|
|
133
|
+
` * If a layer is only active until t=X, it must explicitly hide itself after t=X.`,
|
|
134
|
+
` */`,
|
|
135
|
+
``,
|
|
136
|
+
`import { reel, layer, text, wallCard, stamp, at, stagger, captions, cue, codeCard, terminal, imageCard, chip } from './reel/engine.js';`,
|
|
137
|
+
``,
|
|
138
|
+
`// ── Compositor config ────────────────────────────────────────────────────────`,
|
|
139
|
+
`reel({`,
|
|
140
|
+
` width: ${width},`,
|
|
141
|
+
` height: ${height},`,
|
|
142
|
+
` fps: ${fps},`,
|
|
143
|
+
` duration: ${totalDuration.toFixed(2)},`,
|
|
144
|
+
` title: ${JSON.stringify(title)},`,
|
|
145
|
+
`});`,
|
|
146
|
+
``,
|
|
147
|
+
`// ── Background ────────────────────────────────────────────────────────────────`,
|
|
148
|
+
`layer('background').on(t => ({`,
|
|
149
|
+
` position: 'absolute', inset: 0,`,
|
|
150
|
+
` background: 'var(--bg)',`,
|
|
151
|
+
`}));`,
|
|
152
|
+
``,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
function renderSceneComponent(scene, i, width, height, start, duration) {
|
|
156
|
+
const narration = (scene.narration || '').replace(/`/g, "'");
|
|
157
|
+
const visual = (scene.visual_description || '').replace(/`/g, "'");
|
|
158
|
+
const caption = (scene.caption || `SCENE ${i + 1}`).toUpperCase().replace(/`/g, "'");
|
|
159
|
+
const textLower = (narration + ' ' + visual + ' ' + caption).toLowerCase();
|
|
160
|
+
|
|
161
|
+
// 1. Manual User Image
|
|
162
|
+
if (scene.image_path) {
|
|
163
|
+
return [
|
|
164
|
+
`// User-supplied image for Scene ${i + 1}`,
|
|
165
|
+
`at(${start}, imageCard(${JSON.stringify(scene.image_path)}, {`,
|
|
166
|
+
` caption: ${JSON.stringify(caption)},`,
|
|
167
|
+
` width: ${Math.round(width * 0.72)},`,
|
|
168
|
+
` x: 'center', y: 'center',`,
|
|
169
|
+
` duration: ${duration}`,
|
|
170
|
+
`}));`,
|
|
171
|
+
].join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Manual User Video Clip
|
|
175
|
+
if (scene.video_path) {
|
|
176
|
+
return [
|
|
177
|
+
`// User-supplied video clip for Scene ${i + 1}: ${scene.video_path}`,
|
|
178
|
+
`at(${start}, wallCard([`,
|
|
179
|
+
` { text: ${JSON.stringify(caption)}, size: 28, weight: 800, color: 'var(--accent)' },`,
|
|
180
|
+
`], { x: 'center', y: ${Math.round(height * 0.78)}, width: ${Math.round(width * 0.8)}, duration: ${duration} }));`,
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Code / Tech
|
|
185
|
+
if (/\b(code|function|syntax|api|javascript|python|typescript|json|sql|query|endpoint|algorithm|struct)\b/.test(textLower)) {
|
|
186
|
+
const snippet = visual.length > 20 ? visual : `// ${caption}\nfunction execute() {\n return "${caption.toLowerCase()}";\n}`;
|
|
187
|
+
return [
|
|
188
|
+
`at(${start}, codeCard(${JSON.stringify(snippet)}, 'js', {`,
|
|
189
|
+
` title: ${JSON.stringify(caption)},`,
|
|
190
|
+
` width: ${Math.round(width * 0.76)},`,
|
|
191
|
+
` x: 'center', y: ${Math.round(height * 0.22)},`,
|
|
192
|
+
` duration: ${duration}`,
|
|
193
|
+
`}));`,
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 4. Terminal / Commands
|
|
198
|
+
if (/\b(terminal|command|cli|bash|shell|npm|install|curl|git|docker|deploy|run)\b/.test(textLower)) {
|
|
199
|
+
const cmd = caption.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
200
|
+
return [
|
|
201
|
+
`at(${start}, terminal([`,
|
|
202
|
+
` '$ frameforge ${cmd}',`,
|
|
203
|
+
` '${caption} — initializing...',`,
|
|
204
|
+
` '✓ complete (${duration}s)'`,
|
|
205
|
+
`], {`,
|
|
206
|
+
` title: ${JSON.stringify(caption)},`,
|
|
207
|
+
` width: ${Math.round(width * 0.74)},`,
|
|
208
|
+
` x: 'center', y: ${Math.round(height * 0.24)},`,
|
|
209
|
+
` duration: ${duration}`,
|
|
210
|
+
`}));`,
|
|
211
|
+
].join('\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 5. Stat / Metric / Number
|
|
215
|
+
const metricMatch = textLower.match(/\b(\$?\d+(?:\.\d+)?%?|\d+x)\b/);
|
|
216
|
+
if (metricMatch && /\b(percent|million|billion|trillion|x|fps|ms|seconds|users|growth|speed|metric|stat)\b/.test(textLower)) {
|
|
217
|
+
return [
|
|
218
|
+
`at(${start}, wallCard([`,
|
|
219
|
+
` { text: ${JSON.stringify(metricMatch[0].toUpperCase())}, size: 76, weight: 900, color: 'var(--accent)' },`,
|
|
220
|
+
` { text: ${JSON.stringify(caption)}, size: 28, weight: 800 },`,
|
|
221
|
+
` { text: ${JSON.stringify(narration.slice(0, 110))}, size: 20, weight: 400, color: '#94a3b8' }`,
|
|
222
|
+
`], { x: 'center', y: ${Math.round(height * 0.25)}, width: ${Math.round(width * 0.82)}, duration: ${duration} }));`,
|
|
223
|
+
].join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 6. Default: Kinetic typography card with accent stamp
|
|
227
|
+
return [
|
|
228
|
+
`at(${start}, wallCard([`,
|
|
229
|
+
` { text: ${JSON.stringify(caption)}, size: 36, weight: 800, color: 'var(--accent)' },`,
|
|
230
|
+
` { text: ${JSON.stringify(narration.slice(0, 130))}, size: 24, weight: 400 }`,
|
|
231
|
+
`], { x: 'center', y: ${Math.round(height * 0.30)}, width: ${Math.round(width * 0.82)}, duration: ${duration} }));`,
|
|
232
|
+
`at(${start}, stamp(${JSON.stringify(caption.slice(0, 16))}, { x: ${Math.round(width * 0.75)}, y: ${Math.round(height * 0.18)}, color: 'var(--accent)', duration: ${duration} }));`,
|
|
233
|
+
].join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
sceneMeta.forEach((scene, i) => {
|
|
237
|
+
const start = scene._start.toFixed(2);
|
|
238
|
+
const end = (scene._start + scene._duration).toFixed(2);
|
|
239
|
+
const caption = (scene.caption || `SCENE ${i + 1}`).toUpperCase();
|
|
240
|
+
const narration = (scene.narration || '').replace(/`/g, "'");
|
|
241
|
+
const visual = (scene.visual_description || '').replace(/`/g, "'");
|
|
242
|
+
const cam = scene.camera_hint || 'auto';
|
|
243
|
+
const mood = scene.music_mood || 'neutral';
|
|
244
|
+
|
|
245
|
+
editLines.push(
|
|
246
|
+
`// ── Scene ${i + 1}/${scenes.length}: ${caption} (${start}s – ${end}s) ─────────────────────────────`,
|
|
247
|
+
`// Narration: "${narration.slice(0, 80)}${narration.length > 80 ? '…' : ''}"`,
|
|
248
|
+
`// Visual: "${visual.slice(0, 80)}${visual.length > 80 ? '…' : ''}"`,
|
|
249
|
+
`// Camera: ${cam} | Music: ${mood}`,
|
|
250
|
+
`layer('scene-${i + 1}').on(t => {`,
|
|
251
|
+
` const active = t >= ${start} && t < ${end};`,
|
|
252
|
+
` const fadeIn = t < ${(scene._start + 0.4).toFixed(2)} ? (t - ${start}) / 0.4 : 1;`,
|
|
253
|
+
` const fadeOut = t > ${(scene._start + scene._duration - 0.4).toFixed(2)} ? (${end} - t) / 0.4 : 1;`,
|
|
254
|
+
` const alpha = active ? Math.min(fadeIn, fadeOut) : 0;`,
|
|
255
|
+
` return {`,
|
|
256
|
+
` position: 'absolute', inset: 0,`,
|
|
257
|
+
` opacity: alpha,`,
|
|
258
|
+
` pointerEvents: active ? 'auto' : 'none',`,
|
|
259
|
+
` };`,
|
|
260
|
+
`});`,
|
|
261
|
+
``,
|
|
262
|
+
`// Code-driven visual component for Scene ${i + 1}`,
|
|
263
|
+
renderSceneComponent(scene, i, width, height, start, scene._duration.toFixed(2)),
|
|
264
|
+
``,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
editLines.push(
|
|
269
|
+
`// ── Captions (auto-synced to TTS audio) ─────────────────────────────────────`,
|
|
270
|
+
`// After running FrameForge TTS + uv run scripts/transcribe.py, uncomment:`,
|
|
271
|
+
`// captions('audio/outline.txt');`,
|
|
272
|
+
``,
|
|
273
|
+
`// ── Optional: sound effects ──────────────────────────────────────────────────`,
|
|
274
|
+
`// cue('whoosh', ${sceneMeta[1]?._start?.toFixed(2) || '3.0'}, 0.6);`,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
fs.writeFileSync(path.join(outDir, 'edit.js'), editLines.join('\n'), 'utf8');
|
|
278
|
+
|
|
279
|
+
// ── Copy engine files from assets/project/ ─────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function copyAsset(srcRel, dstRel) {
|
|
282
|
+
const src = path.join(assetsDir, srcRel);
|
|
283
|
+
const dst = path.join(outDir, dstRel);
|
|
284
|
+
if (fs.existsSync(src)) {
|
|
285
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
286
|
+
fs.copyFileSync(src, dst);
|
|
287
|
+
} else {
|
|
288
|
+
console.warn(` ⚠ Asset not found: ${src} (scaffold may be incomplete)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
copyAsset('reel/engine.js', 'reel/engine.js');
|
|
293
|
+
copyAsset('reel/engine.css', 'reel/engine.css');
|
|
294
|
+
copyAsset('reel/virtual-clock.js', 'reel/virtual-clock.js');
|
|
295
|
+
copyAsset('scripts/render.mjs', 'scripts/render.mjs');
|
|
296
|
+
copyAsset('scripts/harness.mjs', 'scripts/harness.mjs');
|
|
297
|
+
copyAsset('scripts/browser.mjs', 'scripts/browser.mjs');
|
|
298
|
+
copyAsset('scripts/verify-purity.mjs','scripts/verify-purity.mjs');
|
|
299
|
+
copyAsset('scripts/pixels.mjs', 'scripts/pixels.mjs');
|
|
300
|
+
copyAsset('scripts/sheets.mjs', 'scripts/sheets.mjs');
|
|
301
|
+
copyAsset('scripts/jobs.mjs', 'scripts/jobs.mjs');
|
|
302
|
+
copyAsset('scripts/encode.mjs', 'scripts/encode.mjs');
|
|
303
|
+
copyAsset('scripts/look.mjs', 'scripts/look.mjs');
|
|
304
|
+
copyAsset('scripts/storyboard-validator.mjs', 'scripts/storyboard-validator.mjs');
|
|
305
|
+
copyAsset('scripts/mix_audio.py', 'scripts/mix_audio.py');
|
|
306
|
+
copyAsset('scripts/verify.py', 'scripts/verify.py');
|
|
307
|
+
copyAsset('scripts/transcribe.py', 'scripts/transcribe.py');
|
|
308
|
+
copyAsset('scripts/capture.mjs', 'scripts/capture.mjs');
|
|
309
|
+
copyAsset('scripts/stock.py', 'scripts/stock.py');
|
|
310
|
+
copyAsset('scripts/video_job.py', 'scripts/video_job.py');
|
|
311
|
+
copyAsset('STORYBOARD.md', 'STORYBOARD.md');
|
|
312
|
+
copyAsset('ANIMATION_GUIDE.md', 'ANIMATION_GUIDE.md');
|
|
313
|
+
|
|
314
|
+
// Pipeline briefings (on-disk agent rules for orchestrator + subagents)
|
|
315
|
+
for (const briefing of ['orchestrator.md', 'scene-author.md', 'diagram-author.md']) {
|
|
316
|
+
copyAsset(`pipeline/briefings/${briefing}`, `pipeline/briefings/${briefing}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Write package.json for the project ───────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
const pkg = {
|
|
322
|
+
name: path.basename(outDir),
|
|
323
|
+
version: '0.1.0',
|
|
324
|
+
type: 'module',
|
|
325
|
+
scripts: {
|
|
326
|
+
'harness': 'node scripts/harness.mjs',
|
|
327
|
+
'harness:doctor': 'node scripts/harness.mjs doctor',
|
|
328
|
+
'harness:verify': 'node scripts/harness.mjs verify',
|
|
329
|
+
'harness:all': 'node scripts/harness.mjs all',
|
|
330
|
+
'look': 'node scripts/look.mjs',
|
|
331
|
+
'validate:storyboard': 'node scripts/storyboard-validator.mjs STORYBOARD.md',
|
|
332
|
+
'render:sheet': 'node scripts/render.mjs sheet',
|
|
333
|
+
'render:storyboard': 'node scripts/render.mjs storyboard',
|
|
334
|
+
'render:strip': 'node scripts/render.mjs strip',
|
|
335
|
+
'render:crop': 'node scripts/render.mjs crop 9:16',
|
|
336
|
+
'render:full': 'node scripts/render.mjs full',
|
|
337
|
+
'render:mux': 'node scripts/render.mjs mux',
|
|
338
|
+
'mix': 'uv run scripts/mix_audio.py',
|
|
339
|
+
'transcribe': 'uv run scripts/transcribe.py',
|
|
340
|
+
'verify': 'uv run scripts/verify.py',
|
|
341
|
+
'capture': 'node scripts/capture.mjs',
|
|
342
|
+
'stock': 'uv run scripts/stock.py',
|
|
343
|
+
'job:submit': 'python scripts/video_job.py submit',
|
|
344
|
+
'job:status': 'python scripts/video_job.py list',
|
|
345
|
+
'job:estimate': 'python scripts/video_job.py estimate',
|
|
346
|
+
},
|
|
347
|
+
dependencies: {
|
|
348
|
+
'playwright': '^1.40.0'
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
fs.writeFileSync(path.join(outDir, 'package.json'), JSON.stringify(pkg, null, 2), 'utf8');
|
|
352
|
+
|
|
353
|
+
// ── Write README ─────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
const readme = `# ${title}
|
|
356
|
+
|
|
357
|
+
HTML-renderer project generated by FrameForge.
|
|
358
|
+
|
|
359
|
+
## Quick start
|
|
360
|
+
|
|
361
|
+
1. Add TTS audio files to \`audio/\`
|
|
362
|
+
(copy from FrameForge output: \`voice_0.m4a\`, \`voice_1.m4a\`, …)
|
|
363
|
+
2. Check visually: \`npm run render:sheet\`
|
|
364
|
+
3. Mix audio: \`npm run mix\`
|
|
365
|
+
4. Full render: \`npm run render:full\`
|
|
366
|
+
5. Verify: \`npm run verify\`
|
|
367
|
+
|
|
368
|
+
## Edit
|
|
369
|
+
|
|
370
|
+
All animation is in \`edit.js\`. Every style must be a pure function of \`t\` (seconds).
|
|
371
|
+
|
|
372
|
+
## Engine
|
|
373
|
+
|
|
374
|
+
The compositor runtime is included at \`reel/engine.js\`.
|
|
375
|
+
To install browser renderer dependencies: \`npx playwright install chromium\`
|
|
376
|
+
|
|
377
|
+
## Scene map
|
|
378
|
+
|
|
379
|
+
| # | Start | Caption | Duration |
|
|
380
|
+
|---|-------|---------|----------|
|
|
381
|
+
${sceneMeta.map(s => `| ${s._index + 1} | ${s._start.toFixed(1)}s | ${(s.caption || '').toUpperCase()} | ${s._duration.toFixed(1)}s |`).join('\n')}
|
|
382
|
+
|
|
383
|
+
Total duration: ${totalDuration.toFixed(1)}s
|
|
384
|
+
`;
|
|
385
|
+
fs.writeFileSync(path.join(outDir, 'README.md'), readme, 'utf8');
|
|
386
|
+
|
|
387
|
+
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
console.log(`\n✅ HTML renderer project scaffolded:`);
|
|
390
|
+
console.log(` Directory: ${outDir}`);
|
|
391
|
+
console.log(` Scenes: ${scenes.length}`);
|
|
392
|
+
console.log(` Duration: ${totalDuration.toFixed(1)}s`);
|
|
393
|
+
console.log(` Title: "${title}"`);
|
|
394
|
+
console.log(`\nNext steps:`);
|
|
395
|
+
console.log(` 1. Install Playwright: npx playwright install chromium`);
|
|
396
|
+
console.log(` 2. Edit STORYBOARD.md: fill in shots, events, reads, transitions`);
|
|
397
|
+
console.log(` 3. Validate storyboard: node scripts/storyboard-validator.mjs STORYBOARD.md`);
|
|
398
|
+
console.log(` 4. Pre-flight check: node scripts/look.mjs`);
|
|
399
|
+
console.log(` 5. Check visuals: node scripts/render.mjs sheet`);
|
|
400
|
+
console.log(` 6. Mix audio (TTS): uv run scripts/mix_audio.py`);
|
|
401
|
+
console.log(` 7. Full render: node scripts/render.mjs full`);
|
|
402
|
+
console.log(` 8. Verify quality gates: uv run scripts/verify.py\n`);
|
|
403
|
+
|
|
404
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
function escapeHtml(str) {
|
|
407
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
408
|
+
}
|