-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclip.mjs
More file actions
134 lines (126 loc) · 8.25 KB
/
Copy pathclip.mjs
File metadata and controls
134 lines (126 loc) · 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// The whole loop, one command: storyboard -> narration-fitted render -> voice -> score -> mux ->
// two-axis judge -> said-vs-shown gate. Exit non-zero until the gates clear.
//
// npm run clip (VOICE=en_US-ryan-high npm run clip to switch narrator)
//
// There is deliberately no shorter path that skips the judge. Every stage here exists because its
// absence shipped a real defect this week: unfitted holds cut narration mid-sentence, an unmapped
// mux shipped three silent videos, and an uncalibrated judge scored a static JPEG the same as a
// launch film. The gates are the product.
import { readFileSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { runRemotion } from "./run-remotion.mjs";
import { linesFromStoryboard, synthesiseBeats, assemble, overruns } from "./narrate.mjs";
import { buildScore, muxOnto } from "./score.mjs";
const VOICE = process.env.VOICE || "en_US-ryan-high"; // picked on measurables: 23% faster speech, 0 overruns, high-grade model; lessac one env var away
const PAD = 0.35;
const OUT = "out/clip.mp4";
const step = (name) => console.log(`\n[clip] ${name}`);
step("storyboard + narration fit");
const raw = readFileSync("src/walkthrough.data.js", "utf8");
const m = raw.match(/^([\s\S]*?export const WALKTHROUGHS\s*=\s*)([\s\S]*?)(;\s*)$/);
const data = JSON.parse(m[2]);
let steps = data[0].steps;
let lines = linesFromStoryboard(steps);
let clips = synthesiseBeats(lines, VOICE);
const newHolds = steps.map((s, i) => Math.round(Math.max((s.hold || 60) / 30, (clips[i]?.seconds ?? 0) + PAD) * 30));
data[0].steps.forEach((s, i) => { s.hold = newHolds[i]; });
writeFileSync("src/walkthrough.data.js", m[1] + JSON.stringify(data, null, 2) + m[3]);
steps = data[0].steps;
lines = linesFromStoryboard(steps);
const over = overruns(lines, clips);
if (over.length) { console.error("[clip] OVERRUNS after fitting:", JSON.stringify(over)); process.exit(1); }
const totalFrames = newHolds.reduce((a, b) => a + b, 0);
console.log(` ${steps.length} beats, ${(totalFrames / 30).toFixed(1)}s, voice ${VOICE}, overruns 0`);
step("render");
const r = runRemotion(["render", "src/index.js", `WT-${data[0].id}`, "out/clip-video.mp4", "--concurrency=2"], { stdio: "pipe", encoding: "utf8", timeout: 540_000, maxBuffer: 64 * 1024 * 1024 });
if (r.status !== 0) {
console.error("[clip] render failed:", (r.stderr || "").slice(-200));
// The 200 characters above are the launcher's `spawn …chrome-headless-shell.exe
// ENOENT`, which on a deep Windows checkout names a file that is present. D1 —
// and run-remotion.mjs has already printed the MAX_PATH explanation for it.
process.exit(1);
}
step("voice + score + mux");
assemble(lines, clips, totalFrames / 30, "out/clip-narration.wav");
const score = buildScore({ steps, outFile: "out/clip-audio.wav", narration: "out/clip-narration.wav" });
muxOnto("out/clip-video.mp4", "out/clip-audio.wav", OUT);
console.log(` mix ${score.post.I.toFixed(1)} LUFS / LRA ${score.post.LRA.toFixed(1)}`);
step("gate 1 — audio is actually there");
const vol = spawnSync("ffmpeg", ["-v", "info", "-i", OUT, "-map", "0:a:0", "-af", "volumedetect", "-f", "null", "-"], { encoding: "utf8", timeout: 300_000 });
const mean = Number((`${vol.stderr}`.match(/mean_volume: (-?[\d.]+)/) || [])[1] ?? -91);
if (mean < -60) { console.error(`[clip] SILENT OUTPUT (${mean} dB) — the mux shipped the wrong stream`); process.exit(1); }
console.log(` ${mean} dB`);
step("gate 2 — said matches shown (Moonshine)");
spawnSync("ffmpeg", ["-y", "-v", "error", "-i", OUT, "-vn", "-ar", "16000", "-ac", "1", "out/clip-stt.wav"], { encoding: "utf8", timeout: 300_000 });
// CHUNKED, 30s slices. Moonshine silently degrades past ~60 seconds: a 63s file with a clear male
// voiceover returned two characters, while the same audio in 30s slices transcribed perfectly. No
// error either way — the gate would have called any long cut "drifted" with a straight face.
const stt = spawnSync("python", ["-c", `
import re, json, wave, math, subprocess
import moonshine_onnx as m
with wave.open('out/clip-stt.wav') as w:
total = w.getnframes() / w.getframerate()
parts = []
for start in range(0, int(math.ceil(total)), 30):
subprocess.run(['ffmpeg','-y','-v','error','-ss',str(start),'-t','30','-i','out/clip-stt.wav','out/clip-stt-slice.wav'], check=True)
out = m.transcribe('out/clip-stt-slice.wav','moonshine/tiny')
parts.append(out[0] if isinstance(out,(list,tuple)) else str(out))
print(json.dumps({"heard": " ".join(parts)}))
`], { encoding: "utf8", timeout: 540_000, env: { ...process.env, PYTHONUTF8: "1" }, maxBuffer: 32 * 1024 * 1024 });
// A crashed transcriber is not a drift verdict. This scored a real cut "0% heard — drifted" when
// the python subprocess died on a transient slice-file error; the same file transcribed 93% a
// minute later. Same rule as the judge: the instrument failing must never read as the work failing.
if (stt.status !== 0) {
console.error(`[clip] STT RUNNER FAILED (exit ${stt.status}) — not a drift verdict:\n${(stt.stderr || "").split("\n").slice(-4).join("\n")}`);
process.exit(1);
}
const heard = JSON.parse(stt.stdout.split("\n").filter(Boolean).pop() ?? "{}").heard ?? "";
const norm = (t) => t.toLowerCase().replace(/—/g, " ").replace(/[^a-z0-9 ]/g, "").split(/\s+/).filter(Boolean);
const tw = norm(lines.map((l) => l.text).join(" "));
const hw = norm(heard);
// UNIQUE-word coverage. Counting every occurrence let one word repeated 85 times pass the gate
// with half the captions missing — Codex constructed it. Distinct vocabulary coverage cannot be
// inflated by repetition.
const tuniq = [...new Set(tw)];
const hset = new Set(hw);
const overlap = tuniq.filter((w) => hset.has(w)).length / Math.max(1, tuniq.length);
console.log(` ${Math.round(overlap * 100)}% of ${tuniq.length} distinct caption words heard`);
if (overlap < 0.85) { console.error("[clip] SAID-VS-SHOWN FAIL — narration and captions have drifted"); process.exit(1); }
step("gate 3 — two-axis judge (median of 3 at the boundary)");
// Measured on one file, three consecutive runs: comprehension 20/16/16 and the mom boolean flipped
// true/false/false. A single run is iteration feedback, not a gate. Clear results take one run;
// anything near the boundary escalates to three — median for scores, majority for mom.
const judgeOnce = () => {
const run = spawnSync("node", ["judge-video.mjs", OUT, "--no-reference"], { encoding: "utf8", timeout: 540_000, maxBuffer: 32 * 1024 * 1024 });
// A crashed judge must not contribute a verdict: reading judge.json after a failed run feeds
// the PREVIOUS run's file into the median.
if (run.status !== 0) return { craft: 0, comp: 0, mom: false, blocked: true };
const v = JSON.parse(readFileSync("out/clip.judge.json", "utf8"));
const craftScore = Object.values(v.scores).reduce((a, x) => a + x.score, 0);
const block = v.comprehension ?? {};
const compScore = Object.keys(block).filter((k) => typeof block[k]?.score === "number").reduce((a, k) => a + block[k].score, 0);
return { craft: craftScore, comp: compScore, mom: block.wouldMomUnderstand === true, blocked: run.status !== 0 };
};
const runs = [judgeOnce()];
if ((runs[0].comp >= 12 && runs[0].comp <= 18) || !runs[0].mom) {
console.log(" boundary result — escalating to median of 3");
runs.push(judgeOnce(), judgeOnce());
}
const med = (xs) => xs.slice().sort((a, b) => a - b)[Math.floor(xs.length / 2)];
const craft = med(runs.map((x) => x.craft));
const comp = med(runs.map((x) => x.comp));
const mom = runs.filter((x) => x.mom).length > runs.length / 2;
console.log(` craft ${craft}/22 · comprehension ${comp}/20 · mom ${mom ? "passes" : "FAILS"} (${runs.length} run${runs.length > 1 ? "s" : ""})`);
// The previous predicate was nearly decorative — Codex's truth table: craft was computed and
// never consulted, mom-fail with comp exactly 14 shipped, and a judge crash in 1 of 3 runs was
// silently ignored. Each clause below answers a constructed counterexample.
if (runs.some((x) => x.blocked)) {
console.error("[clip] JUDGE RUN FAILED — a crashed judge is not a verdict");
process.exit(1);
}
if (!mom || comp < 14 || craft < 14) {
console.error(`[clip] JUDGE BLOCKED (mom ${mom}, comp ${comp}/20, craft ${craft}/22) — read out/clip.judge.md, fix, run again.`);
process.exit(1);
}
console.log(`\n[clip] SHIPPABLE: ${OUT}`);