Verification
Anyone can render a video. The reason Playhead can be trusted in a pipeline is that it reviews its own output afterwards and refuses to call a bad one publishable.
What is being checked, exactly#
Not the plan — the delivered pixels. Verification decodes frames out of the finished MP4, projects the geometry the compositor recorded in compose-manifest.json, and compares the two. A compositor bug that draws the wrong frame or the wrong crop fails here even though every internal calculation was self-consistent.
The manifest is the contract between the two halves: the renderer records what it actually drew — positions, times, overflow facts, camera fallbacks, clock gaps — and the verifier holds it to that.
The ten checks#
| Check | Asserts |
|---|---|
encode | The file is a real, decodable H.264 video at the planned resolution, frame rate and duration. |
target-in-frame | Every action target was fully inside the frame and rendered large enough to read — at least 28px tall for a close-up, unless a wide shot waived it. |
cursor-on-target | The rendered cursor was on the element at the moment of each click, confirmed against the cursor sprite on film. |
action-effect | Each action visibly changed the UI, compared frame to frame before and after the beat. |
output-pixels | For sampled steps, the region of the MP4 where the subject should be correlates with the same region of the source bundle frame. |
frozen-blank | No frozen stretches spanning an action beat, and no blank content regions. |
captions | Every caption fit its card at full size — nothing shrunk, nothing truncated. |
pacing | No step is too fast to follow (at least 1.2 seconds each). |
masks | Masked regions are opaque in the delivered output and absent from the event log. |
audio | Each spoken line is audible inside its own step window, at the target loudness. |
Reading a verdict#
This is the real verify/verdict.json for the video on the examples page, abridged:
{
"schema": "playhead/verdict@3",
"verdict": "publishable",
"video": { "sha256": "0b4ed39080fc7658770aac9a913b7c9b…" },
"manifestHash": "4a700ec618ad55872ee71d446a786beb…",
"coverage": {
"actionsVerified": 13,
"actionsTotal": 13,
"outputSamplesVerified": 3,
"checksSkipped": 1,
"checksWarned": 1
},
"provenance": {
"playheadVersion": "0.4.0",
"specHash": "445f690cea1f2f91aa445064…",
"appUrl": "https://www.saucedemo.com/",
"checkSuiteVersion": "playhead/checks@4",
"host": "win32-x64"
},
"checks": [ … ]
}Coverage is as important as the verdict. A run where every check passed but only two of seven actions could be pixel-verified is much weaker evidence than one with full coverage — and the verdict says so out loud instead of hiding it behind a green tick.
Pass, fail, skip, warn#
| Status | Meaning |
|---|---|
pass | Asserted and satisfied. |
fail | Asserted and violated. The verdict becomes not-publishable and render exits 2. |
skip | Nothing to assert — masks on a spec with no masking rules, audio on a silent video. Skips do not block publication; they are counted in coverage.checksSkipped. |
warn | The check could not measure what it wanted to. It reports honestly rather than passing by default, and the count lands in coverage. |
action-effect cannot compare before and after, so it warns and records the gap. That is the difference between a suite that proves something and a suite that decorates a build log.A real failure, and what it taught#
The first cut of this site’s hero video clicked the 20-pixel “toggle subsection” chevrons in Wikipedia’s contents rail. Every geometry check passed. output-pixels did not:
FAIL output-pixels: 2/6 sampled regions diverge from the plan
contents/0 @19.4s — output region does not match the planned
source region (correlation 0.49)
mechanical/0 @57.2s — correlation 0.49
✗ verdict: NOT PUBLISHABLEThe threshold is 0.5, so this was a near miss rather than a gross error — and it was correct. A subject that small, mutating the instant it is clicked, cannot be confirmed against its own source frame. The fix was an authoring fix: give the camera substantial subjects. Re-authored with real content links instead of chevrons, the same flow verified clean.
That is the intended experience. The check does not tell you your renderer is broken; it tells you the video is not good enough to publish, and points at the two seconds to look at.
Running verification on its own#
playhead verify out/capture out/out.mp4 --json
playhead verify out/capture out/out.mp4 --junit report.xmlJUnit output emits one testcase per check, so a CI failure names the craft rule it broke rather than saying “video job failed”. CI setup →
The contact sheet#
Every run writes verify/contact-sheet.png: key moments as an annotated grid, so a reviewer can sanity-check a two-minute video in ten seconds. Its hash is recorded in the verdict, which means the sheet cannot be swapped after the fact.
Optional: a second opinion#
playhead render specs/tour.yaml --visionSends the contact sheet to a local Ollama model for a qualitative review and records the result under vision in the verdict. It is advisory and never overrides a deterministic check. Frames do not leave your machine, and the flag is off by default.
Comparing runs#
playhead compare baseline/ current/Verdict drift, newly failing checks, coverage change and per-step timing deltas — and exit 2 when the walkthrough regressed. This is how you notice that a step got three seconds slower before anyone complains about the video feeling sluggish.