mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
test(producer): add long-form render fixture generator and manual gate notes
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
457687e77c
commit
ac946ef11b
@@ -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 <repo>/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 <repo> && bun run build
|
||||
cd /tmp/hf-longform
|
||||
node <repo>/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]*'
|
||||
```
|
||||
@@ -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,
|
||||
) => `<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#000}#root{position:relative;width:${W}px;height:${H}px;overflow:hidden;background:#000}video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}#badge{position:absolute;right:40px;top:40px;font:700 64px sans-serif;color:#fff;background:rgba(0,0,0,.5);padding:8px 24px}</style></head><body>
|
||||
<div id="root" data-composition-id="root" data-width="${W}" data-height="${H}" data-start="0" data-duration="${DUR}" data-no-timeline>
|
||||
${body}
|
||||
<div id="badge">overlay</div>
|
||||
</div></body></html>`;
|
||||
|
||||
const single = () =>
|
||||
comp(
|
||||
`<video class="clip" id="v0" src="assets/long.mp4" data-start="0" data-duration="${DUR}" data-media-start="0" data-has-audio="true"></video>`,
|
||||
);
|
||||
|
||||
const clips = (n = 60) =>
|
||||
comp(
|
||||
Array.from({ length: n }, (_, i) => {
|
||||
const d = DUR / n;
|
||||
return `<video class="clip" id="v${i}" src="assets/long.mp4" data-start="${(i * d).toFixed(3)}" data-duration="${d.toFixed(3)}" data-media-start="${(i * d).toFixed(3)}" data-has-audio="true"></video>`;
|
||||
}).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)`);
|
||||
Reference in New Issue
Block a user