Community程式設計與開發github.com

AppLoidx/record-demo

Agent skill / project related to launch or product demo video: AppLoidx/record-demo

record-demo 是什麼?

record-demo is a Claude Code agent skill that agent skill / project related to launch or product demo video: AppLoidx/record-demo.

相容平台Claude Code~Codex CLICursor
npx skills add AppLoidx/record-demo

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Record demo videos

Write an ad-hoc Playwright storyboard, then run the self-contained engine from any cwd. Do not add the storyboard to an application repo unless the user asks for that. Keep the storyboard self-contained; the runner stages that file inside the engine before Playwright loads it, so do not use storyboard-relative helper imports. <skill-dir> means the directory where this repository is cloned, for example ~/.claude/skills/record-demo.

Write the storyboard

Import the DSL with this portable import:

import { demo } from "record-demo/storyboard";

The runner rewrites this specifier to the engine's real path when staging, so the storyboard file can live anywhere. Editor type resolution for record-demo/storyboard comes from engine/tsconfig.json paths - outside the repo, map record-demo/storyboard to the absolute <skill-dir>/engine/lib/storyboard.ts path in your tsconfig paths, or keep the file in engine/scripts/.

Use engine/scripts/example-site.demo.ts as the reference. Prefer getByRole selectors, use d.click(locator) instead of locator.click(), pace actions with d.hold(...), and assert or wait for the final state.

demo("storyboard-title", async (d) => {
  await d.seed(async (page) => {
    await page.getByRole("heading", { name: "Example Domain" }).waitFor();
  });

  await d.step("Read the page", async (page) => {
    const heading = page.getByRole("heading", { name: "Example Domain" });
    await d.zoomTo(heading, { padding: 48 });
    await d.hold(2_000);
    d.zoomOut();
    await heading.waitFor({ state: "visible" });
  });
});

The engine overrides the timeline name with --name or the script basename, so the string passed to demo(...) does not need to match the output slug.

Preflight (before any capture)

When recording against a locally served checkout: git fetch origin in the serving checkout and assert HEAD == origin/main (or the user-named branch) — a stale checkout records a stale design. If stale, rebuild/restart the server first. Print the serving SHA into the take notes so every recording is attributable.

Record

Require Node >= 22.18 (nvm or system), ffmpeg, Xvfb, and xdpyinfo. Install dependencies once inside engine/ with pnpm install.

node <skill-dir>/engine/record.mjs \
  --script /absolute/or/cwd-relative/demo.demo.ts \
  --url https://example.org \
  [--name output-slug] [--formats 16x9,1x1,9x16] \
  [--chrome none|macos|browser] [--storage-state <path>] [--raw]

Use --formats to select one or more composition presets (default 16x9) and --chrome to choose the rendered window frame (default none). Formats render in the requested order, and the first is the primary used to encode the GIF. The 9x16 format is an aggressive center crop (about 44% of the page width is visible in the wide shot), so keep the action centered when authoring for portrait.

Use --raw to stop after the clean capture. Without it, expect these deliverables:

  • engine/out/<name>-raw.mp4
  • engine/out/<name>-<format>.mp4 for every requested format
  • engine/out/<name>.gif encoded from the primary format

The runner prints absolute deliverable paths when it finishes. Treat live-site reproducibility as the target site's responsibility and never embed real credentials in a storyboard.

Authenticated sites

Record authenticated flows only with staging or test accounts; never use production credentials. The agent should first log in once with its own tooling, using a headless Playwright script. If sign-in requires an emailed OTP or magic link, the preferred route is the agent's own mail access - a connected Gmail connector/MCP on Claude Code, or a mail CLI on other runtimes - so it reads the code itself; otherwise ask the user to paste it. After login, save the session with await context.storageState({ path: storageStatePath }), where storageStatePath is a one-shot temporary path created as storageStatePath="$(mktemp -d)/<account>.storage-state.json". Then run the recording command with --storage-state "$storageStatePath".

Playwright storageState restores cookies and localStorage only. Apps that keep auth in IndexedDB or sessionStorage will silently record a logged-out screen, so verify the authenticated page before capturing. The storage-state file is a secret: never print or commit it. The recorder deletes it immediately after loading it; it is a one-shot handoff, so re-recording requires a fresh login and nothing auth-related is left on disk. The one-off login helper script also holds credentials: keep it temporary and never commit it.

Edit a recorded demo in Remotion Studio

Export a self-contained project after recording, then install and open Studio:

node <skill-dir>/engine/scripts/export-project.mjs <name>
cd <skill-dir>/engine/out/<name>-project
pnpm install
pnpm studio

Studio's timeline panel shows editor-style tracks (Screen filmstrip, one named clip per caption, spotlight, zoom move, and outro) - drag the playhead to scrub, click a clip's row to find its event. Use the props panel or fixtures/timeline.json to edit captions, zoom timing (keep zoom moves at or above the 1650ms floor), spotlight timing, and the outro title. Edit remotion/theme.ts for colors, typography, spacing, and motion. Render the finished 16:9 video with pnpm render.

Motion and storyboard rules (what makes a clip postable)

  • One camera move per scene, not per field. Zoom once to the section, act inside it. Per-input zooms read as edgy cuts.
  • Land wide through page transitions, stay wide on sparse pages. d.zoomOut() BEFORE a click that navigates; never zoom to a status pill on a near-empty page.
  • Sections near the page bottom: d.zoomTo([...locators], { scroll: "center", padding: 48 })
    • the camera clamp cannot center a page-edge rect; scroll-centering fixes it at the root. Locator[] frames a whole section via union bbox.
  • d.spotlight([...locators]) = Apple-style focus dim (cutout + ring). Generous holdMs, end explicitly with d.spotlightOff() + hold(600) before the step ends. Spotlight rects must fit the viewport (enforced); zoom rects may overflow by design.
  • Seed waits for real data, not just the trigger element - otherwise the video opens on a skeleton. Wait for a known content node + ~600ms.
  • d.settle(locator) before outcome captions. Views that re-fetch after a submit blank briefly; a plain waitFor resolves on the first paint and the outcome caption fires over a white page.
  • Captions are imperatives tied to the cursor action; put an outcome caption (d.caption(...) late in the step) on closing scenes; trim step captions so imperatives never hang over completed actions.
  • d.outro(title) closes postable clips with a calm end slate after the final scene.
  • Always assert the end state - without it the pipeline renders a polished video of a broken flow.
  • Never ship a clip with a localhost URL, internal hostname, provider name, or real credentials on screen. Mask presentation-only strings in the seed (window.open patch and a MutationObserver input rewrite).

Verify before reporting done

  1. pnpm typecheck && pnpm test in engine/.
  2. node scripts/score-motion.mjs out/timeline.json - FAIL blocks; WARN zoom-rate ~0.78/s is the accepted steady state; any discontinuity verdict is a camera bug; dead-air WARNs mean a static stretch >= 3.5s with under 40px of cursor travel - re-pace or caption it.
  3. node scripts/prove-composition.mjs after engine changes.
  4. Watch the actual render: ffmpeg -i out/<name>-16x9.mp4 -vf fps=1 out/frames/%03d.png and read the frames. Zoom clipping, caption lag, cutout desync and on-screen dev URLs are invisible in a green test run.

Gotchas

SymptomCause / fix
Invalid demo viewport ... abortGeometry drifted - fix launch flags, never relax the assertion.
Cursor offset ~174px verticallyChrome ran tabbed, not --app.
Could not find clapperboard framesDetector tolerance is 64 (external captures average impure); if it still misses, check the capture isn't black (Chrome on wrong DISPLAY).
Captions/cursor drift over the clipClapperboard sync - don't wall-clock subtract.
Spotlight cutout off its targetRect raced a smooth scroll - measureTarget's stability loop should hold; if it recurs, raise its timeout.
Words cut mid-letter at crop edgezoomTo(locator, { padding: 120 }) for modals.
Zoom clips content off-framemaximumScale taste issue (1.45 works), not a clamp bug.

Verify engine changes

Run from engine/:

pnpm typecheck && pnpm test
node scripts/prove-composition.mjs

相關技能