Imported from tandryukha/aidemo (
AGENTS.md). Install upstream withnpx skills add tandryukha/aidemo. Copyright stays with the author.
Working on aidemo — guide for coding agents
You are working on the demo engine, not just using it. The README explains what aidemo is and why it's built this way; CONTRIBUTING.md covers process (DCO sign-off, PR expectations, CI policy). If this file contradicts them, they win.
Mental model: storyboard.json (script + per-scene voice/music plan +
browser action-spec) → voice (OpenAI TTS) → record (deterministic replay in
real Chrome, injected cursor, timeline) → captions (Whisper word timing) →
compose (ffmpeg: trim idle, sync to narration, zoom, cards, mux) →
output/final-demo.mp4.
Commands
| Task | Command |
|---|---|
| Install | npm install — Node 20+, system Chrome, ffmpeg+ffprobe on PATH (no Playwright browser download) |
| Type check | npm run typecheck |
| Fixture server | node examples/local-demo/serve.mjs (port 8787) |
| E2E smoke test | node bin/aidemo.mjs render examples/local-demo --headless |
| Dry-run actions only | node bin/aidemo.mjs probe examples/local-demo --headless |
| Resume a take | node bin/aidemo.mjs record <dir> --from-scene <id> (also render, MCP fromScene) — reuses the previous take's earlier scenes (hash-guarded), replays their actions fast, records from <id> |
| Draft from a Playwright trace/test | node bin/aidemo.mjs import-trace <trace.zip|spec.ts> --name <demo> — actions + selectors → scenes, no LLM (MCP import_trace) |
| Draft from a URL | node bin/aidemo.mjs init <name> --from-url <url> — inspect first, headings → scenes, unique selectors → beats (MCP init_demo {fromUrl}) |
| Selector discovery | node bin/aidemo.mjs inspect <url> --dir <demo> — unique selectors per visible element (MCP inspect job); the same scan writes logs/drift-*.json suggestions when a take's selector matches nothing |
| Validate a storyboard (no browser) | node bin/aidemo.mjs validate <dir> (--file <path>, --json; non-zero exit on issues) |
| Lint / pacing forecast (no browser) | node bin/aidemo.mjs lint <dir> (--lang, --json, --strict) — also auto-runs in probe/record/render; measured counterpart is output/report.json from compose |
| Frames for review | node bin/aidemo.mjs frames <dir> --every 3 (--source raw for the take) |
| Walkthrough bundle | node bin/aidemo.mjs walkthrough <dir> — output/walkthrough/ (index.html, guide.md, frames, captions) from the final video (also auto in render with output.walkthrough) |
| One pipeline stage | node bin/aidemo.mjs voice|record|captions|compose <dir> |
| Screenshot stills | node bin/aidemo.mjs stills <dir> — extract named PNGs from an existing take (also auto-runs in render when the storyboard has still markers) |
| Golden regression check | node bin/aidemo.mjs probe <dir> --update-golden (write baseline) / --golden (CI guard, non-zero exit on drift) |
| Multi-language matrix | node bin/aidemo.mjs render <dir> --langs de,fr — one take → N locales (also --lang on voice/captions/compose) |
| Personalized variants | node bin/aidemo.mjs render <dir> --variants variants.json (or --param k=v) |
| Always-fresh embed snippets | node bin/aidemo.mjs embed <dir> — stable raw-GitHub URLs for READMEs/PRs |
| CI render (consumers) | uses: tandryukha/aidemo@stable (composite action, action.yml) — see docs/CI.md |
| MCP server (agent interface) | node bin/aidemo.mjs mcp — stdio; smoke test: npm run mcp-smoke (needs Chrome) |
| Print authoring guide | node bin/aidemo.mjs guide (--topic core|schema|attention|polish|…, --list) |
| Environment check | node bin/aidemo.mjs doctor |
render, voice, and captions need OPENAI_API_KEY in .env (or
OPENAI_BASE_URL pointing at a local OpenAI-compatible server, or
AIDEMO_TTS_PROVIDER=local + npm install kokoro-js for in-process TTS with
offline captions — no key at all); probe, record, and compose re-runs
don't. Every engine change should keep the fixture rendering end-to-end
(that's the smoke test CI can't run for you — it needs Chrome, ffmpeg, and a
TTS/STT endpoint or the local provider).
Layout
bin/aidemo.mjs CLI entry (launches tsx → src/cli.ts)
src/types.ts storyboard schema (zod) — the contract everything shares
src/ pipeline stages: voice, recorder/player/cursor (record),
captions/caption-render, compose/zoom/cards/music/ffmpeg
(compose writes output/report.json), attention (highlight/
spotlight/callout/keystroke/click-ring PNGs + redact blur
filter), inspect (page scan → unique selectors; drift
ranking for failed selectors), anchors ({{@word}} markers → piecewise
retime), frame (produced-look canvas PNG), guide
(topic slices of AUTHORING.md), import-trace (Playwright
trace.zip / spec → draft storyboard), lint (browser-free
pacing forecast + pitfalls), recorder (take lifecycle,
resume via per-scene hashes + raw.keep-* footage),
stills (screenshot mode), frames (review PNGs),
walkthrough (HTML/Markdown bundle from the final video), setup
(cookie/storageState seeding + preflight hook),
i18n (multi-language),
params/variants (personalized renders), golden (probe
regression), embed (always-fresh URLs), capture
(native/OBS), starter (init templates), distribute
(skill install/update/feedback, doctor, repo-init)
src/mcp/ the MCP server (server = tools/resources, jobs = job model)
docs/AUTHORING.md canonical authoring guide — served by the engine
(MCP get_authoring_guide / `aidemo guide`)
action.yml composite GitHub Action (uses: tandryukha/aidemo@stable)
docs/CI.md CI render recipe; docs/EMBEDS.md always-fresh embeds
docs/plans/ deferred designs (public-mcp.md) + roadmap-leftovers-2026-09.md
(open items after v0.14.0); docs/recipes/ how-tos
examples/workflows/ copy-paste consumer CI workflow templates
.claude/skills/ record-demo (thin adapter → AUTHORING.md) + dev skills
.claude-plugin/ marketplace.json — Claude Code plugin marketplace catalog
plugins/record-demo/ published plugin (bundles record-demo skill + aidemo MCP)
blog/ SEO blog source: article JSON + blog.config.json →
blog-engine-baked, committed docs/blog/ (served at
aidemo.top/blog); writer contract via `blog-engine guide
AUTHORING`, map in data/topics.json
examples/local-demo/ self-contained fixture + storyboard — the smoke test
test/mcp-smoke.mjs MCP-surface smoke test (local, needs Chrome)
demos/ untracked local working area
docs/ public docs + README media (docs/internal/ is gitignored, private)
Invariants — don't break these
- No LLM in the capture loop. The recording is a deterministic replay of a fixed action-spec. Agents author storyboards; they never steer a live take.
- Schema ↔ authoring-doc sync.
src/types.tsis the storyboard schema;docs/AUTHORING.mdis the canonical authoring guide that documents it (served to agents via the MCPget_authoring_guidetool andaidemo guide;.claude/skills/record-demo/SKILL.mdis a thin adapter with no schema content). Change the schema → updatedocs/AUTHORING.mdin the same commit. - Published plugin skill stays in sync. The Claude Code plugin ships a copy
of the record-demo skill at
plugins/record-demo/skills/record-demo/SKILL.md. It must be byte-identical to.claude/skills/record-demo/SKILL.md. Edit the canonical one, thennpm run sync:plugin-skill;npm run check:plugin-skillfails on drift. On a version bump, also bumpversioninplugins/record-demo/.claude-plugin/plugin.json. - ffmpeg portability. Many ffmpeg builds lack
subtitles/drawtext. Captions and cards are headless-Chrome-rasterized PNGs overlaid with time-gatedenable. Don't add filters beyond that baseline. - Storyboards are backward compatible. Cinematic and content features
(zoom, scroll easing, ducking, cards, scene
transitioncrossfades,outputsizing/letterbox,stillmarkers,params/placeholders, per-scenenarrations) are opt-in; existing storyboards must render unchanged, and a run with no new flag must be byte-for-byte the old behavior. - Polish is compose-time, not record-time. A bad zoom must stay a recompose, never a re-record.
ffmpeg gotchas (learned the hard way)
- Stream-copy
concatof-loop 1PNG→x264 card segments carries leading negative pts and silently killsoverlaycaptions — the final card assembly must re-encode (concatSegments(..., reencode: true)); scene-only concat can stay stream-copy. - Never use unbounded
apad: it never EOFs and-tdoes not stop it (runaway encode until killed). Alwaysapad=whole_dur=<sec>. Same class:-stream_loop -1throughatrimnever EOFs — loop a computed finite count. zoompan: useon/FPSas the time base (input must be CFR), single-quote every expression (they contain commas), and pre-upscale 2× below ~1600 px width or integer-pixel crops shimmer.- The final mux writes
-movflags +faststart(moov first, web-playable), so compare renders by stream, not by file hash:ffmpeg -i out.mp4 -map 0:v -c copy -f md5 -(and-map 0:a) must match the pre-change render for an unchanged storyboard; the container bytes legitimately differ. - Debugging compose:
AIDEMO_KEEP_TMP=1preserves.compose-tmp/intermediates. Logs land in<demo>/logs/<command>.log; a failed take also leaveslogs/fail-*.png/json.
Repo conventions
demos/is untracked scratch anddocs/internal/is gitignored — never link to either from tracked files.- Commit style: short imperative subject (
fix: compose hangs on unbounded apad); every commit signed off (git commit -s— DCO, required to merge). - CI is typecheck + gitleaks + dependency review. PRs must not add install scripts, new network endpoints, or unpinned GitHub Actions.
- Canonical repo:
github.com/tandryukha/aidemo. Consumers run the engine vianpx -y github:tandryukha/aidemo#stable(git ref, the primary channel) ornpx -y @tandryukha/aidemo(npm, additive — published fromrelease.ymlwhen repo varNPM_PUBLISH=true, via OIDC trusted publishing + provenance);server.jsonis the MCP-registry manifest. See RELEASING.md for how thestabletag moves and the one-time npm setup. - npm
filesallowlist tracks runtime reads.package.jsonfilesships onlybin/,src/,docs/AUTHORING.md,.claude/skills/record-demo/SKILL.md. If you add code that reads a new file relative toENGINE_ROOT/REPO_ROOT(e.g. another served doc or template), add it tofilestoo — else it works from the git checkout but the npm package silently breaks. Verify withnpm pack --dry-run(and an install-from-tarballaidemo guidesmoke).