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.
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.
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.
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.
--watchre-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-shawhen the bytes need to match.
$ 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
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.
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.
| 0 | publishable — ship it |
| 1 | the app broke the flow — a partial bundle and a watchable failure clip are written |
| 2 | rendered, but failed verification |
| 3 | infrastructure problem (ffmpeg, browser, fonts) |
| 4 | bad spec or arguments |
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 →
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.
$ 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
MCP, in your editor
Playhead does not need its own model. npx playhead init wires Cursor, Claude Code and Codex with an MCP server and skills, then your agent explores the live app, copies locators verbatim from the grounded catalog, and renders.
The agent is the author; Playhead is the grounded compiler. Guessing an accessible name is the one thing the playbook forbids.
> Film creating a new deal on localhost:3000 playhead_session_start url=localhost:3000 playhead_catalog 42 elements, 3 regions playhead_act click role=link[name="Deals"] playhead_catalog pipeline view playhead_act click role=button[name="New deal"] playhead_commit → specs/new-deal.yaml playhead_validate_spec live: 9/9 locators ok playhead_renderverdict: publishable
playhead explore
Print every addressable element on the page as a ready-to-paste locator — grouped by toolbar, marked for stability, reading through open shadow DOM, with container targets listed separately for use as a camera focus:.
Add --spec tour.yaml to explore the way render will see the page: same session, same network rules, with the spec’s setup: steps run first.
$ playhead explore http://localhost:3000 # Acme Admin — 42 interactable elements # stability: ● high · ◐ medium · ○ low ## nav ● [link] role=link[name="Deals"] ● [link] role=link[name="Users"] ## dialog — Canvas controls — ● [button] testid=graph-canvas-toolbar-fit ◐ [input] label=Deal name ## focus targets (use as focus: on a step) ● testid=graph-canvas-toolbar
Six jobs, one artifact.
Anywhere a screen recording is currently made by a human at 4pm on a Friday and is wrong by Monday.
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.
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.
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.
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.
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.
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.
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 attestre-hashes every frame before trusting a verdict.
- 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 verificationWrite 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.