From ac946ef11bb08461e88eb5ab8390d62d89db7dff Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 17 Sep 2026 17:03:40 -0700 Subject: [PATCH] test(producer): add long-form render fixture generator and manual gate notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §8 fixtures every phase's manual gate renders against: one 300 s video spanning the whole composition, and the same source cut into 60 clips. Committed as a generator rather than fixtures because the source alone is ~600 MB. Co-Authored-By: Claude Opus 5 (1M context) --- packages/producer/tests/long-form/README.md | 45 +++++++++++++++++++++ packages/producer/tests/long-form/gen.mjs | 44 ++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 packages/producer/tests/long-form/README.md create mode 100644 packages/producer/tests/long-form/gen.mjs diff --git a/packages/producer/tests/long-form/README.md b/packages/producer/tests/long-form/README.md new file mode 100644 index 0000000000..94e5764318 --- /dev/null +++ b/packages/producer/tests/long-form/README.md @@ -0,0 +1,45 @@ +# Long-form render fixtures + +Manual gates for `plans/long-form-render/`. Not part of the automated lanes (they render for minutes and need ~600 MB of source). + +## Generate the source once + +```sh +mkdir -p /tmp/hf-longform/assets && cd /tmp/hf-longform +ffmpeg -y -f lavfi -i "testsrc2=size=1920x1080:rate=30" -f lavfi -i "sine=frequency=440:sample_rate=48000" \ + -t 300 -c:v libx264 -preset ultrafast -crf 23 -g 30 -pix_fmt yuv420p -c:a aac -b:a 96k -movflags +faststart assets/long.mp4 +node /packages/producer/tests/long-form/gen.mjs +``` + +For the 40-minute soak fixture use `-t 2400` and `DUR=2400 node .../gen.mjs`. + +## Run a gate against the worktree build + +```sh +cd && bun run build +cd /tmp/hf-longform +node /packages/cli/dist/cli.js render a-single --fps 30 -w 1 --quality draft -o a-single/renders/out.mp4 +``` + +Check duration: `ffprobe -v error -show_entries format=duration -of csv=p=0 a-single/renders/out.mp4` → `300.000000`. + +Never run two of these concurrently on a dev Mac — the fleet limit is why the gates are sequential. + +## Gates by phase + +| Phase | Command | Expect | +| ----- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| 0 | `render a-single --fps 30 -w 1` (stock env) | log `streaming-encode gate {"enabled":true,"reason":"single_worker",...}`; output 300.000 s | +| 1 | `render a-single --fps 30 -w 4` (stock env) | log `Parallel screenshot capture will stream to the encoder`; work dir < 2 GB; output 300.000 s | +| 2a | `HF_SEGMENTED_CAPTURE=true render a-single --fps 30 -w 1` | `segment_*.mp4` in work dir; output 300.000 s; PSNR ≥ 45 dB vs the Phase 0 output | +| 2b | kill the 2a render at ~40 %, rerun with `--resume` | log `resuming: N segments complete`; output byte-identical to an uninterrupted run | +| 2c | 2a with `HF_SEGMENT_BROWSER_RECYCLE=1` | one `[Render] segment browser recycled` line per segment; output unchanged | +| 2d | 2a with `-w 4` | four `segment worker` lines; output 300.000 s | + +The Phase 0 mutation check is the same render with `PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true`: the gate line must flip to `"enabled":false,"reason":"duration_cap"` and the render must fail at the disk preflight on a host without ~75 GB free. + +PSNR between two renders: + +```sh +ffmpeg -i a.mp4 -i b.mp4 -lavfi "[0:v][1:v]psnr" -f null - 2>&1 | grep -o 'average:[0-9.inf]*' +``` diff --git a/packages/producer/tests/long-form/gen.mjs b/packages/producer/tests/long-form/gen.mjs new file mode 100644 index 0000000000..aef143a1c8 --- /dev/null +++ b/packages/producer/tests/long-form/gen.mjs @@ -0,0 +1,44 @@ +// Long-form render fixtures (spec §8). Run from a scratch directory that +// already contains assets/long.mp4 (see README.md for the ffmpeg command). +// DUR=300 node gen.mjs # 5-minute fixtures (default) +// DUR=2400 node gen.mjs # 40-minute soak fixtures +import { mkdirSync, writeFileSync, linkSync, existsSync } from "node:fs"; + +const W = 1920; +const H = 1080; +const DUR = Number(process.env.DUR ?? "300"); +if (!Number.isFinite(DUR) || DUR <= 0) + throw new Error(`DUR must be a positive number, got ${process.env.DUR}`); +if (!existsSync("assets/long.mp4")) throw new Error("assets/long.mp4 missing; see README.md"); + +const comp = ( + body, +) => ` +
+${body} +
overlay
+
`; + +const single = () => + comp( + ``, + ); + +const clips = (n = 60) => + comp( + Array.from({ length: n }, (_, i) => { + const d = DUR / n; + return ``; + }).join("\n"), + ); + +for (const [name, html] of Object.entries({ "a-single": single(), "c-clips60": clips() })) { + mkdirSync(`${name}/assets`, { recursive: true }); + writeFileSync(`${name}/index.html`, html); + if (!existsSync(`${name}/assets/long.mp4`)) linkSync("assets/long.mp4", `${name}/assets/long.mp4`); + writeFileSync( + `${name}/hyperframes.json`, + JSON.stringify({ name, fps: 30, width: W, height: H }, null, 2), + ); +} +console.log(`generated a-single c-clips60 (DUR=${DUR}s)`);