Files
hyperframes/packages/producer/tests
James RussoandClaude Opus 4.7 21f5066832 feat(producer): enable webm in distributed mode via concat-copy (#951)
* feat(producer): enable webm in distributed mode via concat-copy

PR 8.2 of the WebM distributed-rendering plan (v1.5 backlog #1; see
DISTRIBUTED-RENDERING-PLAN.md §7.2). Wires libvpx-vp9 webm through the
distributed pipeline now that PR 8.1 proved concat-copy works.

Architectural decision: Path A (concat-copy) — based on PR 8.1's smoke
test result (9/9 tests pass for both yuv420p and yuva420p VP9 streams).
The simpler architecture wins; no re-encode in assemble, no encode-
parallelism loss.

Changes:

- plan.ts:
  - DistributedRenderConfig.format and PlanResult.format now include
    "webm" — type-level acceptance matches the runtime gate.
  - rejectUnsupportedDistributedFormat() no longer trips on webm. HDR
    mp4 remains the only refused configuration.
  - resolveEncoderTriple() returns libvpx-vp9-software + yuva420p +
    preset="good" for format="webm". yuva420p preserves alpha — the
    format's main reason for existing for web delivery.
  - codec= remains rejected for non-mp4 formats (mov is always ProRes
    4444; webm is always libvpx-vp9). The error message lists all four
    distributed-supported formats.
  - FormatNotSupportedInDistributedError docstring updated to reflect
    the new reality (only HDR is unsupported).

- freezePlan.ts: LockedRenderConfig.encoder gains "libvpx-vp9-software".
  Mirrors libx265-software / prores-software / png-sequence in shape;
  the chunk worker reads this discriminant to decide encode args.

- renderChunk.ts: drops the now-incorrect cast that excluded webm from
  buildSyntheticRenderJob's format input; tightens the preset-format
  cast to include webm.

- assemble.ts: docstring + comment updates. The mp4/mov concat-copy
  path is format-agnostic — webm uses the exact same code (applyFaststart
  is a no-op for webm via the existing chunkEncoder.ts gate;
  muxVideoWithAudio already routes webm to libopus audio).

- planFormatBanlist.test.ts: webm-rejection tests removed; replaced with
  "accepts webm" tests + a HDR+webm combo test that verifies HDR is the
  trip regardless of format.

- plan.test.ts: new describe block pins the webm wiring contract:
  format="webm" produces an encoder=libvpx-vp9-software /
  pixelFormat=yuva420p planDir with closedGop=true and gopSize=chunkSize.

- webm-concat-copy.test.ts (smoke): extended with a yuva420p variant
  that proves the alpha pixel format the distributed pipeline actually
  emits also round-trips through concat-copy. 9/9 tests pass locally.

§8 format support matrix in DISTRIBUTED-RENDERING-PLAN.md is intentionally
left unchanged at this PR — it flips to ✓ in PR 8.4 once the end-to-end
fixture (PR 8.3) is green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(producer): include webm in plan-time needsAlpha + strengthen alpha smoke

PR review feedback from Miguel and Vai on #951 caught a real bug:
`plan.ts`'s `needsAlpha` disjunction excluded `"webm"`, so the plan
stage froze `forceScreenshot: false` into the `LockedRenderConfig`
even though distributed webm uses `yuva420p`. Every chunk worker
captured opaque RGB via BeginFrame (which doesn't preserve alpha on
Linux headless-shell), and libvpx-vp9 encoded uniformly-opaque alpha
that the encoder then dropped — producing un-keyable webm.

Two changes:

1. **plan.ts**: include `"webm"` in `needsAlpha`. Matches the
   in-process renderer's logic at `renderOrchestrator.ts:1469`
   (`const needsAlpha = isWebm || isMov || isPngSequence`); the two
   sites must stay in sync since the distributed pipeline's PSNR
   regression compares against the in-process baseline.

2. **Smoke test (yuva420p describe)**: source frames now use a real
   alpha gradient (`geq=a='X*255/W'` on top of `testsrc2`) instead of
   `testsrc2 + format=rgba` which was uniformly opaque. The decode-
   pix_fmt assertion is dropped (ffprobe reports `yuv420p` for
   VP9-with-alpha because the alpha lives in a Matroska
   `BlockAdditional` sidecar) and replaced with two stronger checks:
   - `TAG:ALPHA_MODE=1` is present on the stream — proves the
     encoder was actually configured for alpha
   - alpha plane variance after `-c:v libvpx-vp9 -i ... -pix_fmt rgba
     -vf extractplanes=a,signalstats` — proves the alpha sub-stream
     round-trips through concat-copy with spatially-varying content,
     not uniform/dropped alpha
   - decode-test gate is now exit-code-only (was `exitCode || stderr`
     which would flake on chatty ffmpeg `-v error` builds emitting
     non-fatal DTS/container notes)

These checks would have caught the `needsAlpha` bug before review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(aws-lambda): widen narrow format types to include webm

CI on PR #951 was failing at typecheck/build because the producer's
`DistributedRenderConfig.format` widened to include webm in this PR
but the aws-lambda package's narrow `"mp4" | "mov" | "png-sequence"`
type literals in `events.ts`, `handler.ts`, and `validateConfig.ts`
hadn't kept up. `renderToLambda.ts:87` passed `config.format` (now
including webm) into a parameter typed against the narrow union,
producing TS2345.

This widening originally landed in PR #952 (test fixture PR) but
needs to be atomic with the producer's widening here to keep each
PR independently typecheck-clean.

Also refactor `formatExtension` from a switch dispatch to a
`Record<DistributedFormat, string>` lookup. Adding the webm case
tipped the switch's CRAP to the 30.0 fallow threshold; the lookup
table drops cyclomatic from 5 to 1 with the same compile-time
exhaustiveness guarantee (TS errors on missing entries when
`DistributedFormat` adds a new format). The runtime
`_exhaustive: never` throw was only protecting against a string
slipping past TS; `validateConfig.ts`'s `ALLOWED_FORMATS` already
gates untrusted input at the SDK boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:46:21 -04:00
..
2026-04-27 18:16:09 -04:00

Producer regression test fixtures

Each subdirectory under this folder is a regression fixture for the HTML-to-video pipeline. The harness at packages/producer/src/regression-harness.ts walks every subdirectory, runs the composition, and PSNR-compares the rendered output against a checked-in golden baseline.

Fixture layout

<fixture-name>/
├── meta.json           # name, tags, PSNR threshold, renderConfig
├── src/
│   ├── index.html      # composition entry point
│   └── assets/...      # any locally-referenced media
└── output/
    ├── compiled.html   # golden compiled HTML (validated as a snapshot)
    └── output.mp4      # golden rendered video

meta.json is validated by validateMetadata in src/regression-harness.ts. The required fields are:

  • name (string), description (string), tags (string[])
  • minPsnr (number, dB)
  • maxFrameFailures (integer)
  • minAudioCorrelation (0..1), maxAudioLagWindows (integer ≥1)
  • renderConfig.fps (integer like 30 or a rational string like "30000/1001")

Optional renderConfig fields:

  • format"mp4" (default) or "webm"
  • workers — integer ≥ 1
  • hdr — boolean (default false)
  • variables — JSON object of render-time variable overrides
  • chunkSize — integer ≥ 1 (used by --mode=distributed-simulated)
  • maxParallelChunks — integer ≥ 1 (used by --mode=distributed-simulated)

Generating / updating a baseline

Always inside Docker. Host Chrome / FFmpeg versions drift across distros, so a baseline captured on the host won't match the bytes CI renders.

# From the repo root.
docker build -t hyperframes-producer:test -f Dockerfile.test .

# Generate a baseline (single fixture):
bun run --cwd packages/producer docker:test:update <fixture-name>

# Generate all baselines (rarely needed):
bun run --cwd packages/producer docker:test:update

The --update flag writes output/compiled.html and output/output.mp4 from the current render. Without --update, the harness compares against those baselines.

Running the harness locally

# Run every fixture (parallel, in-process mode — the default).
bun run --cwd packages/producer docker:test

# Run a single fixture:
bun run --cwd packages/producer docker:test font-variant-numeric

# Run sequentially (lower memory):
bun run --cwd packages/producer docker:test -- --sequential

Harness modes

--mode=<value> chooses which render path the harness exercises:

Mode What it calls Use for
in-process (default) executeRenderJob Day-to-day baselines. This is the same path the hyperframes render CLI takes, and it is what produced every existing output/output.mp4.
distributed-simulated plan()renderChunk() × N → assemble() from @hyperframes/producer/distributed Validates the distributed pipeline against the in-process baseline. No Temporal or Lambda involvement — the controller and chunk worker are both this process.

--mode=distributed-simulated

bun run --cwd packages/producer docker:test -- --mode=distributed-simulated
bun run --cwd packages/producer docker:test font-variant-numeric -- --mode=distributed-simulated

The distributed pipeline cannot run every fixture. Fixtures that fail any of these gates are skipped with a clear log line (and counted as passing in the summary):

  • fps.den !== 1 — distributed mode is integer-fps only (no NTSC).
  • fps.num ∉ {24, 30, 60} — closed set per DistributedRenderConfig.
  • format === "webm"plan() refuses webm.
  • hdr === true — distributed mode is SDR-only at v1.

Both modes use the fixture's authored minPsnr as the per-test threshold — distributed must clear the same quality bar in-process clears against the same frozen baseline. (Internal contract: distributed vs in-process renders of the same fixture should clear 50 dB PSNR against each other within the same Docker image. Against the frozen committed baseline, neither mode reaches that consistently due to shared encoder/JPEG-capture jitter — that's why the fixture's authored threshold gates here, not the 50 dB contract value.) An absolute 10 dB pathology floor catches fully-black-output regressions when a fixture authors a permissive threshold. A distributed failure at the fixture's own threshold means the distributed pipeline has drifted — file an issue rather than relaxing the fixture.

--update is incompatible with --mode=distributed-simulated: the in-process renderer is the source of truth for baselines, and the distributed mode's job is to verify the contract against the same baseline.

Validating PR 4.1 (the harness mode itself)

The smallest fixtures (font-variant-numeric, many-cuts) are sufficient to verify the mode plumbing end to end:

docker build -t hyperframes-producer:test -f Dockerfile.test .

# In-process: existing behavior, unchanged.
bun run --cwd packages/producer docker:test font-variant-numeric
bun run --cwd packages/producer docker:test many-cuts

# Distributed-simulated: same baselines, distributed pipeline.
bun run --cwd packages/producer docker:test font-variant-numeric -- --mode=distributed-simulated
bun run --cwd packages/producer docker:test many-cuts -- --mode=distributed-simulated

Both modes must pass at each fixture's authored minPsnr against the existing baseline. If --mode=distributed-simulated fails where --mode=in-process passes, the distributed primitive has a regression — file an issue rather than relaxing the fixture's threshold.

Distributed-only fixtures

Fixtures under tests/distributed/<name>/ are authored specifically for the distributed pipeline. They follow the same meta.json schema as the top-level fixtures, but they always set chunkSize / maxParallelChunks so a plan() over the fixture produces N>1 chunks. Each fixture exercises one of:

  • per-format chunk-boundary correctness (mp4 H.264, mp4 H.265, ProRes, png-sequence)
  • per-adapter chunk-seam state preservation (GSAP, Anime.js, Three.js, Lottie, CSS, WAAPI)

Each distributed fixture covers one or more equivalence axes — see the meta.json description field for what a given fixture is locking in.

Fixture pattern (4.2 onward)

Each tests/distributed/<name>/ fixture has the same structure as a top-level fixture (meta.json + src/index.html + output/output.mp4). Differences worth knowing:

  • renderConfig.chunkSize is required — pick a value that yields N≥2 chunks for your fixture's frame count (e.g. 60 frames at chunkSize: 15 produces N=4). Without this the fixture renders in a single chunk and never exercises the seam.
  • The fixture's ID on the CLI is just <name> (no distributed/ prefix). bun run --cwd packages/producer docker:test mp4-h264-sdr works the same as for a top-level fixture.
  • The distributed tag is informational — it doesn't gate any tag-based filter today. Add it so the fixture is easy to find by tag.
  • The composition should stress state continuity across the chunk seams: an animation crossing a seam, a counter, a rotation. A fully-static composition would pass even if chunk-boundary state was broken.
  • Baselines must be generated inside Docker — see the section above. The baseline is rendered by the in-process renderer (the source of truth for golden output); --mode=distributed-simulated is validated against the same baseline.

Tags

Common tags values control which fixtures the default bun test invocation runs. --exclude-tags transparency (the default for bun test) skips webm/png-sequence alpha fixtures that need a working chrome-headless-shell alpha pipeline.