Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 735984720c1a253ad8f8a262b05ffcd2ab11e7cb
parent 657789f4f24952d148eebdea4bb0df934cd7e1c2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 21 Sep 2026 01:42:20 -0400

deliver: a fixture that measures the cut, and the docs for the panel

deliver-fixture is its own project on its own source, 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 one number over the whole cut
hides the section where a clip was thrown out.

The window is generated with keyframes every three seconds, the shape a fetch
with --force-keyframes-at-cuts leaves. a01 starts on one and a02 does not, and
MEASURED, neither copy survives: the aac frames do not end where the keyframe
does, so the copy comes out 0.14 s long. That is the whole argument for the
cut probing its own output instead of reading the window's keyframes, and both
clips still come out to the frame -- which is what the spec asserts, with
ffprobe, rather than asserting a file exists.

The fixture's apply-manifest.py and build.py are two-line stand-ins, and the
docs say why that is the honest limit: the real pair are the REPORT's own,
hand-written, in an unversioned reports directory, and differ per report. What
is under test is that the panel finds them, runs them in order, refuses the
first over a half-walked cut, and shows their output verbatim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mumtool/docs/clip-bench.md | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aumtool/e2e/deliver.spec.ts | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/e2e/fixtures/make-fixture.mjs | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 446 insertions(+), 0 deletions(-)

diff --git a/umtool/docs/clip-bench.md b/umtool/docs/clip-bench.md @@ -351,6 +351,70 @@ the CLI's formatting, tmp+rename under a lock, an mtime token). A stale token is **409 with both values**, never a silent overwrite: the other writer is usually somebody's judgement. +## Deliver — the half that starts when the walk ends + +The bench answers one question per clip. What a report owes its readers after +that is a different list, and it lives on the **project page** as `Deliver` +(`components/projects/DeliverSection.tsx`, `/api/report/deliver`): + +| | | +|---|---| +| **counts by section** | folded on the id's letter prefix, because `69 of 163 confirmed` says nothing about the section where eleven of nineteen clips were thrown out — and that is the section whose argument has to change. Section HEADINGS are read out of the report's own `content.py`, nth heading to nth letter. | +| **cut N confirmed clips from cache** | one job, **one step per clip**, so `k of n` and Stop are the job runner's own (`stepIndex`, and a cancel that kills the step's process group). | +| **not fetched** | a confirmed clip with nothing cached that holds it. Listed, never downloaded here — the managed fetch is the editor's, on the clip page. | +| **build share batch** | `share-<name>/{orig,std,small}/<Section>/<id>_<date>_<title>.mp4` + `LIST.md`. | +| **apply rulings** | runs the project's own `apply-manifest.py`, then `umtool corrections`. | +| **rebuild reports** | runs the project's own `build.py`, once per `content*.py`. | + +**A confirmed clip's frames are already on this disk.** Nobody can judge a clip +until its window is cached, so `clips/<id>.mp4` — what the written report's +players read — is a cut, not a download. `lib/report/cut.mjs` asks the project +cache for the tightest window containing the clip and cuts at +`clip.start − window.from`, through build-video's own `cutArgs()`: one spelling +of the arithmetic, shared with the render's segment pass. + +**The cut is measured, not predicted.** It stream-copies first and then probes +the result; more than 50 ms off the length asked for and it re-encodes. A copy +can only begin on a keyframe, and even a keyframe-aligned copy comes out long +when the aac frames do not end there (0.14 s, measured on the fixture) — +predicting that from the window's keyframes would have been wrong in exactly +the cases that matter. + +**The two share profiles are `lib/report/encode.mjs`, and only there.** They +came off `~/reports/elfpire-eva/share-report-clips/reencode.py`, which is where +the first batch was actually encoded; ported rather than spawned, because a +python file beside one project's deliverables is not a profile the next project +can reach. + +**Exclusions are the folders, not a list.** The next batch skips every id an +existing `share-*/LIST.md` already shipped (read back from the file NAMES it +lists, plus the `<!-- shared-ids: … -->` marker this writes) and every clip +ruled `incorrect`. A `shared.json` would have been a second record to drift. + +**Nothing rewrites prose.** `apply-manifest.py` prints the lines of +`content*.py` that cite each clip the walk threw out, and the panel shows the +same grep itself. What a wrong clip does to an argument is a judgement about +the argument. + +**Apply is refused while the walk is unfinished** unless `apply partial` is +ticked: the script syncs `clips.json` from the manifest and deletes the mp4 of +every clip whose window moved, so running it over a half-walked cut bakes +"nobody has looked at this yet" into the deliverable as though it were a +verdict. + +**One job at a time, machine-wide in this process** (`lib/jobs.ts`): a bench +fetch and a Deliver cut cannot run together, and the second one gets a 409 +naming the first. That is not a limitation to route around — both write the +same `clips/` directory. + +**What the fixture cannot cover.** `apply-manifest.py` and `build.py` are the +REPORT's own, hand-written, living in an unversioned `~/reports/<project>/` +directory and differing per report. `e2e/deliver.spec.ts` runs two-line +stand-ins: what is under test is that the panel finds them, runs them in order, +refuses the first over a half-walked cut and shows their output verbatim — +never what they do. A project whose scripts are absent gets the buttons hidden +or a 400, not a guess. + ## Discovered by getting it wrong once **The page and the inbox must answer the same question the same way.** `umtool diff --git a/umtool/e2e/deliver.spec.ts b/umtool/e2e/deliver.spec.ts @@ -0,0 +1,211 @@ +import { test, expect } from "@playwright/test"; +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// --------------------------------------------------------------------------- +// DELIVER: what happens after the last clip is judged. +// +// The bench's own specs cover judging. This covers the other half — cutting the +// confirmed clips out of the cache, packaging a batch, and running the report's +// own fold-back scripts — and its strongest assertions are on DISK, because +// every one of these produces a file somebody else will open. +// +// The clips are MEASURED, not merely present. A cut that lands on the wrong +// keyframe still writes an mp4; what makes it wrong is that it is not the +// seconds that were asked for, and only ffprobe can say so. +// +// Nothing here reaches the network: the fixture's window was generated by +// ffmpeg, the cut reads it, and the panel has no fetch of its own by design. +// --------------------------------------------------------------------------- + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURE = path.join(HERE, "..", ".e2e-song"); +const PROJECT = "reports/deliver-fixture"; +const DIR = path.join(FIXTURE, "reports", "deliver-fixture"); + +const seconds = (file: string): number => + Number( + execFileSync("ffprobe", [ + "-v", "error", "-show_entries", "format=duration", + "-of", "default=nw=1:nk=1", file, + ]).toString().trim(), + ); + +/** The job the panel started, polled to a terminal state. */ +async function waitForJob(page: import("@playwright/test").Page, timeout = 120_000) { + await expect(page.locator("[data-deliver-job]")).toHaveAttribute( + "data-deliver-state", + /done|failed/, + { timeout }, + ); + const state = await page.locator("[data-deliver-job]").getAttribute("data-deliver-state"); + const log = (await page.locator("[data-deliver-log]").textContent()) ?? ""; + return { state, log }; +} + +test("the deliver panel counts verdicts and folds them by id prefix", async ({ page }) => { + await page.goto(`/browse/${PROJECT}`); + const panel = page.locator("[data-deliver]"); + await expect(panel).toBeVisible(); + + // 4 confirmed (a01 a02 a03 b01), 1 incorrect (b02), 1 unjudged (b03). + await expect(panel).toHaveAttribute("data-deliver-confirmed", "4"); + await expect(panel).toHaveAttribute("data-deliver-incorrect", "1"); + await expect(panel).toHaveAttribute("data-deliver-unreviewed", "1"); + + // TWO SECTIONS, folded on the letter. The whole reason this is not one + // number: section B is where a clip was thrown out. + const a = panel.locator('[data-section="A"]'); + const b = panel.locator('[data-section="B"]'); + await expect(a).toHaveAttribute("data-section-confirmed", "3"); + await expect(a).toHaveAttribute("data-section-incorrect", "0"); + await expect(b).toHaveAttribute("data-section-confirmed", "1"); + await expect(b).toHaveAttribute("data-section-incorrect", "1"); + await expect(b).toHaveAttribute("data-section-unreviewed", "1"); + // And the heading came out of the report's own prose, not out of the id. + await expect(a).toContainText("The first fixture section"); + + // b01 is confirmed AND already has a file, so it is not waiting on a cut; + // a03 has nothing cached, so it is not a cut either. Two left. + await expect(panel).toHaveAttribute("data-deliver-need-cut", "2"); +}); + +test("a confirmed clip with no cached window is listed as not fetched, not cut", async ({ + page, +}) => { + await page.goto(`/browse/${PROJECT}`); + const notFetched = page.locator("[data-not-fetched]"); + await expect(notFetched).toBeVisible(); + // a03's window is 30–33 s and the cache holds 0–24. Nothing here downloads + // it: the link goes to the bench, where the editor fetch lives. + await expect(notFetched.locator('[data-not-fetched-id="a03"]')).toBeVisible(); + await expect(notFetched.locator('[data-not-fetched-id="a03"]')).toHaveAttribute( + "href", + `/browse/${PROJECT}/clip/a03`, + ); + // And it is NOT in the cut list, which is the distinction the two headings + // exist to make. + await expect(page.locator('[data-need-cut] [data-need-cut-id="a03"]')).toHaveCount(0); + expect(existsSync(path.join(DIR, "clips", "a03.mp4"))).toBe(false); +}); + +test("cutting from cache writes clips/<id>.mp4 of exactly the clip's length", async ({ + page, +}) => { + test.setTimeout(180_000); + await page.goto(`/browse/${PROJECT}`); + await page.locator('[data-action="deliver-cut"]').click(); + + // `k of n` is the JOB's count -- one step per clip -- so the readout and the + // work cannot disagree. + // Either "1 of 2" while it runs or "done · 2 steps" if it beat the first + // poll: what is asserted is that the readout is the job's step count. + await expect(page.locator("[data-deliver-progress]")).toContainText(/of 2|2 steps/); + const { state, log } = await waitForJob(page); + expect(state, log).toBe("done"); + expect(log).toContain("CUT-OK a01"); + expect(log).toContain("CUT-OK a02"); + + // THE MEASUREMENT. a01 starts on one of the window's keyframes and a02 does + // not; both must come out to the length the manifest asked for. + expect(seconds(path.join(DIR, "clips", "a01.mp4"))).toBeCloseTo(3.0, 1); + expect(seconds(path.join(DIR, "clips", "a02.mp4"))).toBeCloseTo(3.0, 1); + + // And the panel has nothing left to cut. + await page.reload(); + await expect(page.locator("[data-deliver]")).toHaveAttribute("data-deliver-need-cut", "0"); +}); + +test("the share batch skips every id an existing LIST.md already shipped", async ({ page }) => { + test.setTimeout(180_000); + await page.goto(`/browse/${PROJECT}`); + await page.locator("[data-batch-name]").fill("second"); + await page.locator('[data-action="deliver-share"]').click(); + const { state, log } = await waitForJob(page); + expect(state, log).toBe("done"); + + const root = path.join(DIR, "share-second"); + const folder = "A-The-first-fixture-section"; + for (const variant of ["orig", "std", "small"]) { + expect( + existsSync(path.join(root, variant, folder, "a01_2025-01-01_A-Fixture-Stream.mp4")), + `${variant}/a01`, + ).toBe(true); + expect( + existsSync(path.join(root, variant, folder, "a02_2025-01-02_A-Fixture-Stream.mp4")), + `${variant}/a02`, + ).toBe(true); + } + + // THE EXCLUSIONS. b01 went out in share-first, and the folders are the + // record: nothing was written down anywhere else. b02 was ruled incorrect. + const list = readFileSync(path.join(root, "LIST.md"), "utf8"); + expect(list).toContain("a01_2025-01-01"); + expect(list).not.toContain("b01_"); + expect(list).not.toContain("b02_"); + expect(list).toContain("1 already shared (b01)"); + expect(list).toContain("1 ruled incorrect (b02)"); + expect(existsSync(path.join(root, "orig", "B-The-second-fixture-section"))).toBe(false); + + // The std encode is the one people download, and it is EXACTLY 1280x720. + const size = execFileSync("ffprobe", [ + "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", + "-of", "csv=p=0", path.join(root, "std", folder, "a01_2025-01-01_A-Fixture-Stream.mp4"), + ]).toString().trim(); + expect(size).toBe("1280,720"); +}); + +test("apply rulings is refused while the walk is unfinished, and runs the report's own scripts when it is meant", async ({ + page, +}) => { + test.setTimeout(120_000); + await page.goto(`/browse/${PROJECT}`); + + // b03 has never been judged. apply-manifest.py deletes the mp4 of every clip + // whose window moved, so running it over a half-walked cut bakes "nobody has + // looked at this" into the deliverable as though it were a verdict. + await page.locator('[data-action="deliver-apply"]').click(); + await expect(page.locator("[data-deliver-error]")).toContainText("have not been judged"); + expect(existsSync(path.join(DIR, "applied.marker"))).toBe(false); + + // Ticked, it proceeds -- and the second step is `umtool corrections`, which + // is the same list the page shows, in the form the next sweep's prompt wants. + await page.locator('[data-action="apply-partial"]').check(); + await page.locator('[data-action="deliver-apply"]').click(); + const { state, log } = await waitForJob(page); + expect(state, log).toBe("done"); + expect(existsSync(path.join(DIR, "applied.marker"))).toBe(true); + // VERBATIM. The lines of prose that cite the clip the walk threw out are the + // whole point of running it, and the panel must not paraphrase them. + expect(log).toContain("content.py:6:"); + expect(log).toContain("the speaker is the guest, not the host"); +}); + +test("the panel shows the prose lines citing a clip the walk ruled incorrect", async ({ + page, +}) => { + await page.goto(`/browse/${PROJECT}`); + const cited = page.locator('[data-incorrect="b02"]'); + await expect(cited).toBeVisible(); + await expect(cited).toContainText("content.py:6"); + await expect(cited).toContainText("The claim rests on [clip:b02]"); +}); + +test("rebuild runs build.py once per content variant present", async ({ page }) => { + test.setTimeout(120_000); + await page.goto(`/browse/${PROJECT}`); + await page.locator('[data-action="deliver-rebuild"]').click(); + const { state, log } = await waitForJob(page); + expect(state, log).toBe("done"); + + // content.py -> the default stem; content_lawyer.py -> report-lawyer. Two + // steps, two files, and neither overwrote the other's. + expect(log).toContain("BUILD-OK content -> report.html"); + expect(log).toContain("BUILD-OK content_lawyer -> report-lawyer.html"); + expect(readFileSync(path.join(DIR, "report.html"), "utf8")).toContain("built from content"); + expect(readFileSync(path.join(DIR, "report-lawyer.html"), "utf8")).toContain( + "built from content_lawyer", + ); +}); diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs @@ -682,6 +682,19 @@ const CUES = { [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/<id>.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: [ @@ -953,6 +966,163 @@ writeProject( ]), ); +// -- 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("<!-- built from %s -->\\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. +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.", + "", + "## 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"), +]); + // -- STUB BINARIES, so a build is offline and deterministic -------------------- // // The pipeline shells out to yt-dlp for the availability preflight and for every @@ -1235,4 +1405,5 @@ console.log(` deep/nested/solo-fixture (collapse case), bench-fixture 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(` ${taken} candidate files copied, 2 mix tracks synthesised`);