#!/usr/bin/env node // Build a throwaway song dir for the e2e suite. // // The tool writes to the SAME files the CLI scripts do, and those files hold // thousands of real human verdicts. A spec that judged a clip against them // would be indistinguishable from a person doing it -- so the suite never runs // against them. This copies the small JSON state and a couple of candidate // files, and SYMLINKS the heavy audio (39 GB, read-only in practice). // // node make-fixture.mjs import { mkdirSync, copyFileSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, symlinkSync, } from "node:fs"; import { spawnSync } from "node:child_process"; import path from "node:path"; import { SONG_DATA } from "../../song/paths.mjs"; import { songCapabilities } from "./song-capabilities.mjs"; import { makeSitesFixture } from "./sites-fixture.mjs"; const CODE = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..", "song"); const dest = path.resolve(process.argv[2] ?? path.join(process.cwd(), ".e2e-song")); // A readdir that answers "nothing" instead of throwing. SONG_DATA's default // (`~/reports/quartering-uh-song/data`) does not exist on most machines — see the // capabilities block below. const listDir = (p) => { try { return readdirSync(p); } catch { return []; } }; rmSync(dest, { recursive: true, force: true }); mkdirSync(path.join(dest, "code"), { recursive: true }); mkdirSync(path.join(dest, "data", "cand2"), { recursive: true }); // -- code + state ------------------------------------------------------------ for (const f of listDir(CODE)) { if (f.endsWith(".mjs") || f.endsWith(".py")) copyFileSync(path.join(CODE, f), path.join(dest, "code", f)); } // Fresh, empty state: the suite asserts on counts, and inheriting 3,959 // accepted clips would make every assertion a moving target. // // The one exception is a SYNTHETIC ACCEPTED PALETTE. The pitch rail draws the // distribution of accepted clips, and with a wholly empty state there is no // distribution -- median and p95 are 0 and the instrument has nothing to say. // These keys deliberately match no real candidate, so they feed the rail // without appearing in any queue or moving any count: `loadClipList` builds // from cand2/, and nothing here is in it. // // The values are a plausible corpus shape (a peak near 103Hz with a thin high // tail), so median and p95 come out near the real 103 / 128. const PALETTE_F0 = [ 88, 92, 94, 96, 97, 98, 99, 100, 100, 101, 102, 102, 103, 103, 103, 104, 104, 105, 106, 107, 108, 110, 112, 115, 118, 124, 128, 133, 141, 158, ]; const seededKeys = PALETTE_F0.map((_, i) => `__fixture${i}@0.00`); const seed = { "accepted.json": { accepted: seededKeys, rejected: [] }, "unclean.json": [], "provisional.json": [], "impure.json": [], "keepside.json": {}, "decided.json": {}, "reasons.json": {}, "corepitch.json": Object.fromEntries( seededKeys.map((k, i) => [k, { f0: PALETTE_F0[i], dur: 0.25 }]), ), "words.json": {}, "vocab.json": { words: [] }, // The undo stack starts empty, like every other pile. "journal.json": [], // So do the shortlists. /browse/find writes here, and inheriting real ones // would mean a spec reordering a list somebody is actually cutting from. "shortlist.json": { version: 1, lists: {} }, // And the face judgements. This is the ONE file /browse/faces writes, and // make-thumb.mjs reads it to decide which source videos to skip -- so // inheriting real ones would mean a spec quietly retiring an episode // somebody's next cover was going to draw from. "face-verdicts.json": { version: 1, faces: {} }, }; for (const [f, v] of Object.entries(seed)) { writeFileSync(path.join(dest, "code", f), JSON.stringify(v, null, 1)); } for (const f of ["dates.json", "titles.json", "order-model.json"]) { if (existsSync(path.join(CODE, f))) copyFileSync(path.join(CODE, f), path.join(dest, "code", f)); } // No manifest: every clip then takes the compute-a-window path, which is the // one worth exercising. writeFileSync(path.join(dest, "code", "um-manifest.json"), JSON.stringify({ version: 1, items: [] }, null, 1)); // -- a few candidates, from videos whose audio is actually present ------------ // // TOLERANT OF A MISSING SONG_DATA, and that is the whole point of the // capabilities file below. `SONG_DIR`'s default does not exist on most // machines — so this readdir // used to throw and take the entire suite down before the test server started, // including the two dozen specs that have nothing to do with the song project. const cands = listDir(path.join(SONG_DATA, "cand2")).filter((f) => f.endsWith(".json")); let taken = 0; for (const f of cands) { if (taken >= 3) break; const one = JSON.parse(readFileSync(path.join(SONG_DATA, "cand2", f), "utf8")); const v = one.video ?? one.candidates?.[0]?.video; if (!v || !existsSync(path.join(SONG_DATA, "wav48", `${v}.wav`))) continue; if (!one.candidates?.length) continue; writeFileSync( path.join(dest, "data", "cand2", f), JSON.stringify({ ...one, candidates: one.candidates.slice(0, 6) }, null, 1), ); taken += 1; } // -- the heavy directories, by reference -------------------------------------- for (const d of ["wav48", "asr", "media"]) { const src = path.join(SONG_DATA, d); if (existsSync(src)) symlinkSync(src, path.join(dest, "data", d)); } // The DETECTOR, also by reference, and it needs two things beside the code. // // facecrop.py resolves its model relative to its own directory (`../models`), // and the fixture's copy of it lives in code/ -- so the model has to sit beside // that copy or every detection fails with a missing onnx. lib/faces.ts looks // for the venv at SONG_SCRATCH/facedet, and SONG_SCRATCH is the parent of // SONG_DIR, which in the fixture is the fixture root. // // Symlinked rather than copied for the same reason wav48/ is: a venv is ~200 MB // and read-only in practice. Without these the detect route answers 503 and the // spec that checks autoCrop() agrees with facecrop.py has nothing to agree with. const MODELS = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..", "models"); if (existsSync(MODELS)) symlinkSync(MODELS, path.join(dest, "models")); const FACEDET = path.join(path.dirname(SONG_DATA), "facedet"); if (existsSync(FACEDET)) symlinkSync(FACEDET, path.join(dest, "facedet")); // -- WHAT THIS FIXTURE ACTUALLY GOT ------------------------------------------- // // The heavy inputs are SYMLINKED from SONG_DATA, and on a machine where that // directory is absent or partial the fixture builds fine and is simply empty: // no candidates (the loop above requires a wav per video), no asr, no media, no // face detector. Every spec that judges a clip then failed — and failed // LOUDLY, as a red suite, over a machine that never had the 39 GB rather than // over anything a change broke. Red that means "you are on a different laptop" // trains people to ignore red. // // So the fixture records what it found and the specs that need it skip // themselves. A capability is a DIRECTORY THAT EXISTS, checked here once, not a // guess made per spec — and `song` is the compound the clip specs actually // need: candidates to list AND audio to cut. const capabilities = { ...songCapabilities(SONG_DATA), // What actually landed, as opposed to what the source offered: a candidate // is copied only when its wav is present, and the detector needs both halves. copiedCandidates: taken, facedet: existsSync(path.join(dest, "facedet")) && existsSync(path.join(dest, "models")), }; writeFileSync( path.join(dest, "fixture-capabilities.json"), JSON.stringify(capabilities, null, 1) + "\n", ); if (!capabilities.song) { // ON STDERR, because a suite that skips part of itself must say why where // somebody reading a CI log will look. process.stderr.write( `make-fixture: no song bulk data at ${SONG_DATA} ` + `(cand2=${capabilities.cand2} wav48=${capabilities.wav48} asr=${capabilities.asr} ` + `media=${capabilities.media}) — the song-data specs will SKIP. ` + `Set SONG_DIR to a copy to run them.\n`, ); } // -- one FLAGGED SOURCE, derived from the symlinked asr/ ---------------------- // // suspect-sources.json was neither copied nor seeded before this, so the // "flagged source" mark on /browse/find was untestable: with no file the set is // empty and every assertion about it passes vacuously. // // It is DERIVED, never copied. The real file holds 116 real human judgements // about which episodes carry a second speaker, and a suite that asserted // against them would be asserting against somebody's ear. So this picks the // first episode that actually says "the" -- so the flag has hits to hide -- and // writes one key naming it. The spec reads this file back rather than guessing, // which keeps the assertion exact whatever the corpus holds. let flagged = null; const asrDir = path.join(dest, "data", "asr"); if (existsSync(asrDir)) { for (const f of readdirSync(asrDir).filter((x) => x.endsWith(".json")).sort()) { let words; try { words = JSON.parse(readFileSync(path.join(asrDir, f), "utf8")).words ?? []; } catch { continue; } const w = words.find((x) => String(x.w).toLowerCase().replace(/[^a-z]/g, "") === "the"); if (!w) continue; flagged = { video: f.slice(0, -5), start: +w.start }; break; } } writeFileSync( path.join(dest, "code", "suspect-sources.json"), JSON.stringify( flagged ? [ { k: `${flagged.video}@${flagged.start.toFixed(2)}`, why: "seeded by make-fixture so the flagged-source filter has something to hide", }, ] : [], null, 1, ), ); // -- the mix bench: a reports dir of its own, and two SYNTHESISED tracks ------- // // The bench RENDERS, and its default output directory is the real reports // directory -- the one holding every finished deliverable. A spec that rendered // there would be indistinguishable from a person doing it, exactly as a spec // that judged a clip would be. So the suite gets its own reports root // (SONG_REPORTS_DIR, set by playwright.config.ts) and its own inputs. // // The inputs are GENERATED rather than copied, because the assertions are about // measured instants and a synthetic file is the only way to know the true answer // in advance: // // bg.mp4 a low tone for 3s, then a high one -- so the band-ratio step // detector must report a cue at 3.00s. This is the Pokemon alarm // in miniature: both halves are the SAME loudness, so an envelope // cannot see the change and only the brightness curve can. // song.mp4 silence for 2s, then a tone -- so firstSound must be 2.00s. const reports = path.join(dest, "reports"); mkdirSync(reports, { recursive: true }); const ff = (args) => { const r = spawnSync("ffmpeg", ["-nostdin", "-v", "error", "-y", ...args], { encoding: "utf8" }); if (r.status !== 0) throw new Error(`ffmpeg failed: ${r.stderr || r.status}`); }; // Both are padded well past listMedia's 256 KB "is this a real track" floor. ff([ "-f", "lavfi", "-i", "testsrc=size=640x360:rate=30:duration=6", "-f", "lavfi", "-i", "sine=frequency=220:duration=3", "-f", "lavfi", "-i", "sine=frequency=3000:duration=3", "-filter_complex", "[1:a][2:a]concat=n=2:v=0:a=1[a]", "-map", "0:v", "-map", "[a]", "-t", "6", "-c:v", "libx264", "-b:v", "1200k", "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", path.join(reports, "bg.mp4"), ]); ff([ "-f", "lavfi", "-i", "testsrc=size=640x360:rate=30:duration=6", "-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo:d=2", "-f", "lavfi", "-i", "sine=frequency=440:duration=4", "-filter_complex", "[1:a][2:a]concat=n=2:v=0:a=1[a]", "-map", "0:v", "-map", "[a]", "-t", "6", "-c:v", "libx264", "-b:v", "1200k", "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", path.join(reports, "song.mp4"), ]); // -- one PLAN file, so "used in a build" is testable --------------------------- // // The drop-confirm exists because dropping a clip that is placed in a rendered // plan makes that render unreproducible from the palette. Testing it needs a // plan that references a clip the suite can actually reach, so this writes one // naming the FIRST candidate of the first copied cand2 file -- and deliberately // only that one, so the spec can also assert that a clip NOT in a build is // dropped without a prompt. const copied = readdirSync(path.join(dest, "data", "cand2")).filter((f) => f.endsWith(".json")); let planned = null; if (copied.length) { const one = JSON.parse(readFileSync(path.join(dest, "data", "cand2", copied[0]), "utf8")); const c = one.candidates?.[0]; if (c) { planned = `${c.video}@${(+c.start).toFixed(2)}`; writeFileSync( path.join(reports, "fixture-build.plan.json"), JSON.stringify( { scale: "test", voices: [ { name: "melody", plan: [ { slotStart: 1.5, video: c.video, srcStart: +c.start, srcEnd: +c.end }, // A second placement of the SAME clip, so "used in 1 build, // 2 placements" is exercised rather than assumed. { slotStart: 9.25, video: c.video, srcStart: +c.start, srcEnd: +c.end }, ], }, ], }, null, 1, ), ); } } // -- a videos/ tree, for the browse pages ------------------------------------- // // PURELY ADDITIVE: bg.mp4 and song.mp4 above keep their exact shape, because // mix.spec.ts asserts measured instants against them. // // Every file gets a DISTINCT duration, so a duration assertion has a true // answer and `promote` can be checked by the length of what ends up shipping // rather than by trusting the response. Two songs: // // alpha all four cuts, three variants, a plan, a clips.csv and a README // beta two cuts and nothing else -- the hole in the cut set, which a // scan-derived cut list would render as complete const VIDEOS = path.join(reports, "videos"); /** * A silent video of a known length, at a colour that identifies it on sight. * * CBR is not cosmetic. listMedia() drops anything under 256 KB as "a fragment, * a probe or a one-note extraction", and a flat colour encodes to about eight * kilobytes -- so an unpadded fixture cut is invisible to the mix picker, and * the grouped-picker spec sees an empty list while the code under it is * perfectly correct. Real deliverables are 26 to 157 MB. * * `-b:v` alone does nothing here: libx264 defaults to CRF and ignores it on * content this compressible. Constant bitrate forces the padding, and leaves * the duration exactly as asked. */ const BULK = [ "-c:v", "libx264", "-b:v", "1500k", "-minrate", "1500k", "-maxrate", "1500k", "-bufsize", "3000k", "-x264-params", "nal-hrd=cbr:force-cfr=1", ]; const clip = (file, seconds, colour) => { mkdirSync(path.dirname(file), { recursive: true }); ff([ "-f", "lavfi", "-i", `color=c=${colour}:size=320x180:rate=15:duration=${seconds}`, "-f", "lavfi", "-i", `anullsrc=r=48000:cl=stereo:d=${seconds}`, "-t", String(seconds), ...BULK, "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", file, ]); }; const ALPHA = path.join(VIDEOS, "alpha"); clip(path.join(ALPHA, "wide.mp4"), 6, "navy"); clip(path.join(ALPHA, "wide-short.mp4"), 3, "teal"); clip(path.join(ALPHA, "vertical.mp4"), 5, "purple"); clip(path.join(ALPHA, "vertical-short.mp4"), 2, "maroon"); // `wide-short-nokit` MUST bind to `wide-short`, never to `wide`. That is the // single highest-value assertion in the browse suite, so both live here. clip(path.join(ALPHA, "variants", "wide-nokit.mp4"), 7, "olive"); clip(path.join(ALPHA, "variants", "wide-short-nokit.mp4"), 4, "green"); clip(path.join(ALPHA, "variants", "vertical-short-crop.mp4"), 1, "gray"); const BETA = path.join(VIDEOS, "beta"); clip(path.join(BETA, "wide.mp4"), 8, "red"); clip(path.join(BETA, "wide-short.mp4"), 9, "blue"); // A song-level cover, keyed the way thumbs/ is: `-a-*`. mkdirSync(path.join(reports, "thumbs"), { recursive: true }); ff([ "-f", "lavfi", "-i", "color=c=orange:size=320x180", "-frames:v", "1", path.join(reports, "thumbs", "alpha-a-auto.jpg"), ]); writeFileSync( path.join(ALPHA, "README.md"), "# Alpha — the fixture song\n\nBuilt by nothing. It exists so the browse pages have a tree.\n\n" + "| file | |\n|---|---|\n| `wide.mp4` | 6s |\n| `wide-short.mp4` | 3s |\n", ); // The plan and the CSV name the SAME real candidate the plan above already // discovered, so the provenance join has something real to join to -- // titles.json and dates.json are already copied into the fixture. if (planned) { const [pv, ps] = [planned.slice(0, planned.lastIndexOf("@")), Number(planned.slice(planned.lastIndexOf("@") + 1))]; mkdirSync(path.join(ALPHA, "plan"), { recursive: true }); writeFileSync( path.join(ALPHA, "plan", "alpha.plan.json"), JSON.stringify( { scale: "test", voices: [ { name: "melody", plan: [ { slotStart: 1.5, noteDur: 0.2, video: pv, srcStart: ps, srcEnd: ps + 0.3, shift: -0.02, keepSide: 0 }, ], }, ], }, null, 1, ), ); writeFileSync( path.join(ALPHA, "clips.csv"), "songTime,voice,clipId,video,srcStart,srcEnd,noteDur,shift,keepSide\n" + `1.500,melody,${pv}@${ps.toFixed(2)},${pv},${ps.toFixed(3)},${(ps + 0.3).toFixed(3)},0.200,-0.02,0\n`, ); } // -- what the six deck tools are asserted against ------------------------------ // // PURELY ADDITIVE, like the videos/ tree above. bg.mp4 and song.mp4 keep their // exact shape because mix.spec.ts asserts measured instants against them, and // alpha's cuts and variants keep theirs because browse.spec.ts asserts both. // // So the deck's own subject is a THIRD song, `gamma`, which nothing else in the // suite touches. That isolation is what lets one assertion be "exactly these // open decisions and nothing else": alpha is mutated by browse.spec (it // promotes a variant, writes notes and overwrites spec.json) and beta collects // a note on a cut slot, so neither can carry an exact count. // // Every seeded item has a TRUE ANSWER. A fixture that only proves a page // renders proves nothing about whether it is right. const DECK = path.join(VIDEOS, "deck"); // (3) LOUDNESS. Two otherwise-identical files 10 dB apart. An absolute LUFS // assertion would be brittle across ffmpeg builds; a DELTA has a true answer // and is immune to encoder detail. Measured here: exactly 10.0 LU. const tone = (file, db) => { mkdirSync(path.dirname(file), { recursive: true }); ff([ "-f", "lavfi", "-i", "color=c=black:size=320x180:rate=15:duration=4", "-f", "lavfi", "-i", "sine=frequency=440:duration=4", "-filter_complex", `[1:a]volume=${db}dB[a]`, "-map", "0:v", "-map", "[a]", "-t", "4", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", file, ]); }; tone(path.join(DECK, "wide.mp4"), 0); tone(path.join(DECK, "variants", "wide-quiet.mp4"), -10); // A variant that HAS been judged. It must not appear in the inbox, which is // the assertion that keeps "unjudged" meaning something. tone(path.join(DECK, "variants", "wide-judged.mp4"), -6); writeFileSync( path.join(DECK, "verdicts.json"), JSON.stringify( { "variants/wide-judged.mp4": { verdict: "reject", note: "already decided, so it is not an open decision", at: "2026-01-01T00:00:00.000Z", }, }, null, 1, ), ); // (1) THE INBOX. One spec error that is genuinely blocking (a background // outside every root), one stale recipe (a build stamped with bytes the file // does not have), one unjudged variant that competes with a shipped cut -- and // three missing cuts that must come out as `info`, because nothing in deck // says it was ever meant to have a vertical. writeFileSync( path.join(DECK, "spec.json"), JSON.stringify( { version: 1, song: "deck", background: { path: "/etc/hosts", note: "deliberately outside every root" }, loudness: { targetLufs: -14, truePeak: -1 }, }, null, 1, ), ); writeFileSync( path.join(DECK, "build.json"), JSON.stringify( { version: 2, song: "deck", builds: { "wide.mp4": { script: "fixture.sh", env: {}, plans: [], assets: [], duration: 4, note: "stamped with the wrong byte count on purpose", file: { bytes: 1, mtime: "2020-01-01T00:00:00.000Z" }, written: "2020-01-01T00:00:00.000Z", }, }, }, null, 1, ), ); writeFileSync( path.join(DECK, "README.md"), "# Deck — the deck fixture\n\nTwo tones 10 dB apart, one judged variant and one that is not.\n", ); // (5) THUMBNAILS. Covers whose corner videos decide whether accepting one is // allowed: alpha-b is disjoint from the accepted alpha-c and may be accepted; // alpha-d shares v1 with it and must be refused BY NAME. alpha-c is accepted // while the filename convention would pick alpha-a-auto.jpg -- which is exactly // the bug thumbFor() had. for (const [n, colour] of [["alpha-c.jpg", "green"], ["alpha-b.jpg", "blue"], ["alpha-d.jpg", "red"]]) { ff([ "-f", "lavfi", "-i", `color=c=${colour}:size=320x180`, "-frames:v", "1", path.join(reports, "thumbs", n), ]); } // The corner videos are DERIVED from the symlinked media/, not invented. // // They used to be `v1`...`v11`, which have no media at all -- fine while the // only question asked of a corner was whether accepting it would clash, and // useless the moment /browse/faces wanted to draw the frame it was cut from and // run a detector over it. So the eleven slots are filled with real episode ids, // in sorted order so the fixture is the same on every run, and the SHARING // PATTERN below is untouched: alpha-b is disjoint from the accepted alpha-c and // may be accepted; alpha-d shares its first corner with alpha-c and must be // refused BY NAME. When media/ is missing the synthetic names come back, so the // clash assertions still hold on a machine with no corpus. const mediaDir = path.join(dest, "data", "media"); const episodes = existsSync(mediaDir) ? readdirSync(mediaDir).filter((f) => f.endsWith(".mp4")).sort().map((f) => f.slice(0, -4)) : []; const slot = (n) => episodes[n - 1] ?? `v${n}`; // A distinct second per corner, and well inside every episode -- these clips run // for hours, so any of these lands on a real decodable frame. const corners = (ns) => ns.map((n, i) => ({ video: slot(n), srcStart: 60 + 7 * i, frameAt: 60 + 7 * i })); const thumbManifest = { version: 1, thumbs: { "alpha-a": { out: path.join(reports, "thumbs", "alpha-a-auto.jpg"), corners: corners([1, 2, 3, 4]) }, "alpha-c": { out: path.join(reports, "thumbs", "alpha-c.jpg"), corners: corners([1, 2, 3, 4]) }, "alpha-b": { out: path.join(reports, "thumbs", "alpha-b.jpg"), corners: corners([5, 6, 7, 8]) }, "alpha-d": { out: path.join(reports, "thumbs", "alpha-d.jpg"), corners: corners([1, 9, 10, 11]) }, }, used: [], }; writeFileSync(path.join(dest, "code", "thumb-manifest.json"), JSON.stringify(thumbManifest, null, 1)); // The accepted alpha-c records its `out` RELATIVE to the reports tree, as the // tracked manifests do since release 12 (make-thumb and accept-thumb write it // that way), while the run log above keeps the older absolute form -- so the // suite reads both. deck.spec's "serves the ACCEPTED cover" is the relative one. writeFileSync( path.join(dest, "code", "thumb-accepted.json"), JSON.stringify( { version: 1, thumbs: { "alpha-c": { ...thumbManifest.thumbs["alpha-c"], out: path.join("thumbs", "alpha-c.jpg") } }, used: [1, 2, 3, 4].map(slot), }, null, 1, ), ); // (4) PLAN DIFF and (6) THE USAGE WALK. // // alpha/plan/ grows three files: // // alpha.plan.json four notes rather than one, so a diff has something to // report as unchanged // alpha-v2.plan.json the same, with ONE slot re-sung, one note dropped, one // added, and a whole `bass` voice that exists only here // alpha-body.json no `.plan.json` suffix, three directories down -- the // exact shape usageIndex() used to miss entirely // overlays.json a bare ARRAY in the same directory, which the shape // test must reject rather than parse as a plan if (planned) { const [pv, ps] = [planned.slice(0, planned.lastIndexOf("@")), Number(planned.slice(planned.lastIndexOf("@") + 1))]; const note = (slot, start, shift) => ({ slotStart: slot, noteDur: 0.2, video: pv, srcStart: start, srcEnd: start + 0.3, shift, keepSide: 0, }); writeFileSync( path.join(ALPHA, "plan", "alpha.plan.json"), JSON.stringify( { scale: "test", voices: [ { name: "melody", plan: [note(1.5, ps, -0.02), note(2.5, ps + 1, 0.1), note(3.5, ps + 2, 0.2), note(4.5, ps + 3, 0.3)], }, ], }, null, 1, ), ); writeFileSync( path.join(ALPHA, "plan", "alpha-v2.plan.json"), JSON.stringify( { scale: "test", voices: [ { name: "melody", plan: [note(1.5, ps, -0.02), note(2.5, ps + 9, 0.1), note(3.5, ps + 2, 0.2), note(5.5, ps + 4, 0.4)], }, { name: "bass", plan: [note(1.5, ps + 5, 0), note(2.5, ps + 6, 0)] }, ], }, null, 1, ), ); writeFileSync( path.join(ALPHA, "plan", "alpha-body.json"), JSON.stringify({ scale: "test", voices: [{ name: "melody", plan: [note(0.5, ps, 0)] }] }, null, 1), ); writeFileSync( path.join(ALPHA, "plan", "overlays.json"), JSON.stringify([{ at: 1, text: "not a plan" }], null, 1), ); } // -- a trim set, for the hook editor ------------------------------------------ // // A 10s stem and two hooks cut from it, with a catalogue that names their // windows. Hook 01's window sits well inside the stem so a selection can be // widened PAST it, which is the case the editor exists for. const MKV = path.join(dest, "data", "mkvocals"); mkdirSync(path.join(MKV, "hooks-verb"), { recursive: true }); ff([ "-f", "lavfi", "-i", "sine=frequency=300:duration=10", "-ar", "48000", "-ac", "1", "-c:a", "pcm_s16le", path.join(MKV, "stem48.wav"), ]); for (const [file, ss, to] of [["hook-01.wav", 2.0, 3.0], ["hook-02.wav", 5.0, 6.5]]) { ff([ "-ss", String(ss), "-t", String(to - ss), "-i", path.join(MKV, "stem48.wav"), "-ar", "48000", "-ac", "1", "-c:a", "pcm_s16le", path.join(MKV, "hooks-verb", file), ]); } writeFileSync( path.join(MKV, "hooks-verb", "mk-vocals.json"), JSON.stringify( { stem: "mkvocals/stem48.wav", sr: 48000, hooks: [ { id: "hook-01", file: "hook-01.wav", srcStart: 2.0, srcEnd: 3.0, dur: 1.0, text: "Fixture one." }, { id: "hook-02", file: "hook-02.wav", srcStart: 5.0, srcEnd: 6.5, dur: 1.5, text: "Fixture two." }, ], }, null, 1, ), ); // -- PROJECTS: report videos, a sweep report, and the two routing traps -------- // // REPORTS_ROOT defaults to dirname(SONG_REPORTS_DIR), so everything the project // walk sees is inside this fixture. Each of these exists to give one finding a // TRUE ANSWER rather than a plausible one: // // report-fixture a good manifest whose clips have known cue text, so // "ends mid-sentence" and "the widener would move this" // are checkable rather than believable // no-origin-fixture no siteOrigin at all -- the defect that shipped 19 dead // QR codes in a real cut // localhost-fixture siteOrigin http://localhost:3000 -- the defect that // shipped a real video whose codes resolve on nobody's phone // bike-fixture a sweep report with no manifest: the third kind, and the // proof that adding one costs a registry entry and a view // find/ a project named for a TOOL PAGE. It can never win the // route, and before this it failed silently // deep/nested/solo a pass-through folder chain, so collapsing has an answer const CHANNELS = path.join(dest, "channels"); // Two sources, deliberately different in the one way that matters to widening. // // vid1 is PUNCTUATED and carries a run-on cue at 3-6s, so a clip ending at 6.0 // ends mid-sentence and widen() must walk it out to 9.0 (the next cue that // closes one). vid2 has NO terminator anywhere, which is the real degradation // this corpus has -- widening cannot help there and the tool has to say so // instead of silently doing nothing. const CUES = { vid1: [ [0, 3, "This is a complete sentence."], [3, 6, "And this one runs on and because"], [6, 9, "of that it finishes here."], [9, 12, "Another whole sentence entirely."], [12, 15, "A fourth one, done."], [15, 18, "trailing off and then"], [18, 21, "it lands at last."], ], // Cited by gone-fixture. Its cue file exists (so the manifest is readable) but // the stub yt-dlp reports it removed, which is what gives `source-unavailable` // a true answer rather than a plausible one. gone1: [ [0, 3, "This upload has since been deleted."], [3, 6, "But its transcript is still in the archive."], ], // Cited by the long-form fixtures: a transcript that STOPS at 6 s under a // clip that runs to 12. The real case is quartering-gout's c03 (861–897 s on // cues that ended at 880), recorded only in prose until the Sources view. short1: [ [0, 3, "The transcript covers this much."], [3, 6, "And then it simply stops."], ], vid2: [ [0, 3, "no punctuation anywhere in this upload"], [3, 6, "the asr never emitted a full stop"], [6, 9, "so every cue just runs into the next"], [9, 12, "and widening has nothing to find"], [12, 15, "which is a thing to say out loud"], ], // TWO SOURCES NOBODY ELSE TOUCHES, for the editor-fetch specs. // // A clip window now lands in the CORPUS, which is keyed by VIDEO and shared // by every project — that is the feature. So a spec asserting "this window // is not fetched yet" cannot cite a video another spec fetches: clip-bench's // own fetch tests pull vid1, and the editor-fetch spec then found its window // already cached and never made a request. Project isolation is not enough // any more; source isolation is. vid3: [ [0, 3, "The editor fetch fixture speaks."], [3, 6, "And this is the sentence it cites."], [6, 9, "Then it carries on for a while."], [9, 12, "Before finally stopping here."], ], vid4: [ [0, 3, "The sweep fixture opens."], [3, 6, "It has a second clip to fetch."], [6, 9, "And nothing else cites it."], ], // The DELIVER fixture's source. Its own, for the same reason vid3/vid4 are // their own: a cut writes clips/.mp4 and packages them, and a spec that // shared a source with the fetch specs would depend on which ran first. vid6: [ [0, 3, "The deliver fixture opens."], [3, 6, "The first clip is confirmed."], [6, 9, "And so is the second."], [9, 12, "Which does not start on a keyframe."], [12, 15, "The third is nowhere on this disk."], [15, 18, "The fourth was ruled incorrect."], [18, 21, "The fifth nobody has judged."], [21, 24, "And that is the whole cut."], ], // Long enough that one clip's PADDED window can contain another's. See // editor-fetch-reuse-fixture. vid5: [ [0, 3, "The reuse fixture begins here."], [3, 6, "It runs on for a while."], [6, 9, "Long enough to hold two clips."], [9, 12, "One inside the other's pad."], [12, 15, "Which is the whole point."], [15, 18, "The second one costs nothing."], [18, 21, "Because the first already paid."], [21, 24, "And containment is the predicate."], [24, 27, "That is where it ends."], ], }; for (const [vid, rows] of Object.entries(CUES)) { const dir = path.join(CHANNELS, "testchan", "data", vid); mkdirSync(dir, { recursive: true }); writeFileSync( path.join(dir, "transcript.cues.json"), JSON.stringify( { title: `Fixture source ${vid}`, uploadDate: "20250101", // The uploader's DISPLAY name. A real record (local or from a published // shard) carries it, and it heads the burned-in attribution line, so a // fixture without one would exercise only the fallback. channel: "Fixture Channel", webpageUrl: `https://example.invalid/watch?v=${vid}`, duration: rows[rows.length - 1][1], cues: rows.map(([start, end, text]) => ({ start, end, text })), }, null, 1, ), ); } // A font the header's drawtext can actually load, or no header. // // build-video.mjs draws the citation line with `fontfile=''` // and an empty one is a filtergraph error, not a missing label. Rather than // assume a font, look for one and honestly set headerHeight: 0 when there is // none -- which is itself a documented manifest configuration ("a cut whose // sources are listed elsewhere does not need its own attribution burnt in"). const FONT_CANDIDATES = [ "/usr/share/fonts/TTF/FiraSans-Regular.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/liberation/LiberationSans-Regular.ttf", ]; const FONT = FONT_CANDIDATES.find((f) => existsSync(f)) ?? null; const manifest = (slug, title, provenance, timeline, ledger = null) => ({ schemaVersion: 1, slug, title, subtitle: "a fixture", generatedOn: "2026-01-01", provenance: { channelSlug: "testchan", ...provenance }, render: { width: 640, height: 360, fps: 15, audioRate: 48000, audioChannels: 2, maxHeightSource: 360, fetchPad: 3, snapWindow: 1.6, transition: 0.2, crf: 30, preset: "ultrafast", // No timelineNodes, so no footer -- which is what keeps ImageMagick out of // the fixture build entirely. footerHeight: 0, headerHeight: FONT ? 24 : 0, ...(FONT ? { fontRegular: FONT, fontBold: FONT } : {}), palette: { bg: "#12100c", fg: "#f6f1e6", muted: "#a2957f", accent: "#c8752a", amber: "#ffc860" }, }, timelineNodes: [], timeline, ...(ledger ? { ledger } : {}), }); const writeProject = (rel, doc, root = reports) => { const dir = path.join(root, rel); mkdirSync(dir, { recursive: true }); writeFileSync(path.join(dir, "video.manifest.json"), JSON.stringify(doc, null, 2) + "\n"); return dir; }; const REPORT = writeProject( "report-fixture", manifest("report-fixture", "The Report Fixture", { siteOrigin: "https://archive.example" }, [ // Ends inside the run-on cue, and is not locked -> exactly one // clip-mid-sentence decision in the whole fixture, and widen() would move // its end from 6.00 to 9.00. { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, quote: "and because" }, // Ends on a full stop -> clean, and widen() is a no-op. { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, quote: "another whole sentence" }, // An unpunctuated source -> feeds the no-punctuation row, never the // mid-sentence one. { type: "clip", id: "c03", video: "vid2", start: 1.0, end: 4.0, cite: 1, section: 0, quote: "no punctuation" }, // Also ends mid-cue, but lockEnd ACKNOWLEDGES it, so it must stay silent. { type: "clip", id: "c04", video: "vid1", start: 15.0, end: 18.0, cite: 15, section: 0, lockEnd: true, quote: "trailing off" }, // A card, and an entry of a type NOTHING IN THE CODE KNOWS ABOUT. // // The timeline's vocabulary is open: quartering-employee-count carries // `scroll` and `chart` entries beside its cards, and code that treated // anything-not-a-card as a clip sent `undefined` into path.join() and 500'd // the whole project page. Every other real manifest is clips only, which is // exactly why that survived testing. `zz-unknown` is here so it cannot again. { type: "card", id: "k01", style: "chapter", seconds: 3, heading: "A card" }, { type: "zz-unknown", id: "z01", seconds: 5, heading: "An entry type from the future" }, ]), ); writeProject( "no-origin-fixture", manifest("no-origin-fixture", "No Origin", {}, [ { type: "clip", id: "c01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "q" }, ]), ); writeProject( "localhost-fixture", manifest("localhost-fixture", "Localhost Origin", { siteOrigin: "http://localhost:3000" }, [ { type: "clip", id: "c01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "q" }, ]), ); // At the REPORTS_ROOT itself, not under reports/ -- shadowing is about the FIRST // path segment, because that is the one a static route under app/browse/ wins. // `reports/find` is perfectly routable; `find` can never be. writeProject( "find", manifest("find", "Shadowed By A Tool Page", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "q" }, ]), dest, ); writeProject( path.join("deep", "nested", "solo-fixture"), manifest("solo-fixture", "Down A Pass-Through Chain", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "q" }, ]), ); // A SECOND copy of the same project, for the specs that WRITE. // // The clip bench saves windows and sets locks; the index and decision specs // assert what report-fixture's windows are. One fixture for both means the // suite passes or fails depending on which file playwright happened to run // first -- which it did, once, and the failure named the wrong thing entirely. // So the read-only assertions get report-fixture and every mutation gets this. const BENCH = writeProject( "bench-fixture", manifest("bench-fixture", "The Bench Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, quote: "another whole sentence" }, { type: "clip", id: "c03", video: "vid2", start: 1.0, end: 4.0, cite: 1, section: 0, quote: "no punctuation" }, { type: "clip", id: "c04", video: "vid1", start: 15.0, end: 18.0, cite: 15, section: 0, lockEnd: true, quote: "trailing off" }, ], // A three-row ledger, so the claim bench and the two claim decision kinds // have something exact to assert against: // // k01 is fully adjudicated and pinned to a clip -- the settled case. // k02 is adjudicated but DERIVED, so `not_his_number` must fire on it and // it must stay out of the stated series. // k03 carries none of the six fields, so it is the blocking row the inbox // has to show and the page has to offer controls for. [ { id: "k01", date: "2022-01-01", company: "media", value: 4, display: "4", label: "counts four", quote: "and because", src: "vid1 @ 0:03", channel: "testchan", video: "vid1", cite: 3, entryId: "c01", scope: "media", scopeBasis: "names the channel", scopeConfidence: "clear", population: "employees", valueKind: "uttered", flags: [] }, { id: "k02", date: "2022-02-01", company: "all", value: 9, display: "9", label: "4 + 5, summed by us", quote: "another whole sentence", src: "vid1 @ 0:09", channel: "testchan", video: "vid1", cite: 9, entryId: "c02", scope: "all", scopeBasis: "no company named", scopeConfidence: "read", population: "employees", valueKind: "derived", flags: [] }, { id: "k03", date: "2022-03-01", company: "media", value: 5, display: "5", label: "nobody has ruled on this one", quote: "no punctuation", src: "vid2 @ 0:01", channel: "testchan", video: "vid2", cite: 1 }, ]), ); // A FOURTH copy, for the WALK -- and this one is READ-ONLY. // // The walk visits the clips that still need judgement and are fetched, so what // it skips has to be pinned down by the fixture rather than by whichever spec // ran last. bench-fixture cannot do it: every test in that file writes verdicts // into it, so "ready 2 of 3" would be true only until somebody pressed `y`. // // Four clips, all on vid1 (whose cues run 0-21, so none of them outruns its // transcript), locked so none of them raises a decision: // // w01 3.00- 6.00 fetched, unjudged -> on the walk // w02 9.20-11.20 NOT fetched -> skipped; nothing to play // w03 12.00-15.00 fetched, CONFIRMED -> skipped; already answered // w04 15.00-18.00 fetched, unjudged -> on the walk // // w02 sits in the GAP between two cached files rather than merely outrunning // one: the bench plays the best OVERLAPPING file it has, so a clip half-held by // a neighbour's download still shows a picture. That is a different state from // "nothing to play", and the walk skips both -- neither can be watched through. // // So the walk is w01 <-> w04, over two different reasons, and the counter reads // `ready 2 of 3`: three clips need judgement, two of them can have it today. const WALK = writeProject( "walk-fixture", manifest("walk-fixture", "The Walk Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "w01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "w02", video: "vid1", start: 9.2, end: 11.2, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, { type: "clip", id: "w03", video: "vid1", start: 12.0, end: 15.0, cite: 12, section: 0, lock: true, quote: "a fourth one", verdict: "confirmed" }, { type: "clip", id: "w04", video: "vid1", start: 15.0, end: 18.0, cite: 15, section: 0, lock: true, quote: "trailing off" }, ]), ); // -- THE EDITOR-FETCH FIXTURE ------------------------------------------------- // // One clip on vid3 — a source nothing else cites — and NOTHING in // out/clips-raw, so it reads "not fetched yet" and `ready 0 of 1`. That is the // state the editor fetch exists to leave: // after it, the window is in the CORPUS (channels/testchan/data/vid1/clips/) // rather than in this project's own out/ — which is the whole argument, since // the next report citing vid1 gets it for free. // // Its own project for the same reason bench-fixture is: every test here writes // (a fetch, and then a corpus file), and sharing would make one suite's result // depend on the other's order. writeProject( "editor-fetch-fixture", manifest("editor-fetch-fixture", "The Editor Fetch Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "e01", video: "vid3", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and this is the sentence", note: "the speaker names the number here" }, ]), ); // The SAME state, on a different source, for the "fetch every unfetched clip" // button. Two projects rather than two clips in one, because the first test // leaves a window in the corpus and the corpus is keyed by VIDEO: sharing vid1 // would make the button's pending list depend on which spec ran first, which is // the order-dependence bench-fixture already exists to avoid. writeProject( "editor-fetch-many-fixture", manifest("editor-fetch-many-fixture", "The Editor Fetch Sweep Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "f01", video: "vid4", start: 0.0, end: 3.0, cite: 0, section: 0, lock: true, quote: "the sweep fixture opens" }, { type: "clip", id: "f02", video: "vid4", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "a second clip to fetch" }, ]), ); // A SWEEP WHERE THE SECOND CLIP IS ALREADY PAID FOR. // // g01 is 5–25 s, so the route's default ±20 s of pad makes it 0.00–45.00. // g02 is 10–15 s, whose padded ask is [0, 35] — inside that. So a sweep that // fetches g01 finds g02 already covered and the route answers 409 "already // cached", which is the COMMON case (a generous window routinely covers its // neighbour) and must not read as a failure that aborts the run. // // Its own source again: the corpus is keyed by video, so this could not share // one with the fixtures above. writeProject( "editor-fetch-reuse-fixture", manifest("editor-fetch-reuse-fixture", "The Editor Fetch Reuse Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "g01", video: "vid5", start: 5.0, end: 25.0, cite: 5, section: 0, lock: true, quote: "runs on for a while" }, { type: "clip", id: "g02", video: "vid5", start: 10.0, end: 15.0, cite: 10, section: 0, lock: true, quote: "one inside the other" }, ]), ); // -- THE TAG FIXTURE ---------------------------------------------------------- // // Cited videos over TWO SECTIONS, for "tag cited videos as …". Nothing here is // fetched, cut or judged: the action reads the manifest and asks the editor, so // the fixture is the citation graph and nothing else. It shares sources with // the read-only fixtures above deliberately — it writes no corpus file. // // a01, a02 two clips on vid1 -> section A is ONE video, which is the point: // a report cites a recording many times and // the tag lands on it once // b01 vid2 -> section B // b02 a clip whose LOCAL name is not the archive's: `video` is the URL // slug a cue directory is named for, while siteVideo/siteChannel say // what the published archive calls it. Rumble's two ids, and the // editor must be asked with the archive's. // A project whose manifest names NO channel — neither on the clip nor in its // provenance. The tag route must say which clips it could not name rather than // quietly tagging a smaller set than the operator asked for. writeProject( "editor-tag-nochannel-fixture", manifest("editor-tag-nochannel-fixture", "The Unnamed Channel Fixture", { channelSlug: null, siteOrigin: "https://archive.example" }, [ { type: "clip", id: "a01", video: "vid1", channel: "testchan", start: 3.0, end: 6.0, cite: 3, section: 0, quote: "this one is named" }, { type: "clip", id: "a02", video: "vid2", start: 1.0, end: 4.0, cite: 1, section: 0, quote: "this one is not" }, ]), ); writeProject( "editor-tag-fixture", manifest("editor-tag-fixture", "The Editor Tag Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "a01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, quote: "and because" }, { type: "clip", id: "a02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, quote: "another whole sentence" }, { type: "clip", id: "b01", video: "vid2", start: 1.0, end: 4.0, cite: 1, section: 0, quote: "no punctuation" }, { type: "clip", id: "b02", video: "a-rumble-url-slug", siteVideo: "v2embedid", siteChannel: "testchan-rumble", start: 1.0, end: 4.0, cite: 1, section: 0, quote: "two ids" }, ]), ); // -- THE DELIVER FIXTURE ------------------------------------------------------ // // What a walked report owes its readers, in one project: the cut clips, the // batch, the fold-back. Its own source (vid6) and its own directory, because // every test here WRITES -- a cut lands in clips/, a batch in share-*/, and the // project's own scripts leave marker files. // // Six clips over TWO SECTIONS, which is the point: the panel folds on the id's // letter prefix, and "4 of 6 confirmed" says nothing about the section where a // clip was thrown out. // // a01 3.00- 6.00 confirmed, cached, no file -> cut; starts ON one of the // window's keyframes // a02 9.50-12.50 confirmed, cached, no file -> cut; the nearest keyframe // is at 9.00, so a copy would // be half a second long // a03 30.00-33.00 confirmed, NOTHING cached -> "not fetched": a download, // which this panel never does // b01 0.00- 3.00 confirmed, cached, HAS a file AND is listed in an existing // share-first/LIST.md -> the // batch must skip it // b02 15.00-18.00 INCORRECT, with a correction and a line of prose citing it // b03 18.00-21.00 nobody has judged it -> Apply is refused without "partial" const DELIVER = writeProject( "deliver-fixture", manifest("deliver-fixture", "The Deliver Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "a01", video: "vid6", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, verdict: "confirmed", date: "2025-01-01", title: "A Fixture Stream", quote: "The first clip is confirmed." }, { type: "clip", id: "a02", video: "vid6", start: 9.5, end: 12.5, cite: 9, section: 0, lock: true, verdict: "confirmed", date: "2025-01-02", title: "A Fixture Stream", quote: "Which does not start on a keyframe." }, { type: "clip", id: "a03", video: "vid6", start: 30.0, end: 33.0, cite: 30, section: 0, lock: true, verdict: "confirmed", date: "2025-01-03", title: "A Fixture Stream", quote: "Nothing on this disk holds it." }, { type: "clip", id: "b01", video: "vid6", start: 0.0, end: 3.0, cite: 0, section: 0, lock: true, verdict: "confirmed", date: "2025-01-04", title: "A Fixture Stream", quote: "The deliver fixture opens." }, { type: "clip", id: "b02", video: "vid6", start: 15.0, end: 18.0, cite: 15, section: 0, lock: true, verdict: "incorrect", correction: "the speaker is the guest, not the host", date: "2025-01-05", title: "A Fixture Stream", quote: "The fourth was ruled incorrect." }, { type: "clip", id: "b03", video: "vid6", start: 18.0, end: 21.0, cite: 18, section: 0, lock: true, date: "2025-01-06", title: "A Fixture Stream", quote: "The fifth nobody has judged." }, ]), ); // The report's OWN scripts, which the panel runs rather than reimplements. // // Two-line stand-ins for ~/reports/elfpire-eva's apply-manifest.py and // build.py: they write a marker and print what the real ones print. That is // the whole of what can be tested about them here -- the real pair live beside // the prose they rewrite, in an unversioned reports directory, and differ per // report. What IS under test is that the panel finds them, runs them in order, // refuses to run the first one over a half-walked cut, and shows their output // verbatim. writeFileSync( path.join(DELIVER, "apply-manifest.py"), `#!/usr/bin/env python3 # Fixture stand-in: prints what the real one prints, and leaves a marker. import json, os, re HERE = os.path.dirname(os.path.abspath(__file__)) m = json.load(open(os.path.join(HERE, "video.manifest.json"))) clips = [e for e in m["timeline"] if e.get("type") == "clip"] wrong = [e for e in clips if e.get("verdict") == "incorrect"] content = open(os.path.join(HERE, "content.py"), encoding="utf-8").read().splitlines() print("clips.json: 0 field change(s) across 0 clip(s)") print("review: %d clips: %d incorrect, %d confirmed, %d not yet reviewed" % ( len(clips), len(wrong), sum(1 for e in clips if e.get("verdict") == "confirmed"), sum(1 for e in clips if not e.get("verdict")))) for e in wrong: print("== %s" % e["id"]) print(" correction: %s" % e.get("correction")) pat = re.compile(r"\\[clip:%s\\]|[\\"']%s[\\"']" % (e["id"], e["id"])) for i, line in enumerate(content): if pat.search(line): print(" content.py:%d: %s" % (i + 1, line.strip()[:220])) open(os.path.join(HERE, "applied.marker"), "w").write("applied\\n") print("APPLY-DONE") `, { mode: 0o755 }, ); writeFileSync( path.join(DELIVER, "build.py"), `#!/usr/bin/env python3 # Fixture stand-in for the report renderer: one file per variant, so "once per # content module, with build.py's own stem convention" is checkable on disk. import argparse, os ap = argparse.ArgumentParser() ap.add_argument("--content", default="content") ap.add_argument("--out", default="report") a = ap.parse_args() HERE = os.path.dirname(os.path.abspath(__file__)) open(os.path.join(HERE, a.out + ".html"), "w").write("\\n" % a.content) print("BUILD-OK %s -> %s.html" % (a.content, a.out)) `, { mode: 0o755 }, ); // The prose. Its SECTIONS' headings are what name a batch's folders (nth // heading to nth letter), and one paragraph cites the clip the walk threw out // -- which is the line the panel has to surface and never rewrite. writeFileSync( path.join(DELIVER, "content.py"), `TITLE = "The Deliver Fixture" SECTIONS = [ {"id": "one", "heading": "1. The first fixture section", "blocks": [("p", "Two clips carry this one."), ("clips", ["a01", "a02", "a03"])]}, {"id": "two", "heading": "2. The second fixture section", "blocks": [("p", "The claim rests on [clip:b02], which the walk threw out."), ("clips", ["b01", "b02"])]}, ] `, ); writeFileSync( path.join(DELIVER, "content_lawyer.py"), `TITLE = "The Deliver Fixture, for lawyers" SECTIONS = [ {"id": "one", "heading": "1. The narrow cut", "blocks": [("clips", ["a01"])]}, ] `, ); // A batch that already went out. The folders ARE the record, so the exclusion // list is read back from this rather than from a second file that would drift // from it -- by FILE NAME for b01, and out of the prose for a02, which is the // real project's case: six clips went out in an earlier set under different // ids, and the sentence naming them is the only thing that ties the two. mkdirSync(path.join(DELIVER, "share-first"), { recursive: true }); writeFileSync( path.join(DELIVER, "share-first", "LIST.md"), [ "# deliver-fixture — share batch `first`", "", "1 clip, sent before this fixture was born — and 1 already shared (a02) in", "an earlier set under another id, which is the only record that it went out.", "", "## 2. The second fixture section (`B-The-second-fixture-section/`)", "", "- **b01_2025-01-04_A-Fixture-Stream.mp4** — 2025-01-04 · 3s", "", ].join("\n"), ); // The cache the cut reads: ONE window holding every clip but a03. // // KEYFRAMES EVERY THREE SECONDS, which is what a window fetched with // --force-keyframes-at-cuts has. a01 starts on one (3.00) and a02 does not // (9.50) -- and measured, NEITHER copy survives, because the aac frames do not // end where the keyframe does and a copy of this window comes out 0.14 s long. // That is exactly why the cut measures its result instead of predicting it // from the keyframes, and why both clips still come out to the frame. mkdirSync(path.join(DELIVER, "out", "clips-raw"), { recursive: true }); ff([ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=30:duration=24", "-f", "lavfi", "-i", "sine=frequency=440:duration=24", "-t", "24", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-force_key_frames", "expr:gte(t,n_forced*3)", "-c:a", "aac", "-ar", "48000", "-ac", "2", path.join(DELIVER, "out", "clips-raw", "vid6_0.00-24.00.mp4"), ]); // b01 already has its file: it is confirmed AND shared, so it is the clip the // batch must leave out rather than one the cut has to make. mkdirSync(path.join(DELIVER, "clips"), { recursive: true }); ff([ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=30:duration=3", "-f", "lavfi", "-i", "sine=frequency=330:duration=3", "-t", "3", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", path.join(DELIVER, "clips", "b01.mp4"), ]); // A SECOND deliver project, for STOP. // // Its own, because the cut is one job over every clip that needs one: a spec // that stopped halfway through deliver-fixture's two would leave that project // in a state the batch tests do not expect, and a six-clip job is what makes // "stopped after k of n" a measurement rather than a race. // // Six confirmed clips, none of them cut, all inside the same cached window. // With E2E_UMTOOL_CUT_DELAY_MS set (playwright.config.ts) each step takes about // two seconds, so the spec can see the first CUT-OK, press Stop, and know that // at least three clips could not possibly have been reached. const STOP = writeProject( "deliver-stop-fixture", manifest("deliver-stop-fixture", "The Deliver Stop Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "s01", video: "vid6", start: 0.0, end: 2.0, cite: 0, section: 0, lock: true, verdict: "confirmed", date: "2025-02-01", title: "A Fixture Stream", quote: "The deliver fixture opens." }, { type: "clip", id: "s02", video: "vid6", start: 2.0, end: 4.0, cite: 2, section: 0, lock: true, verdict: "confirmed", date: "2025-02-02", title: "A Fixture Stream", quote: "The first clip is confirmed." }, { type: "clip", id: "s03", video: "vid6", start: 4.0, end: 6.0, cite: 4, section: 0, lock: true, verdict: "confirmed", date: "2025-02-03", title: "A Fixture Stream", quote: "And so is the second." }, { type: "clip", id: "s04", video: "vid6", start: 6.0, end: 8.0, cite: 6, section: 0, lock: true, verdict: "confirmed", date: "2025-02-04", title: "A Fixture Stream", quote: "And so is the third." }, { type: "clip", id: "s05", video: "vid6", start: 8.0, end: 10.0, cite: 8, section: 0, lock: true, verdict: "confirmed", date: "2025-02-05", title: "A Fixture Stream", quote: "Which does not start on a keyframe." }, { type: "clip", id: "s06", video: "vid6", start: 10.0, end: 12.0, cite: 10, section: 0, lock: true, verdict: "confirmed", date: "2025-02-06", title: "A Fixture Stream", quote: "Nor does this one." }, ]), ); mkdirSync(path.join(STOP, "out", "clips-raw"), { recursive: true }); copyFileSync( path.join(DELIVER, "out", "clips-raw", "vid6_0.00-24.00.mp4"), path.join(STOP, "out", "clips-raw", "vid6_0.00-24.00.mp4"), ); // An EMPTY clips/, so the spec can count files without first asking whether // the directory exists -- and so "nothing has been cut yet" is a state the // fixture states rather than one it leaves to chance. mkdirSync(path.join(STOP, "clips"), { recursive: true }); // -- STUB BINARIES, so a build is offline and deterministic -------------------- // // The pipeline shells out to yt-dlp for the availability preflight and for every // fetch. Neither belongs in a test: the first needs the network and the second // needs somebody else's server to still be serving. YTDLP_BIN and QRENCODE_BIN // already exist as overrides for exactly this, so the fixture provides both. // // They are NODE scripts, not shell. The yt-dlp stub has to parse // `--download-sections *FROM-TO` and do fractional arithmetic on it, and doing // that in bash means awk, which means three layers of quoting inside a // generated file. It got mangled once; this cannot. // // The stub gives `source-unavailable` a TRUE answer: any URL naming `gone1` // fails the way a removed upload does, so a manifest citing it is genuinely // blocked rather than assumed to be. const BIN = path.join(dest, "bin"); mkdirSync(BIN, { recursive: true }); writeFileSync( path.join(BIN, "yt-dlp"), `#!/usr/bin/env node // Fixture stub for yt-dlp. Deterministic, offline. import { spawnSync } from "node:child_process"; import { appendFileSync, mkdirSync } from "node:fs"; import path from "node:path"; const argv = process.argv.slice(2); const all = argv.join(" "); // EVERY invocation is logged, because one spec's assertion is that there were // ZERO of them: the editor fetch must not shell out to yt-dlp here. appendFileSync(${JSON.stringify(path.join(BIN, "yt-dlp.invocations"))}, all + "\\n"); if (argv.includes("--version")) { process.stdout.write("2026.01.01-fixture\\n"); process.exit(0); } if (all.includes("gone1")) { process.stderr.write("ERROR: [youtube] gone1: Video unavailable. This video has been removed by the uploader\\n"); process.exit(1); } if (argv.includes("--simulate")) process.exit(0); const out = argv[argv.indexOf("-o") + 1]; if (!out || argv.indexOf("-o") < 0) { process.stderr.write("stub: no -o\\n"); process.exit(2); } const sec = argv[argv.indexOf("--download-sections") + 1] ?? "*0-5"; const [from, to] = sec.replace(/^\\*/, "").split("-").map(Number); const dur = Math.max(1, (to || 5) - (from || 0)); mkdirSync(path.dirname(out), { recursive: true }); const r = spawnSync( "ffmpeg", ["-nostdin", "-v", "error", "-y", "-f", "lavfi", "-i", \`color=c=darkgreen:size=320x180:rate=15:duration=\${dur}\`, "-f", "lavfi", "-i", \`sine=frequency=440:duration=\${dur}\`, "-t", String(dur), "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", out], { stdio: "inherit" }, ); process.exit(r.status ?? 1); `, { mode: 0o755 }, ); writeFileSync( path.join(BIN, "qrencode"), `#!/usr/bin/env node // Fixture stub for qrencode: a real code is not needed to prove one was overlaid. import { spawnSync } from "node:child_process"; import { mkdirSync } from "node:fs"; import path from "node:path"; const argv = process.argv.slice(2); if (argv.includes("--version")) { process.stdout.write("qrencode version 0.0.0-fixture\\n"); process.exit(0); } const i = argv.indexOf("-o"); if (i < 0) process.exit(2); const out = argv[i + 1]; mkdirSync(path.dirname(out), { recursive: true }); const r = spawnSync( "ffmpeg", ["-nostdin", "-v", "error", "-y", "-f", "lavfi", "-i", "color=c=white:size=64x64", "-frames:v", "1", out], { stdio: "inherit" }, ); process.exit(r.status ?? 1); `, { mode: 0o755 }, ); // A THIRD copy, for the build specs. // // Same reason bench-fixture exists: a build writes out/, stamps deliverables // aside and is refused when one is newer than its manifest. Sharing a project // with the bench specs would make each suite's result depend on the other's // order, which has already cost one confusing red. const BUILD = writeProject( "build-fixture", manifest("build-fixture", "The Build Fixture", { siteOrigin: "https://archive.example" }, [ // c01 carries a CUT inside its extent: 3.00-6.00 is what somebody reviewed, // 3.50-4.20 is what the quote needs, and the build must render the second // while the fetch and the cache stay keyed on the first. The end is chosen // to sit MORE than snapWindow (1.6 s) from the silence at 5.92 -- otherwise // snapping would carry it back out to the extent's own edge and the test // would pass whether or not the cut was honoured. { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cutStart: 3.5, cutEnd: 4.2, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, ]), ); // A cut whose source is GONE. The preflight must block it, and must block it // before anything encodes -- which is the whole reason it is step 1 rather than // a preamble somebody remembers to run. writeProject( "gone-fixture", manifest("gone-fixture", "A Source That Is Gone", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "gone1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "deleted" }, ]), ); // -- THE ON-SCREEN DECK ------------------------------------------------------- // // A stub HYPERFRAMES_BIN, so a deck build renders offline in a second instead // of fetching a renderer with npx and driving a browser per frame. // // It keeps the renderer's contract, which is what the build checks: exactly // round(data-duration * fps) files `frame_%06d.png` (numbered from 1), at the // composition's own size, in --output. Every argv is logged, so a spec can // tell a render from a cache hit. // // The frames ALTERNATE between RGBA and RGB PNGs on purpose. A real render's // sequence mixes the two (a frame with nothing translucent in it is written // without alpha), and when it does ffmpeg rebuilds the overlay graph per // format change and drops frames -- the deck came out shorter than the cut. // `-reinit_filter 0` + `format=rgba` is the fix, and a stub of one format // would never exercise it. // // The review still is the system chromium's own screenshot (CHROME), not a // stub: it is ~1 s, and a still that only proved a file was written would say // nothing about the composition. writeFileSync( path.join(BIN, "hyperframes"), `#!/usr/bin/env node // Fixture stub for the HyperFrames renderer. Deterministic, offline. import { spawnSync } from "node:child_process"; import { appendFileSync, copyFileSync, mkdirSync, readFileSync, rmSync } from "node:fs"; import path from "node:path"; const argv = process.argv.slice(2); appendFileSync(${JSON.stringify(path.join(BIN, "hyperframes.invocations"))}, argv.join(" ") + "\\n"); if (argv.includes("--version")) { process.stdout.write("0.0.0-fixture\\n"); process.exit(0); } const val = (n) => { const i = argv.indexOf(n); return i < 0 ? null : argv[i + 1]; }; if (argv[0] !== "render" || val("--format") !== "png-sequence") { process.stderr.write("stub: only 'render --format png-sequence' is implemented\\n"); process.exit(2); } const fps = Number(val("--fps")); const out = val("--output"); const proj = argv[argv.length - 1]; const html = readFileSync(path.join(proj, "index.html"), "utf8"); const num = (re, d) => { const m = re.exec(html); return m ? Number(m[1]) : d; }; const width = num(/data-width="(\\d+)"/, 1920); const height = num(/data-height="(\\d+)"/, 190); const duration = num(/data-composition-id="[^"]*"[^>]*data-duration="([\\d.]+)"/, 0); if (!(fps > 0) || !out || !(duration > 0)) { process.stderr.write(\`stub: fps \${fps}, output \${out}, duration \${duration}\\n\`); process.exit(2); } const frames = Math.round(duration * fps); mkdirSync(out, { recursive: true }); // Two source frames, one of each format, then copied out alternately. const tmp = path.join(out, ".stub"); mkdirSync(tmp, { recursive: true }); const make = (file, color, pixFmt) => { const r = spawnSync("ffmpeg", ["-nostdin", "-v", "error", "-y", "-f", "lavfi", "-i", \`color=c=\${color}:size=\${width}x\${height}\`, "-frames:v", "1", "-pix_fmt", pixFmt, file], { stdio: "inherit" }); if (r.status !== 0) process.exit(r.status ?? 1); }; const rgba = path.join(tmp, "rgba.png"); const rgb = path.join(tmp, "rgb.png"); make(rgba, "0x1a2030@0.8", "rgba"); make(rgb, "0x2a3040", "rgb24"); for (let i = 1; i <= frames; i += 1) { copyFileSync(i % 2 ? rgba : rgb, path.join(out, \`frame_\${String(i).padStart(6, "0")}.png\`)); } rmSync(tmp, { recursive: true, force: true }); process.stderr.write(\`stub: \${frames} frames \${width}x\${height} -> \${out}\\n\`); `, { mode: 0o755 }, ); // The deck as the pipeline draws it: a 1920x1080 frame, because that is what // the default panel fits (190 px under footage scaled to 0.82) -- at the other // fixtures' 640x360 the switch's `deck: {}` is refused before anything else // can be tested. The source windows stay 320x180; they are scaled into the // footage box like any other. // // The deck REFUSES to draw without its two faces (they are copied in beside // the composition, never left to local()), so without a font on this machine // there is no deck to test and onscreen.spec.ts skips, naming why. const deckManifest = (slug, title, timeline) => { const m = manifest(slug, title, { siteOrigin: "https://archive.example" }, timeline); m.render = { ...m.render, width: 1920, height: 1080, chrome: { engine: "hyperframes", layout: "deck", deck: {} }, }; delete m.render.headerHeight; return m; }; // onscreen-fixture: the On-screen section and the bench's fields WRITE here -- // the switch, the settings, the table, a stale token. Never built: its // schedule is the estimate, which is the state a report is in when titles are // first written. A card, because a row is any entry and not only a clip; a // teaser last, because the page and the table must name one by its lines. const ONSCREEN = writeProject( "onscreen-fixture", deckManifest("onscreen-fixture", "The On-screen Fixture", [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, { type: "card", id: "k01", style: "chapter", seconds: 3, heading: "A card" }, { type: "teaser", id: "t01", seconds: 6, lines: ["Next Season", { text: "The Big Build in the Valley", break: "in the Valley" }, "Spring 2027"], tail: "?", }, ]), ); // onscreen-build-fixture: BUILT by the spec, then re-rendered on-screen over // the segments that build left. Its own project because a build stamps out/ // and the section specs above must not depend on whether it ran first. Clips // only: a card segment needs ImageMagick with Pango, which the build specs // keep out of the fixture. const ONSCREEN_BUILD = writeProject( "onscreen-build-fixture", deckManifest("onscreen-build-fixture", "The On-screen Build Fixture", [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, ]), ); // onscreen-posts-fixture: the Posts table writes here (onscreen-posts.spec.ts). // The clips carry their own `date`, so which clip a post rides on is the // date rule's answer and not the cue file's one shared upload day: // p-early Aug 1 older than every clip -> c01 ("first") // p-mid Sep 5 after c01's Sep 3 -> c01 ("date") // p-late Sep 12 after c02's Sep 10 -> c02 ("date") // Never built, so the posts' timing is the estimate's. Every post pins its // archive channel (`siteChannel`), so its QR is the archive's page for it and // neither the preview nor a build asks archive.example where the post is kept. const ONSCREEN_POSTS = writeProject( "onscreen-posts-fixture", { ...deckManifest("onscreen-posts-fixture", "The On-screen Posts Fixture", [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because", date: "2024-09-03" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence", date: "2024-09-10" }, { type: "card", id: "k01", style: "chapter", seconds: 3, heading: "A card" }, ]), posts: [ { id: "p-early", platform: "bluesky", author: "Fixture Author", handle: "fixture.example", date: "2024-08-01T12:00:00.000Z", text: "Older than every clip in the cut.", url: "https://bsky.app/profile/fixture.example/post/early", siteChannel: "fixture-bsky", }, { id: "p-mid", platform: "bluesky", author: "Fixture Author", handle: "fixture.example", date: "2024-09-05T09:30:00.000Z", text: "Two days after the first clip.\nA second line.", url: "https://bsky.app/profile/fixture.example/post/mid", siteChannel: "fixture-bsky", }, { id: "p-late", platform: "x", author: "Fixture Author", handle: "fixture", date: "2024-09-12T18:00:00.000Z", text: "Two days after the second clip.", url: "https://x.com/fixture/status/1", siteChannel: "fixture-x", }, ], }, ); // onscreen-feed-fixture: the posts FEED (`posts.layout: "feed"`), BUILT by // onscreen-posts.spec.ts -- every segment framed into the feed's box, one feed // sequence for the whole cut from the stub renderer, no hold -- then refused // a --chrome-only once the layout says popup. Clips only, as the build // fixture above: a card needs Pango. Its posts pin `siteChannel` too: the // build looks nothing up on the network. const ONSCREEN_FEED = (() => { const m = deckManifest("onscreen-feed-fixture", "The On-screen Feed Fixture", [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because", date: "2024-09-03" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence", date: "2024-09-10" }, ]); m.render.chrome.deck = { posts: { layout: "feed" } }; return writeProject("onscreen-feed-fixture", { ...m, posts: [ { id: "f-one", platform: "bluesky", author: "Fixture Author", handle: "fixture.example", date: "2024-09-05T09:30:00.000Z", text: "Rides on the first clip, in from its start.", url: "https://bsky.app/profile/fixture.example/post/one", siteChannel: "fixture-bsky", }, { id: "f-two", platform: "bluesky", author: "Fixture Author", handle: "fixture.example", date: "2024-09-12T18:00:00.000Z", text: "Rides on the second clip.", url: "https://bsky.app/profile/fixture.example/post/two", siteChannel: "fixture-bsky", }, ], }); })(); mkdirSync(path.join(reports, "bike-fixture"), { recursive: true }); writeFileSync( path.join(reports, "bike-fixture", "sweep-report.md"), [ "# The Bike Fixture", "", "A cited report that nobody has turned into a video yet.", "", '> "the first citation"', "— [source @ 0:03](https://archive.example/?v=testchan%2Fvid1&t=3)", "", '> "the second citation"', "— [source @ 0:09](https://archive.example/?v=testchan%2Fvid1&t=9)", "", ].join("\n"), ); // One CACHED SOURCE WINDOW, so the bench and a --skip-fetch build have real // material without a network. It covers exactly the window c01 would fetch // (start-3 to end+3 = 0.00-9.00), and it is tone / silence / tone / silence / // tone with the silences centred on 3.0s and 6.0s -- the two cut points -- so // snapping has an exact answer instead of a plausible one. mkdirSync(path.join(REPORT, "out", "clips-raw"), { recursive: true }); ff([ "-f", "lavfi", "-i", "color=c=darkgreen:size=320x180:rate=15:duration=9", "-f", "lavfi", "-i", "sine=frequency=440:duration=9,volume=enable='between(t,2.9,3.1)+between(t,5.9,6.1)':volume=0", "-map", "0:v", "-map", "1:a", "-t", "9", // Past listMedia's 256 KB floor, so the mix picker can see it. See clip(). ...BULK, "-pix_fmt", "yuv420p", "-c:a", "aac", "-ar", "48000", "-ac", "2", path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), ]); // One report project ships a DELIVERABLE, so "a report video's finished file // appears in the mix picker" is testable without depending on the build specs // having run first. The others deliberately have none: a project whose only // media is out/clips-raw has nothing to offer a picker, because those are // intermediates and are excluded by name. mkdirSync(path.join(reports, "no-origin-fixture", "out"), { recursive: true }); for (const dir of [BENCH, BUILD, ONSCREEN, ONSCREEN_BUILD, ONSCREEN_POSTS, ONSCREEN_FEED]) { mkdirSync(path.join(dir, "out", "clips-raw"), { recursive: true }); copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(dir, "out", "clips-raw", "vid1_0.00-9.00.mp4"), ); } // TWO MORE cached windows for vid1, and they are the regression rather than // decoration. // // clips-raw is keyed by VIDEO and a report cites the same stream more than // once, so the directory holds a file per clip. c04 is 15.00-18.00 and these // give it three candidates: the 9-second file (no overlap at all), a 6-second // one that overlaps by a second without containing it, and the 4-second one // that actually holds it. Sorted by WIDTH -- which is what the bench did -- // the answer is the first, and the player seeks 15 s into a file that ends at // 9. The content is the same nine seconds of tone in all three: what is being // tested is which NAME is chosen. for (const name of ["vid1_10.00-16.00.mp4", "vid1_14.50-18.50.mp4"]) { copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(BENCH, "out", "clips-raw", name), ); } // walk-fixture's cache: a file holding w01, one holding w03 and one holding // w04, and NOTHING touching w02 -- 9.20-11.20 falls in the gap between // 0.00-9.00 and 11.50-15.50, so it has no picture at all rather than half of // one. The content is the same nine seconds of tone in all three; what is being // fixed here is which windows are held, not what is in them. mkdirSync(path.join(WALK, "out", "clips-raw"), { recursive: true }); for (const name of ["vid1_0.00-9.00.mp4", "vid1_11.50-15.50.mp4", "vid1_14.50-18.50.mp4"]) { copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(WALK, "out", "clips-raw", name), ); } copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(reports, "no-origin-fixture", "out", "no-origin-fixture.mp4"), ); // -- the long-form pass: sources, revisions, notes, exports ------------------- // // longform-fixture READ-ONLY. c02 cites short1 (cues end 6 s) to 12 s -> // one clip-cue-gap; its citeUrl points elsewhere -> a // cite override; a legacy .bak beside the manifest; a // hand-written chapters.ffmeta so `export` has the // build's offsets without a build; a build-notes.md // longform-edit-fixture the snapshot / window / diff specs WRITE here // dash-fixture the dashboard spec BUILDS this one (offline, ~3 s) const LONG = writeProject( "longform-fixture", { ...manifest("longform-fixture", "The Longform Fixture", { siteOrigin: "https://archive.example", coverage: "A long provenance note, well over one hundred and twenty characters, so the project page has to render it as a paragraph under Notes rather than in the scalar table." }, [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because", chapter: "First chapter" }, { type: "clip", id: "c02", video: "short1", start: 2.0, end: 12.0, cite: 2, section: 0, lock: true, quote: "stops", chapter: "Second chapter", citeUrl: "https://mirror.example/watch?v=other&t=2" }, ]), }, ); // The legacy shape, as employee-count and ferret-rescue have it: c02 was // shorter when this copy was taken, so the diff must report ONE window change. { const cur = JSON.parse(readFileSync(path.join(LONG, "video.manifest.json"), "utf8")); cur.timeline[1].end = 9.0; writeFileSync(path.join(LONG, "video.manifest.json.bak"), JSON.stringify(cur, null, 2) + "\n"); } writeFileSync(path.join(LONG, "build-notes.md"), "# Build notes\n\nThe second clip outruns its transcript on purpose.\n"); mkdirSync(path.join(LONG, "out"), { recursive: true }); writeFileSync( path.join(LONG, "out", "chapters.ffmeta"), [";FFMETADATA1", "", "[CHAPTER]", "TIMEBASE=1/1000", "START=0", "END=3400", "title=First chapter", "", "[CHAPTER]", "TIMEBASE=1/1000", "START=3400", "END=13800", "title=Second chapter", ""].join("\n"), ); writeProject( "longform-edit-fixture", manifest("longform-edit-fixture", "The Longform Edit Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another" }, ]), ); const DASH = writeProject( "dash-fixture", manifest("dash-fixture", "The Dashboard Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, ]), ); mkdirSync(path.join(DASH, "out", "clips-raw"), { recursive: true }); copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(DASH, "out", "clips-raw", "vid1_0.00-9.00.mp4"), ); // -- the media root (release 17) ------------------------------------------------ // // storage.spec.ts moves storage-fixture's out/ to a media root and back, builds // through the link, and unplugs the root. Its CLI is given UMTOOL_MEDIA_DIR = // this directory; the APP is not (every other spec's out/ stays a directory). // A SIBLING of the fixture, not inside it: REPORTS_ROOT is the fixture root, and // a media root inside the tree it mirrors is refused. Reset here, every run. // storage-fixture dash-fixture's shape: a cached window, buildable offline // storage-fresh-fixture no out/ at all: the first writer makes the link const MEDIA = `${dest}-media`; rmSync(MEDIA, { recursive: true, force: true }); mkdirSync(MEDIA, { recursive: true }); for (const slug of ["storage-fixture", "storage-fresh-fixture"]) { const dir = writeProject( slug, manifest(slug, "The Storage Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, ]), ); if (slug !== "storage-fixture") continue; mkdirSync(path.join(dir, "out", "clips-raw"), { recursive: true }); copyFileSync( path.join(REPORT, "out", "clips-raw", "vid1_0.00-9.00.mp4"), path.join(dir, "out", "clips-raw", "vid1_0.00-9.00.mp4"), ); } // deliverables.spec.ts (release 17, slice U2): a report's deliverables behind // the per-project switch. Its CLI moves clips/ and share-first/ to the media // root above; the app (no UMTOOL_MEDIA_DIR) then cuts THROUGH the clips/ link, // and is refused a new batch; the CLI's share batch makes share-second as a // link and reads share-first through its own; the app moves them back. // d01 d02 confirmed, cached in the window, no file yet -> the app's cut // d03 confirmed, has a file, and shipped in share-first -> excluded const DELIVERABLES = writeProject( "deliverables-fixture", manifest("deliverables-fixture", "The Deliverables Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "d01", video: "vid6", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, verdict: "confirmed", date: "2025-03-01", title: "A Fixture Stream", quote: "The first clip is confirmed." }, { type: "clip", id: "d02", video: "vid6", start: 6.0, end: 9.0, cite: 6, section: 0, lock: true, verdict: "confirmed", date: "2025-03-02", title: "A Fixture Stream", quote: "So is the second." }, { type: "clip", id: "d03", video: "vid6", start: 0.0, end: 3.0, cite: 0, section: 0, lock: true, verdict: "confirmed", date: "2025-03-03", title: "A Fixture Stream", quote: "The deliver fixture opens." }, ]), ); mkdirSync(path.join(DELIVERABLES, "out", "clips-raw"), { recursive: true }); copyFileSync( path.join(DELIVER, "out", "clips-raw", "vid6_0.00-24.00.mp4"), path.join(DELIVERABLES, "out", "clips-raw", "vid6_0.00-24.00.mp4"), ); mkdirSync(path.join(DELIVERABLES, "clips"), { recursive: true }); copyFileSync(path.join(DELIVER, "clips", "b01.mp4"), path.join(DELIVERABLES, "clips", "d03.mp4")); mkdirSync(path.join(DELIVERABLES, "share-first", "orig", "D"), { recursive: true }); copyFileSync( path.join(DELIVER, "clips", "b01.mp4"), path.join(DELIVERABLES, "share-first", "orig", "D", "d03_2025-03-03_A-Fixture-Stream.mp4"), ); writeFileSync( path.join(DELIVERABLES, "share-first", "LIST.md"), "# deliverables-fixture — share batch `first`\n\n- **d03_2025-03-03_A-Fixture-Stream.mp4**\n", ); // -- THE TAKES FIXTURE -------------------------------------------------------- // // Alternative renders of one part of a cut, as another agent leaves them in // takes//. Its own project because the spec WRITES takes/verdicts.json. // // finale ref (reference, order 1), slow (similar, 2), hard-cut (different, // 3, its preview not rendered yet -- listed, with no player) // opening intro-a (similar, 5) // bad-kind a take.json whose kind is not one of the three -> skipped, with // the reason on the page // current/ a work directory with no take.json -> not a take, not listed // // report-fixture has no takes/ at all, which is the empty state. const TAKES = writeProject( "takes-fixture", manifest("takes-fixture", "The Takes Fixture", { siteOrigin: "https://archive.example" }, [ { type: "clip", id: "c01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "q" }, ]), ); const take = (id, doc, { hz = null } = {}) => { const dir = path.join(TAKES, "takes", id); mkdirSync(dir, { recursive: true }); writeFileSync(path.join(dir, "take.json"), JSON.stringify({ id, preview: "preview.mp4", ...doc }, null, 2)); if (hz) { ff([ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=15:duration=2", "-f", "lavfi", "-i", `sine=frequency=${hz}:duration=2`, "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", "-movflags", "+faststart", path.join(dir, "preview.mp4"), ]); } }; take("ref", { group: "finale", order: 1, label: "As built", kind: "reference", summary: "The current ending.", changes: [], seconds: 2 }, { hz: 330 }); take("slow", { group: "finale", order: 2, label: "Slow burn", kind: "similar", summary: "Same words; slower beat.", changes: ["beat 1.35 s (was 1.05)", "dip 1.4 s (was 0.6)"], seconds: 2, builtAt: "2026-10-06T23:10:00Z" }, { hz: 440 }); take("hard-cut", { group: "finale", order: 3, label: "Hard cut", kind: "different", summary: "No dip at all.", changes: ["no fade"] }); take("intro-a", { group: "opening", order: 5, label: "Cold open", kind: "similar", summary: "Starts on the quote.", seconds: 2 }, { hz: 550 }); take("bad-kind", { group: "finale", order: 4, label: "Bad", kind: "maybe" }); mkdirSync(path.join(TAKES, "takes", "current", "out"), { recursive: true }); // -- THE VIDEO-NOTES AND TIMELINE FIXTURES ------------------------------------ // // video-notes-fixture is a GENERATED manifest (`generatedBy`) with a built cut // and its schedule, and a take with its own: timed notes resolve against them, // and every edit made to it leaves an `edit` note (video-notes.spec.ts). // timeline-fixture is hand-written, with a teaser, three clips, a post riding // on the second and the deck on: the structural edits' subject // (timeline-edit.spec.ts). Both are written by specs; nothing else reads them. const twoSeconds = (file, hz) => ff([ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=15:duration=2", "-f", "lavfi", "-i", `sine=frequency=${hz}:duration=2`, "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", "-movflags", "+faststart", file, ]); const NOTES_SCHEDULE = { version: 1, kind: "deck", estimated: false, fps: 15, transition: 0, total: 2, segments: [ { id: "k1", type: "card", start: 0, duration: 0.5, end: 0.5, title: "Opening" }, { id: "n01", type: "clip", start: 0.5, duration: 0.7, end: 1.2, title: "The first claim" }, { id: "n02", type: "clip", start: 1.2, duration: 0.8, end: 2, title: "The second claim" }, ], }; const VNOTES = writeProject("video-notes-fixture", { ...manifest("video-notes-fixture", "The Video Notes Fixture", { siteOrigin: "https://archive.example" }, [ { type: "card", id: "k1", heading: "Opening" }, { type: "clip", id: "n01", video: "vid1", start: 0, end: 3, cite: 0, section: 0, lock: true, quote: "This is a complete sentence." }, { type: "clip", id: "n02", video: "vid1", start: 9, end: 12, cite: 9, section: 0, lock: true, quote: "Another whole sentence entirely." }, ]), generatedBy: "polemics/video/make-videos.py", }); { // The deck on: the On-screen section shows the built cut (and its timed // notes) only under it. const m = JSON.parse(readFileSync(path.join(VNOTES, "video.manifest.json"), "utf8")); m.render.chrome = { engine: "hyperframes", layout: "deck" }; writeFileSync(path.join(VNOTES, "video.manifest.json"), JSON.stringify(m, null, 2) + "\n"); } mkdirSync(path.join(VNOTES, "out", "sourced"), { recursive: true }); twoSeconds(path.join(VNOTES, "out", "video-notes-fixture.mp4"), 300); writeFileSync(path.join(VNOTES, "out", "sourced", "schedule.json"), JSON.stringify(NOTES_SCHEDULE, null, 2)); { const dir = path.join(VNOTES, "takes", "alt"); mkdirSync(path.join(dir, "out", "sourced"), { recursive: true }); writeFileSync( path.join(dir, "take.json"), JSON.stringify({ id: "alt", group: "cut", order: 1, label: "Alternate", kind: "similar", summary: "Tighter.", preview: "preview.mp4", seconds: 2 }, null, 2), ); twoSeconds(path.join(dir, "preview.mp4"), 360); writeFileSync(path.join(dir, "out", "sourced", "schedule.json"), JSON.stringify(NOTES_SCHEDULE, null, 2)); } writeProject("timeline-fixture", { ...manifest("timeline-fixture", "The Timeline Fixture", { siteOrigin: "https://archive.example" }, [ { type: "teaser", id: "t1", lines: ["THE PROMISE"] }, { type: "clip", id: "a01", video: "vid1", start: 0, end: 3, cite: 0, section: 1, sectionEnter: true, lock: true, quote: "one" }, { type: "clip", id: "a02", video: "vid1", start: 9, end: 12, cite: 9, section: 1, lock: true, quote: "two" }, { type: "clip", id: "a03", video: "vid1", start: 12, end: 15, cite: 12, section: 2, sectionEnter: true, lock: true, quote: "three" }, ]), posts: [{ id: "p1", platform: "x", author: "Someone", handle: "@someone", date: "2024-01-02", text: "A post.", url: "https://x.com/someone/status/1", attachTo: "a02" }], }); const { sites: SITES } = makeSitesFixture({ dest, reports, channels: CHANNELS }); console.log(`fixture at ${dest}`); console.log(` SITES_DIR=${SITES}`); console.log(` video-notes-fixture (generated, built, schedule + take alt), timeline-fixture (teaser, a01-a03, post p1)`); if (planned) console.log(` planned clip (used in a build): ${planned}`); console.log(` videos/: alpha (4 cuts, 3 variants), beta (2 cuts), deck (1 cut, 2 variants)`); console.log(` deck: 1 spec error, 1 stale recipe, 1 unjudged variant, 1 judged one`); console.log(` loudness: deck wide.mp4 vs variants/wide-quiet.mp4, 10 dB apart`); console.log(` thumbs: alpha-c accepted, alpha-b free, alpha-d clashes on ${slot(1)}`); console.log(` faces: 8 distinct corners over 4 covers, from ${episodes.length ? "real episodes" : "synthetic ids"}`); console.log(` detector: ${existsSync(path.join(dest, "facedet")) ? "facedet symlinked" : "NO facedet -- detect routes will 503"}`); console.log(` plans: alpha (4 notes), alpha-v2 (+bass), alpha-body.json (depth 3), overlays.json (not a plan)`); console.log(` trim set: mk-hooks (10s stem, 2 hooks)`); console.log(` flagged source: ${flagged ? flagged.video : "none — no asr/"}`); console.log(` SONG_CODE_DIR=${path.join(dest, "code")}`); console.log(` SONG_DIR=${path.join(dest, "data")}`); console.log(` SONG_REPORTS_DIR=${reports}`); console.log(` UMTOOL_CACHE_DIR=${path.join(dest, "cache")} (removed with the fixture; never ~/.cache)`); console.log(` storage spec media root: ${MEDIA} (UMTOOL_MEDIA_DIR on its CLI only; reset here)`); console.log(` deliverables-fixture: clips/ (d03) + share-first/, d01/d02 cuttable (deliverables.spec)`); console.log(` YTDLP_BIN=${path.join(BIN, "yt-dlp")} QRENCODE_BIN=${path.join(BIN, "qrencode")} HYPERFRAMES_BIN=${path.join(BIN, "hyperframes")}`); console.log(` CHANNELS_DIR=${CHANNELS} (testchan/vid1 punctuated, vid2 not; vid3/vid4/vid5 for the editor fetch)`); console.log(` projects: report-fixture (4 clips, 1 mid-sentence), no-origin-fixture,`); console.log(` localhost-fixture, bike-fixture (sweep), find/ (shadowed),`); console.log(` deep/nested/solo-fixture (collapse case), bench-fixture (writable),`); console.log(` walk-fixture (read-only: w01/w04 walkable, w02 unfetched, w03 judged),`); console.log(` editor-fetch-{,many-,reuse-}fixture (nothing cached — the editor fetch's subjects),`); console.log(` longform-fixture (cue gap, legacy .bak, ffmeta), longform-edit-fixture, dash-fixture`); console.log(` deliver-fixture (writable: a01/a02 to cut, a03 unfetched, b01 shared, b02 incorrect, b03 unjudged)`); console.log(` onscreen-fixture (writable, deck on, unbuilt), onscreen-build-fixture (built with the deck)`); console.log(` onscreen-posts-fixture (writable, deck on, three posts, unbuilt)`); console.log(` onscreen-feed-fixture (writable, posts feed, built by the spec)`); console.log(` deliver-stop-fixture (writable: six confirmed clips to cut, for Stop and resume)`); console.log(` takes-fixture (writable: takes/ with 4 takes over 2 groups, 1 skipped)`); console.log(` ${taken} candidate files copied, 2 mix tracks synthesised`);