Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ examples/*
!examples/gcp-cloud-run/**
!examples/docs-reference-project
!examples/docs-reference-project/**
!examples/golden-baseline
!examples/golden-baseline/**
# …but never the local smoke run's build/render artifacts.
examples/gcp-cloud-run/scripts/gcp-smoke-artifacts/
packages/studio/data/
Expand Down
6 changes: 4 additions & 2 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,8 @@
"pages": [
"developers/cli",
"packages/cli",
"packages/lint"
"packages/lint",
"guides/visual-regression"
]
},
{
Expand Down Expand Up @@ -825,7 +826,8 @@
"pages": [
"reference/html-schema",
"reference/color-grading",
"reference/audio-effects"
"reference/audio-effects",
"reference/cli-golden"
]
},
{
Expand Down
140 changes: 140 additions & 0 deletions docs/guides/visual-regression.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: "Visual regression testing"
description: "Commit golden baseline frames and let hyperframes check --golden fail the build when a composition's pixels regress."
---

Agents (and humans) regress layouts silently: a refactor nudges a headline, a
token change recolors a button, a timing tweak leaves a beat blank — and every
individual command still exits 0. The golden baseline gate turns "the video
still looks right" into a hard, committable contract: reference frames live in
the repo, and `hyperframes check --golden` fails whenever the rendered pixels
drift from them.

This guide covers the workflow. For the exact directory convention, manifest
fields, and JSON shapes, see the [golden baseline reference](/reference/cli-golden).

## The loop

```bash
# 1. From a state you have visually reviewed, freeze the baselines.
hyperframes check --update-golden

# 2. Commit them — they are small PNGs, one per sample time.
git add golden/ && git commit -m "test: golden baselines for intro"

# 3. From now on, gate every change.
hyperframes check --golden
```

The gate re-captures the composition at the committed sample times (same
font-localized, settled-page capture path as `hyperframes snapshot`),
pixel-diffs each frame against its baseline, and:

- **Pass** — every frame matches within tolerance; exit code 0.
- **Fail** — the failing times are listed with how much differs, a
`golden-diff/` folder appears with per-frame red-highlight diff PNGs and a
`baseline | current | diff` contact sheet, and the exit code is 1.

```text
Golden
✗ t=2s 0.949% pixels differ (max channel delta 130)
1 of 3 frame(s) regressed vs golden/golden-baseline/
Diff sheet: golden-diff/golden-baseline/contact-sheet.jpg
Intended change? Refresh baselines with hyperframes check --update-golden
```

An intentional change is a one-command ritual: re-run with `--update-golden`,
review, and commit the refreshed PNGs. The diff shows up in the PR as an image
diff, so reviewers see exactly what changed on screen.

## For agent loops

Add `--json` and read the summary instead of parsing logs:

```bash
hyperframes check --golden --json
```

```json
{ "ok": false, "golden": { "failed": [{ "id": "intro", "time": 2, "maxDelta": 130 }], "diffSheet": "golden-diff/intro/contact-sheet.jpg" } }
```

The `diffSheet` is a single image an agent can open to see every regression at
once — baseline, current, and highlighted diff side by side — before deciding
whether to fix the code or refresh the baseline.

## CI wiring

The gate is one exit code, so any CI is a few lines:

```yaml
# .github/workflows/visual-regression.yml
name: visual-regression
on: [pull_request]
jobs:
golden:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: npx hyperframes check --golden --no-browser-gpu
working-directory: my-composition
- uses: actions/upload-artifact@v4
if: failure()
with:
name: golden-diff
path: my-composition/golden-diff/
```

Uploading `golden-diff/` on failure gives reviewers the contact sheet without
re-running anything locally.

## Keeping captures deterministic

Pixel equality is only meaningful when the two captures come from the same
rendering stack:

- **Capture baselines where you gate.** Create and refresh baselines on the
same OS/runner class that CI uses. A macOS-made baseline gated on Linux will
differ in font rasterization.
- **Pin the GPU path.** Pass `--no-browser-gpu` in CI (and when refreshing
baselines) to force SwiftShader's deterministic software rendering.
- **Follow the composition determinism rules** — no `Date.now()`, no unseeded
randomness, no render-time network fetches. Remote fonts are localized
automatically during capture, matching the render path.

If your environment still produces benign single-pixel jitter, tune rather than
abandon the gate: `ignoreAntialiasing` (on by default) absorbs 1px edge shifts,
`threshold` widens the per-channel tolerance, and `maxDiffRatio` grants a small
pixel budget. All three live in `golden/<compositionId>/golden.json` — see the
[reference](/reference/cli-golden#the-goldenjson-manifest).

## Choosing sample times

`--update-golden` defaults to five evenly spaced frames with a readable
end-of-timeline tail. For most compositions, better times are the ones a
reviewer would screenshot: the settled end of each beat, not mid-transition.
Pin them explicitly:

```bash
hyperframes snapshot --update-golden --at 0.5,2,3.5
```

Mid-transition times work — the capture is seek-exact — but they make every
retiming of the animation an (intentional) baseline refresh.

## Try it

A runnable example with committed baselines lives at
[`examples/golden-baseline`](https://github.com/heygen-com/hyperframes/tree/main/examples/golden-baseline):
break the accent color, watch the gate fail with a diff sheet, refresh, commit.

## Related topics

- [Golden baseline reference](/reference/cli-golden) — the exact directory
convention, every flag and manifest field, and the JSON summary shape.
- [Deterministic rendering](/concepts/determinism) — the composition rules that
make pixel equality meaningful in the first place.
- [Render from the command line](/guides/rendering) — the wider CLI loop the
gate slots into.
126 changes: 126 additions & 0 deletions docs/reference/cli-golden.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: "Golden baseline gate"
sidebarTitle: "Golden baselines"
description: "The golden/ directory convention, the check --golden and --update-golden flags, the golden.json manifest, the diff artifacts, and the JSON summary agents read."
---

Use this reference for the exact contract of the golden baseline regression
gate: where baselines live, every flag and manifest field, what the gate writes
on failure, and the JSON shape agents consume. For the workflow — when to
commit baselines and how to wire the gate into CI — start with
[Visual regression testing](/guides/visual-regression).

## The convention

Committed reference frames live beside the composition:

```
<project>/
index.html
golden/
<compositionId>/ # from the root's data-composition-id
golden.json # optional manifest
<timeMs>.png # one baseline per sample time (e.g. 1500.png = t=1.5s)
```

Filenames are the sample time in **milliseconds** (`Math.round(seconds * 1000)`).
Anything that does not match `<digits>.png` is ignored by the scanner, so the
manifest and stray notes never collide with baselines.

## Commands

```bash
hyperframes check --golden # gate: re-capture, pixel-diff, fail on regression
hyperframes check --update-golden # refresh baselines (runs the full check too)
hyperframes snapshot --update-golden # refresh baselines only, no check pipeline
hyperframes check --golden --json # agent-readable envelope
```

| Flag | Command | Meaning |
| --------------------------- | ----------------- | -------------------------------------------------------------------------------------------------- |
| `--golden` | `check` | Run the gate after the normal check pipeline; either failing makes the exit code non-zero. |
| `--update-golden` | `check`, `snapshot` | Capture at the resolved sample times and (re)write `golden/<compositionId>/` plus its manifest. |
| `--golden-threshold <0-1>` | `check` | Per-channel pixel tolerance override for this run (does not enable the gate by itself). |
| `--at <times>` | `snapshot` | With `--update-golden`: explicit sample times in seconds, written into the manifest. |

Sample times resolve in precedence order: explicit `--at` (snapshot only) →
manifest `times` → times encoded in existing baseline filenames → the snapshot
default spread (5 evenly spaced frames with a readable end-of-timeline tail).

`check --golden` with no baselines and no manifest fails with instructions to
run `--update-golden` first — a missing baseline is never silently a pass.

## The `golden.json` manifest

```json
{
"times": [0.5, 2, 3.5],
"threshold": 0.1,
"maxDiffRatio": 0,
"ignoreAntialiasing": true
}
```

| Field | Default | Meaning |
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `times` | — | Timeline sample times in seconds. Written automatically by `--update-golden`. |
| `threshold` | `0.1` | Per-channel intensity tolerance as a fraction of 255; a pixel differs only when a channel deviates more. |
| `maxDiffRatio` | `0` | Fraction of differing pixels allowed before the gate fails. `0` = fail on any counted diff. |
| `ignoreAntialiasing` | `true` | Exclude 1px rasterization edge shifts (each side finds the other's color within its own 8-neighborhood). |

A dimension change (resized canvas) always fails, regardless of `maxDiffRatio`.

## Failure artifacts

On regression the gate writes `golden-diff/<compositionId>/` in the project:

```
golden-diff/<compositionId>/
<timeMs>-current.png # what the composition renders now
<timeMs>-diff.png # red = counted diff, amber = ignored anti-aliasing, pale gray = unchanged
contact-sheet.jpg # one baseline | current | diff row per failed time
```

The directory is cleared at the start of every gate run and only re-created on
failure. Add `golden-diff/` to your `.gitignore` — it is derived output.

## JSON summary

With `check --golden --json`, the golden summary rides inside the normal check
envelope, and the top-level `ok` is false when either the pipeline or the gate
fails:

```json
{
"ok": false,
"golden": {
"ok": false,
"updated": false,
"compositionId": "golden-baseline",
"compared": 3,
"times": [0.5, 2, 3.5],
"failed": [
{
"id": "golden-baseline",
"time": 2,
"timeMs": 2000,
"maxDelta": 130,
"diffRatio": 0.009488,
"reason": "pixel-diff"
}
],
"diffSheet": "golden-diff/golden-baseline/contact-sheet.jpg",
"baselines": ["golden/golden-baseline/500.png", "golden/golden-baseline/2000.png"]
}
}
```

`reason` is one of `pixel-diff`, `dimension-mismatch`, or `missing-baseline`.
`maxDelta` is the largest per-channel deviation (0–255) among counted differing
pixels; `diffRatio` is the differing fraction of all pixels.

## Example

A complete runnable project lives at
[`examples/golden-baseline`](https://github.com/heygen-com/hyperframes/tree/main/examples/golden-baseline)
with committed baselines and a scripted regression to try.
4 changes: 4 additions & 0 deletions examples/golden-baseline/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Derived verification artifacts — regenerated by `hyperframes check --golden`
# and `hyperframes snapshot`; only golden/ baselines are meant to be committed.
golden-diff/
snapshots/
92 changes: 92 additions & 0 deletions examples/golden-baseline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Golden baseline regression gate

A minimal project showing the `hyperframes check --golden` workflow: commit
reference frames ("golden baselines") of a composition, then let CI — or an
AI agent's own verification loop — fail loudly whenever a change moves pixels
it was not supposed to move.

## Layout

```
examples/golden-baseline/
index.html # 640x360, 4s composition
golden/
golden-baseline/ # <compositionId>
golden.json # sample times + diff tuning (optional)
500.png # baseline at t=0.5s (<timeMs>.png)
2000.png # baseline at t=2.0s
3500.png # baseline at t=3.5s
```

Baselines follow the convention `golden/<compositionId>/<timeMs>.png`. The
`golden.json` manifest pins the sample times and can tune the diff:

```json
{
"times": [0.5, 2, 3.5],
"threshold": 0.1,
"maxDiffRatio": 0,
"ignoreAntialiasing": true
}
```

- `times` — timeline sample times in seconds.
- `threshold` — per-channel pixel tolerance as a fraction of 255 (default 0.1).
- `maxDiffRatio` — fraction of differing pixels allowed before the gate fails
(default 0: fail on any counted diff).
- `ignoreAntialiasing` — exclude 1px rasterization edge shifts from the count
(default true).

## Workflow

```bash
cd examples/golden-baseline

# 1. Create (or refresh) the baselines from a state you have reviewed, then
# commit golden/ to the repo.
npx hyperframes check --update-golden # or: npx hyperframes snapshot --update-golden

# 2. Gate every subsequent change. Exits non-zero on any regression.
npx hyperframes check --golden

# 3. Agent-readable result (the golden summary rides inside the check report).
npx hyperframes check --golden --json
```

On failure the gate writes `golden-diff/<compositionId>/` containing the
current frame, a red-highlight diff PNG per failed time, and a
`contact-sheet.jpg` with one `baseline | current | diff` row per failure —
one image an agent (or a human) can read to see exactly what moved.

The JSON envelope includes:

```json
{
"ok": false,
"golden": {
"ok": false,
"failed": [
{
"id": "golden-baseline",
"time": 2,
"maxDelta": 210,
"diffRatio": 0.0042,
"reason": "pixel-diff"
}
],
"diffSheet": "golden-diff/golden-baseline/contact-sheet.jpg"
}
}
```

## Try a regression

Change `#accent-bar`'s `background-color` in `index.html` (say `#f5a623` →
`#e0245e`) and run `npx hyperframes check --golden` — the gate fails, names the
regressed times, and writes the diff sheet. If the change was intentional,
refresh with `--update-golden` and commit the new baselines.

Deterministic rendering matters here: capture baselines and gate on the same
rendering stack (CI runner, `--no-browser-gpu` for SwiftShader determinism if
your machines differ). See the visual regression guide in the docs for CI
wiring.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions examples/golden-baseline/golden/golden-baseline/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"times": [0.5, 2, 3.5]
}
Loading
Loading