Agents record their own spools. After an agent builds a feature, it drives the app in a real browser, records a real continuous video (not screenshot stitching), narrates it with AI voice, and renders a designed, captioned MP4 — no human ever hits record. Think of the narrated walkthroughs you'd make with screen-recording products like Loom, except the agent is the producer.
Inspired by BuilderIO/agent-native's Clips, inverted: there a human records and the agent watches; here the agent is the producer.
steps.mjs (agent-authored demo script)
│
├── spool vo → vo/seg_NN.wav + word timestamps OpenAI gpt-4o-mini-tts + whisper-1
│ (in parallel) (bounded concurrency pool)
└── spool record → video.webm + timeline.json Playwright recordVideo, fake cursor,
│ human-speed motion, natural timing
spool render → final.mp4 Remotion: retime each step to fit its
narration, play its capture at 1x then
freeze-hold, click zooms, word-synced
captions, VO at each step's offset
Sync is record-first, narrate-parallel, retimed-in-render: the capture runs at natural
interaction speed while narration is generated concurrently, then the renderer sizes each
step to max(narration+pad, recorded) — playing the recording at 1x and freeze-holding its
last frame for the remaining dead air under the voice. Nothing is padded during capture, so a
5-step build drops from ~90s to roughly max(record, vo) + fast render.
Two authoring paths land on the same render.
Live — you just drove the flow while verifying a feature, so drive it once more and let spool record as you go. No steps.mjs to author or debug:
cd <your-project>
# boots a headless recording browser + an HTTP control server on 127.0.0.1:<port>.
# stdout prints one line: {"port":N,"session":"<dir>"}
spool live spool/my-feature --url http://localhost:3000
# then, per step (narration is required — the renderer fits the window to it):
curl -sX POST 127.0.0.1:$PORT/step -d '{"name":"open","narration":"The dashboard now loads the new flow."}'
curl -sX POST 127.0.0.1:$PORT/js -d '{"code":"await h.click(\"#open\"); await page.waitForSelector(\".result\")"}'
# … more /step + /js … a bad selector returns {ok:false} without killing the session …
curl -sX POST 127.0.0.1:$PORT/end # finalizes video.webm + timeline.json + a generated steps.mjs
spool finish spool/my-feature # vo → render → share → final.mp4 + share/
spool publish spool/my-feature # → https://<host>/l/<id>spool live also writes a generated steps.mjs capturing the config, per-step
names/narration/zoom, and the js snippets that succeeded — so the take is reproducible and
editable as a scripted spool later.
Live (OS target) — capture the whole macOS desktop instead of a browser tab, for demos
that leave the browser (native apps, the terminal, multiple windows). Same control protocol,
no page driver — you drive the desktop yourself (osascript/cliclick/your own tools) between
steps and use /sh to run terminal-visible commands:
spool live spool/my-demo --target os --title "…" # ffmpeg avfoundation full-display capture
curl -sX POST 127.0.0.1:$PORT/step -d '{"name":"open","narration":"…","zoom":"none"}'
# … drive the desktop out-of-band, then optionally log terminal commands …
curl -sX POST 127.0.0.1:$PORT/sh -d '{"cmd":"ls src/record"}' # returns + logs stdout/exit
curl -sX POST 127.0.0.1:$PORT/end # capture.mp4 + timeline.json + steps.os.md
spool finish spool/my-demoArrange your desktop first (hide unrelated windows, bring the app you're demoing to the
front — the capture is the whole display). zoom defaults to "none" on the OS target;
pass {"x":…,"y":…} (capture-pixel coords) to zoom toward a point. Screen Recording
permission is required: if capture comes back black, spool fails fast telling you to grant
it to your terminal in System Settings → Privacy & Security → Screen Recording, then restart
the terminal. Optional: --display <idx> picks which screen (default: the first).
Scripted — reproducible; author the driver up front:
spool init my-feature # scaffolds spool/my-feature/steps.mjs
# author the steps: N steps × { name, narration, zoom, run(page, h) }
spool dry spool/my-feature --headed # debug the driver cheaply, no VO/video
spool build spool/my-feature # (vo ‖ record) → render → share → final.mp4 + share/
spool publish spool/my-feature # → https://<host>/l/<id> — one link, click to watch(spool build on a live/recorded session skips recording and finishes it, so build works
for both paths.)
spool publish uploads the video + share bundle to the hosted watch app (web/ in this
repo, deployable to Vercel + Blob) and returns a single unlisted, unguessable link —
video player, chapters, transcript for humans; raw spool.json on the same page for agents.
Every build also emits share/ — a machine-readable bundle so another agent can
consume the spool without watching video: spool.json (steps, narration, timings, click
coords, keyframe paths), transcript.txt, one keyframe PNG per step, and
console.jsonl (browser console/pageerror/requestfailed telemetry captured during
recording). A receiving agent runs spool read <dir> for an instant digest, then Reads
only the frames it cares about — e.g. to review a demoed feature, file bugs from
console errors, or verify a claimed fix actually renders.
Requirements: node ≥ 20, ffmpeg on PATH, and a voiceover engine. The engine auto-resolves:
- your own key —
OPENAI_API_KEY(env, the project's.env, oropenaiKeyin~/.spool.json); or - hosted (zero-key) — just the
host+tokenyou already put in~/.spool.jsonforspool publish. Voice runs on the hosted app with no OpenAI key of your own — the same dashboard token covers both publishing and voice (subject to a fair-use daily cap); or - local (free) —
--engine localwith aSPOOL_VO_SHscript for local TTS/whisper.
Force one with --engine openai|hosted|local; omit it to auto-detect in that order.
Setup: npm install && npm link in this repo (chromium comes from Playwright's cache,
npx playwright install chromium if missing).
See CONTRACTS.md for the full data contracts (steps.mjs shape,
timeline.json, vo/manifest.json). The only file an agent authors per spool is steps.mjs;
everything else is generated.
- Capture is an adapter.
--target browser(default) = PlaywrightrecordVideo(CDP screencast → WebM, ~25fps, headless, zero OS permissions).--target os= macOS full-displayffmpeg avfoundationcapture (real cursor, 30fps CFR H264, long edge capped at 2560), which needs Screen Recording permission. Both emit the same timeline/render contract; the OS target addstarget:"os"+capture.mp4and drops thepagedriver. - Render is Remotion, not ffmpeg filter graphs. The recording is composited onto a rounded card with gentle zooms toward logged click coordinates (Screen-Studio style) and captions are rendered as designed React, not burned SRT. The one hand-written ffmpeg video pass (WebM → CFR H264) exists because Remotion seeks VFR VP8 pathologically slowly.
- Dry-run first (scripted path).
spool drydrives the steps with no VO or video so the agent can fix selectors/timing before spending TTS calls and render minutes. The live path skips this — you drive once and fix fumbles inline (a failed/jsdoesn't kill the take). - Live is record-derived.
spool liveinverts authoring: instead of writing a driver and debugging it, the agent drives the real app once over an HTTP control port and the steps are derived from the session, then emitted as a reproduciblesteps.mjssnapshot.