Skip to content

feat(cli): measure a composition against a reference with compare --against - #3103

Open
miguel-heygen wants to merge 5 commits into
mainfrom
feat/compare-against-reference
Open

feat(cli): measure a composition against a reference with compare --against#3103
miguel-heygen wants to merge 5 commits into
mainfrom
feat/compare-against-reference

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What

Adds hyperframes compare --against <reference>: measure one composition against an external reference video or still, at up to 8 sampled times.

Each run emits three things:

  • a reference-over-replica contact sheet (--out),
  • a red/cyan deviation overlay per sampled time (<out>-overlay-NN.png): agreement grey, reference-only ink red, replica-only ink cyan,
  • numbers per time: ssim, meanAbsDiff, meanSignedDiff, and ink bounding-box deltas dw / dh / dcx / dcy / scale.

--fail-under <ssim> turns it into a gate (exit 1 when the worst sampled SSIM is below the threshold). Existing multi-variant compare behaviour is unchanged.

Why

Every gate we ship is self-referential. lint and check audit a composition against its own rules, so a scene that is inverted, mis-scaled, or missing its hero element passes both under a green check. When the brief comes with a reference artifact (a video being rebuilt or recut, an approved cut, a design still, the previous accepted render), nothing in the CLI compares the build to it, and agents end up hand-rolling a one-off ffmpeg measuring rig, or eyeballing composites.

How

  • packages/cli/src/utils/referenceDiff.ts: pure frame math (ink bounding box from a median-luma threshold, bounds deviation, mean absolute and mean signed difference, red/cyan overlay buffer, ffmpeg SSIM args + log parsing). No I/O, so it unit-tests without a browser or a built workspace.
  • packages/cli/src/capture/compareAgainstReference.ts: orchestration. Reference frames via ffmpeg, replica frames from one browser session seeked with AUDIT_SEEK_OPTIONS (the producer bridge, the same seek target render drives), replicas normalized to reference dimensions so the sheet, the overlay and SSIM all read the same pixels.
  • SSIM comes from ffmpeg's own ssim filter rather than a reimplementation. No new dependencies: sharp and ffmpeg are already required.
  • No default threshold, deliberately. The floor depends on content (below), so a guessed threshold either passes everything or fails every video-backed build.
  • Skills: /hyperframes-cli gains a "ground the gate in a reference" section, references/compare-and-batch.md gains the full contract with a read-the-numbers guide, /remotion-to-hyperframes points at it for mid-build iteration between full renders, and the /hyperframes router now says to read only the routed workflow's SKILL.md and none of its siblings.

The floor, measured rather than assumed

A replica is a live browser paint; a reference is a decoded compressed video. That decode gap is a floor no correction goes below, and it depends on content. Compared against their own renders:

Content Self-comparison SSIM
Flat graphics and type 0.998 to 0.999
Photographic video layers ~0.93 at --quality high, ~0.89 at draft

meanSignedDiff exists to make that legible: on the video-backed case, 1.6 of a 2.1% mean absolute difference is uniform bias, not structural error. The CLI prints it as diff 2.1% (bias +1.6%).

An earlier revision of this branch documented the video-backed floor as seek drift. That was wrong and is corrected here: probing which reference frame the live seek lands on shows it lands exactly on the right one (SSIM peaks at frame 210 for t=7 and falls off on both sides, 0.881 / 0.893 / 0.888). The residual is encode loss plus browser-versus-FFmpeg colour conversion.

Test plan

Unit tests cover the pure math and the argument parsing (ink bounds, bounds deviation, mean absolute and signed difference, overlay channel mapping, SSIM log parsing, --at / --fail-under validation and caps).

End to end, against real renders:

  • registry/examples/nyt-graph (graphics only) rendered at --quality high, compared against its own render at --at 0,3,7,11,14: SSIM 0.9987 to 0.9999, ink deltas all zero.

  • Same project against an unrelated video: SSIM 0.2892, meanAbsDiff 62.5%.

  • Gate exit codes verified directly: mismatch + --fail-under 0.9 exits 1, match + --fail-under 0.9 exits 0.

  • registry/examples/kinetic-type (video-backed) against its own high-quality render: 0.9353 / 0.9316 / 0.9301 with bias +1.6% / +1.1% / +1.0%, and against the draft render 0.8901 / 0.8930 / 0.8955. Repeat runs are bit-identical, so the number is stable.

  • Frame-landing probe (above) run against neighbouring reference frames 200 through 220.

  • packages/cli suite: 2501 passed, 2 failed. Both failures are the pre-existing transcribe.test.ts cases that also fail on main.

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

…gainst

lint and check only ever audit a composition against its own rules, so a
scene that renders nothing like the artifact it reproduces still passes
both. compare --against adds the outward-looking gate: per-time SSIM (via
ffmpeg's ssim filter), ink bounding-box deltas, a reference-over-replica
contact sheet, a red/cyan deviation overlay, and --fail-under to exit
non-zero on a measured floor.

Skills now route reference-bearing briefs to it, and the entry skill says
to read only the routed workflow's SKILL.md.
@mintlify

mintlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Aug 8, 2026, 1:35 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

…st floor

The documented caveat was wrong. Probing which reference frame the live
seek lands on shows it lands exactly on the right one (peak SSIM at frame
210 for t=7, falling off on both sides), so there is no seek drift to
work around.

The real floor is the decode gap: a replica is a live browser paint, a
reference is a decoded compressed video. Flat graphics self-compare at
0.998-0.999; photographic video sits near 0.93 at high quality and 0.89
at draft, and most of that is a uniform level shift rather than a
structural error.

Adds meanSignedDiff so that shift is a number instead of an eyeball call
on the overlay, printed as 'diff X% (bias +Y%)', and replaces the caveat
with the measured per-content floors.
ssimFfmpegArgs was a one-caller function returning a literal array, plus
a test asserting the array contains its own inputs; the args now sit at
the call site. --labels did nothing on the --against route either: the
sheet rows are already labelled reference and replica, and the only other
use was a progress line.
The parse mirrored the older unknown-typed style, so every field was
type-tested at runtime. Citty hands string flags back as strings and
boolean flags as booleans, so ReferenceCompareCliArgs states that and the
helpers narrow string | undefined instead.
…ls manifest

The reference-path assertion compared against a literal POSIX path, which
Windows resolves to D:\tmp. It now resolves the expectation the same way
the parser does. The skills manifest also missed the hyperframes-cli
reference-gate edit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant