Install
openclaw skills install screencast-studioClawHub Security found sensitive or high-impact capabilities. Review the scan results before using.
Auto-record narrated demo videos of any web UI from a Playwright-driven walkthrough — primary use case is the vibe-coding test loop (you just shipped a feature with AI help and need to verify it / vibe-show it / iterate on it without manual screen recording). Output is a final.mp4 (synthetic cursor lerp + Material click ripples + burned-in subtitles + optional persistent mask regions for sensitive UI) plus a 4-pass review screenshot set for visual + privacy QA. Activate when the user wants to test or share something they just vibe-coded, or asks for a polished walkthrough / OSS feature demo / bug repro screencast / tutorial recording.
openclaw skills install screencast-studioAuto-record narrated demo videos of any web UI — so the user can test, share, and iterate on what they just vibe-coded.
The primary use case: the user has just shipped a feature (often with AI help), and wants to verify it visually + show it to a teammate without manually screen-recording every time. The longer-term vision is building products by scrolling demos — see a version, tell the AI what's off, swipe to the next take, stop when one matches what you wanted. Scrolling demos is the dev loop, not just a way to review the output.
The technical trick: a Playwright headless recording has no real cursor. The visual cursor + click ripples + subtitles you see in the final video are ffmpeg overlays composed from a structured events log, not real mouse events. This decoupling lets the recording script stay declarative ("click this, narrate that") while the production-quality visuals (smooth cursor lerp, ripple flash, subtitle timing) come for free from the post-processor.
Activate when the user asks for any of:
Don't activate when:
gen-cursor + gen-ripple → cursor.png, ripple.png (one-time)
login → storageState.json + page summary
record → raw.webm + events.json (incl. mask events)
postprocess → final.mp4 + subs.srt (mask blur applied)
deploy → output/screencast-{stamp}.mp4
review → review/{flow,visual,coverage,sensitive}/*.png
clean → drop scratch files
npm run ship runs record → postprocess → deploy → review → clean as one command.
When the user says they want to record a demo for some web app, do this:
Ask the user (briefly, ideally in one round):
http://localhost:3000 or remote)/loginD:\AI\my-demo or ~/projects/my-demo)./output/)1440x900Copy templates into the working directory:
templates/record.js → <working-dir>/record.js
templates/postprocess.js → <working-dir>/postprocess.js
templates/review.js → <working-dir>/review.js
templates/login.js → <working-dir>/login.js
templates/gen-cursor.js → <working-dir>/gen-cursor.js
templates/gen-ripple.js → <working-dir>/gen-ripple.js
templates/deploy.js → <working-dir>/deploy.js
templates/clean.js → <working-dir>/clean.js
templates/package.json → <working-dir>/package.json
Then in the working directory:
npm install
npx playwright install chromium
The templates already accept these env vars (no code edit needed for most cases):
SCREENCAST_BASE — target URLSCREENCAST_LOGIN_PATH — defaults to /loginSCREENCAST_VIEWPORT_W / SCREENCAST_VIEWPORT_H — defaults 1440/900DEPLOY_DIR — defaults to ./output/DEPLOY_PREFIX — defaults to screencastSUBTITLE_FONT — overrides the platform-detected CJK fontFor a one-off, edit record.js lines 22-26 directly.
npm run setup # generates cursor.png and ripple.png
npm run login # opens a real browser; user logs in manually
Skip npm run login if your target page is public (no auth needed). record.js will run without a storageState.json if the file doesn't exist — the demo just won't have any logged-in session. The npm run login step exists specifically for apps behind a login screen.
After npm run login (when used), post-login-summary.json will contain visible nav / heading / button text. Read this file before authoring the stage flow — it tells you what selectors are available without poking the live UI.
Before authoring the stage flow, you MUST ask the user about sensitive UI regions to mask. This is a hard requirement, not an optional polish step. If the demo will be shared (OSS / X / public README), unmasked PII in the recording is hard to retract — git history rewrites + CDN takedown tickets are painful.
Read post-login-summary.json and post-login.png for context, then ask the user something like:
Before I start the recording, I want to mask sensitive UI regions. Looking at this app, here's what I notice that often needs masking — please confirm or correct:
- Top-left logo / brand (if internal product)
- Sidebar user badge / username / avatar (bottom-left in many SPAs)
- Footer version number (e.g.
v1.x.x-beta)- Real names, emails, internal project codenames
Which of these should be blurred? Anything else specific to your demo (real customer data, internal task IDs, business strategy text)?
Then edit PERSISTENT_MASKS in record.js (top CONFIG block):
const PERSISTENT_MASKS = [
{ selector: '.user-badge', label: 'username' }, // resolved once at startup
{ selector: 'header .logo', label: 'logo' },
{ box: { x: 0, y: 820, w: 220, h: 80 }, label: 'sidebar-bottom' }, // fixed coordinates
];
Selector vs box:
position: fixed or sticky — otherwise the captured boundingBox drifts when the page scrolls. If the resolver detects non-fixed positioning, it warns in the log; switch to box in that case.{x, y, w, h} in viewport coordinates. Use this when selectors are unreliable, or to cover a known-stable region (e.g. footer area regardless of element).If the user says "no masks needed" (e.g. demo is on a generic public site with no PII), leave the array empty — the recording proceeds without any blur. Don't lecture.
Edit the body of the try { ... } block in record.js (search for STAGE FLOW). The page is already on BASE when your flow starts (record.js handles first navigation + mask resolution before the STAGE FLOW block) — your code does NOT need to call page.goto(BASE) again.
Use the helpers documented in references/helpers-api.md. For a fuller pattern guide see examples/walkthrough-flow.md.
npm run ship
This runs the full pipeline (record → postprocess → deploy → review → clean). On a 2-minute demo with ~20 clicks, expect:
After npm run ship completes you MUST read the review screenshots before declaring the demo done. Subtitle counts and ship success do NOT prove the recording is shippable.
Read all 4 review passes:
review/flow/click-XX-*.png — sample clicks with high y or large delta from previous click; confirm cursor is on targetreview/visual/sub-XX.png — every subtitle: does the text match what UI shows?review/coverage/stage-XX.png — for any tryStep stage: did it actually execute?review/sensitive/ — read EVERY image and answer:
mask-XX-*.png (cropped to mask region): is the area unreadable / blurred to noise?scan-XX-*.png (full frame at 10s intervals): is there ANY PII visible? Look for usernames, real names, emails, UUIDs, version numbers, internal project codenames, business strategy text, real customer data.If the sensitive pass surfaces unmasked PII: STOP. Add a box mask to PERSISTENT_MASKS (you don't need a selector for retroactive masks — just measure coordinates from the offending scan-XX.png). Then run only npm run render && npm run review:sensitive (no need to re-record — the original events.json is reused). Iterate until review/sensitive/ is clean.
If the user explicitly said "no masks needed" upfront but you still find PII in scan-XX.png: do NOT silently ship. Flag it back to the user and ask how to proceed.
See references/known-pitfalls.md for other things to watch for.
Inside the try { ... } block of record.js:
| Helper | What it does |
|---|---|
await sub('subtitle text') | Adds a subtitle event + holds the page for a CJK-aware duration |
await click(locator, '操作描述') | Cursor moves to target, dwells, clicks, dwells (full ceremony) |
await scroll(deltaY, ticks=1) | Wheel-scrolls main content area (mouse parks in viewport center first) |
await hold(ms=400) | Explicit pause |
await tryStep('name', async () => { ... }) | Non-fatal stage; if it throws, log and continue |
page | The underlying Playwright Page (escape hatch for anything the helpers don't cover) |
Full API in references/helpers-api.md.
sub + click + scroll + hold calls. Think of sub as the narrator's voice and click/scroll as the action.tryStep if the UI surface depends on data that may not exist (e.g. an empty list view has no rows to click).await locator.waitFor({ state: 'visible' }) before clicking.exact: true for robustness:
page.getByText('Save', { exact: true }) ✓page.locator('text=Save') ✗ (matches "Save", "Saved", "Save changes" — substring match)See references/prerequisites.md for the full list. Summary:
Auto-installed by npm install: playwright, ffmpeg-static, fluent-ffmpeg
OS-level: Node 18+; chromium binary (one-time npx playwright install chromium); a CJK font (Microsoft YaHei on Windows / PingFang SC on macOS / Noto Sans CJK SC on Linux — the postprocess auto-detects)
Project-specific: the target web app's URL and login credentials; optionally test files for upload stages and a deploy directory
See references/known-pitfalls.md. The big ones:
click helper auto-scrolls into view, but if you bypass it (e.g. raw locator.click()), the synthetic cursor will fly to coordinates outside the recorded frame.scroll helper does this, but raw page.mouse.wheel(0, dy) from cold-start will scroll whatever element is at (0, 0) (usually the sidebar).text=... is substring match — use getByText(s, { exact: true }) for precision.report.txt shows the events fired, not that the demo looks correct and not that PII is hidden. The 4-pass review screenshots (flow / visual / coverage / sensitive) exist precisely so you (or the user) can verify visually after every ship.selector-based masks capture boundingBox once. If the element is not position: fixed / sticky, the mask stays put while the element moves. Use a box covering the worst-case viewport position instead.