v0.4.0 · capture once, compose forever

The app does the work. The video has to keep up honestly.

Give Playhead a small spec and a URL. It drives your real app with real input, composes a narrated 1080p walkthrough, then reviews its own output and exits non-zero unless the result is publishable.

npx playhead init
No API key required Runs offline, on your machine macOS · Linux · Windows
the output

This is the whole product, in 1:25.

Not a promo cut. This is out.mp4 exactly as Playhead delivered it, filmed live on saucedemo.com — a public product-docs site with a search palette, in-page jumps and a language switcher. Every row in the step map is a step from the spec, at the timecode the compositor placed its caption; click one to seek there.

step map23 steps
1:25narrated output, 1920×1080 at 30fps
23 stepsfrom saucedemo-checkout.yaml, 169 source frames
23 linesspoken offline by kokoro, normalized to −16 LUFS
publishable8 checks passed, 13/13 actions pixel-verified

Rendered by playhead 0.4.0Capture 65.0s of sourceChecks playhead/checks@4See the spec, the verdict and the contact sheet →

four stages

A pipeline, not an editor.

There is no timeline to drag and nothing to re-shoot. Each stage writes an artifact the next one consumes, which is what makes the whole thing repeatable in CI.

01specA few dozen lines of YAML in your repo. Elements addressed by role, label and text — never a brittle CSS path.specs/tour.yaml
02captureYour real app in a real browser, driven with real input. Frames on a timestamp index plus a structured event log.out/capture/
03composeCamera path, re-synthesized cursor, captions, narration — all applied here, as a pure function of the bundle.out/out.mp4
04verifyTen deterministic checks against the delivered pixels, then a signed verdict and a contact sheet.verify/verdict.json
the one invariant

Re-render, don’t re-record.

Presentation is not baked into the footage. Captions, voice, theme, aspect ratio and camera framing are all decided at compose time, from a bundle that is already on disk. Change the words and the video changes; the product is never touched again.

  • Edit text, keep footage. --watch re-composes on every save.
  • New aspect ratios from the same capture — portrait and square reflow, they never crop.
  • No network in compose. Which is why it is deterministic, and why CI can trust it.
  • Assert determinism with --expect-sha when the bytes need to match.
terminalno app needed
$ playhead capture specs/tour.yaml -o out
  ✓ bundle: 192 frames, 46.8s

# the app is now irrelevant — everything below # reads the bundle on disk and nothing else

$ playhead compose out/capture --spec edited.yaml
  ✓ rendered 3485 frames → out/out.mp4

$ playhead compose out/capture --aspects "9:16,1:1"
$ playhead compose out/capture --gif 960
$ playhead compose out/capture --watch
  watching edited.yaml — re-composing on save

$ playhead verify out/capture out/out.mp4 --json
  ✓ verdict: publishable
what it actually does

Craft decisions, made by code.

The things a human editor would do by hand — framing, pacing, trimming dead air, writing the captions — are computed from what actually happened during capture.

Films the product, not a mockup

Playwright drives your app behind an in-house driver interface. Locators resolve by role, label and text, straight through open shadow DOM, so a design-system app is as addressable as a plain one.

Capture once, compose forever

The capture bundle is the only artifact that ever touches your app. compose and verify are pure functions of it — no network, no browser — so a wording change is a re-render, not a re-shoot.

Camera and cursor are planned, not filmed

The camera path is computed from measured UI geometry with quantized zoom that never crops a target, and the cursor is re-synthesized from the event log — so it is always exactly where the click landed.

Narration at compose time

Each step carries a spoken line, voiced offline by Kokoro on any platform. Pacing grows so every line finishes inside its own step. Audition one with playhead say before you commit.

Secrets never reach a frame

Masked fields are redacted in the DOM before a pixel is painted, and the overlay is parented into the target’s own subtree so it covers an input inside a modal that paints in the browser top layer.

Deterministic, and provable

Re-compose the same bundle and assert the output hash with --expect-sha. Verdicts carry provenance, coverage and hashes of the video, manifest, contact sheet and every bundle frame.

verification

It refuses to call a bad video publishable.

Ten deterministic checks run against the pixels that were actually delivered — not against the plan that produced them. Below is the real verdict for the video above, unedited.

passencode — h264 1920x1080 @ 30fps, 84.9s
passtarget-in-frame — 17/17 targets fully framed, min rendered height 21px
passcursor-on-target — 8/8 clicks land on target; sprite verified on film at 2/2 sampled beat(s)
passaction-effect — 13/13 actions visibly effective
passoutput-pixels — 3 sampled output regions match their planned source
warnfrozen-blank — frames live, but 17 sampled frame(s) have a blank content region (mid-hydration footage?)
passcaptions — 23 captions fit at full size
passpacing — 23 steps paced ≥1.2s, total 84.9s
passaudio — 23 lines audible in their windows, aac, -16.1 LUFS
verdict: publishableexit 0
0publishable — ship it
1the app broke the flow — a partial bundle and a watchable failure clip are written
2rendered, but failed verification
3infrastructure problem (ffmpeg, browser, fonts)
4bad spec or arguments

video sha256 0b4ed39080fc7658…spec hash 445f690cea1f2f91…

render exits 0 only when the verdict is publishable, so it drops into CI as an end-to-end test with video evidence attached. Read what each check asserts →

authoring

You never hand-write a locator.

Three entry points, one reviewable YAML file, and every locator checked against the live DOM before a single frame is captured.

playhead record

A browser opens. Drive your app the way a user would and press Enter when you are done. Every click, field and dropdown becomes a step, and each locator is chosen from the element you actually touched — then checked against the live page on the spot.

No YAML, no AI, no API key. Behind a login wall, save a session with playhead login first — Playhead never sees the password.

terminalauthor by doing
$ playhead login https://app.example.com
  ✓ session → playhead-session.json

$ playhead record https://app.example.com \
    --storage-state playhead-session.json -o tour.yaml

  recording — drive your app, then press Enter
  ✓ click   role=button[name="New deal"]
  ✓ type    label=Deal name
  ✓ click   role=button[name="Create deal"]
  ✓ 3 steps, every locator validated → tour.yaml
where it earns its keep

Six jobs, one artifact.

Anywhere a screen recording is currently made by a human at 4pm on a Friday and is wrong by Monday.

Docs & onboarding

Walkthroughs that never go stale

The UI moved but the copy did not? Re-render from the same bundle. The video regenerates in minutes and the spec is reviewed like any other file in the repo.

playhead render specs/*.yaml -o docs/video -w 4
Release engineering

A release clip per pull request

Wire the composite Action into CI. Every PR gets a playable URL and a verdict, and the job fails when the flow breaks — with a watchable failure clip attached.

playhead render tour.yaml --junit report.xml
QA & support

Bug reports you can watch

bug-repro output is a ticket you can watch: a running clock, a console feed, a closing report card, and bug-report.md with the URL, steps and errors.

playhead render repro.yaml --jira PROJ-482
Sales & marketing

One capture, every aspect ratio

Compose the same bundle to 16:9, 9:16 and 1:1, export an embeddable GIF, and swap the voice or the wording without going near the product again.

playhead compose out/capture --aspects "9:16,1:1" --gif
Coding agents

Your agent writes the spec

Playhead needs no API key of its own: Cursor, Claude Code and Codex author specs through MCP, copying locators verbatim from a grounded live catalog.

npx playhead init
Regression review

Run-over-run, not vibes

compare diffs two runs: verdict drift, newly failing checks, coverage and per-step timing deltas — and exits 2 when the walkthrough regressed.

playhead compare baseline/ current/

See the rendered proof for each

continuous integration

A video is a test that people want to watch.

A composite GitHub Action ships in the box: render, emit JUnit, upload artifacts and — with host-pages: true — publish a playable URL into the pull request instead of an artifact zip. The Dockerfile runs the whole pipeline on the Playwright base image.

  • One testcase per check in JUnit, so a failure names the craft rule it broke.
  • Exit 1 writes a failure clip — when the app breaks the flow you get a watchable artifact, not a stack trace.
  • Ed25519 or HMAC signing, and playhead attest re-hashes every frame before trusting a verdict.

CI & Docker guide

.github/workflows/video.ymlyaml
- uses: Cypher-aman/playhead@v0
  with:
    specs: specs/*.yaml
    host-pages: true      # playable URL in the PR
    junit: report.xml

# and the contract that makes it a test:
#   exit 0  publishable
#   exit 1  the app broke the flow
#   exit 2  rendered, failed verification
get started

Write it once. Film it every release.

One command installs the CLI, wires your coding agent, fetches Chromium and the offline voice. Then point it at localhost and describe the flow you want on film.

MIT licensed Node ≥ 20 Product frames never leave your machine