Community코딩 & 개발github.com

piyush140104/screen-record-skills

Claude Code skill: scripted browser-walkthrough videos with ElevenLabs voiceover, music, click SFX, and burned-in subtitles.

screen-record-skills란 무엇인가요?

screen-record-skills is a Claude Code agent skill that claude Code skill: scripted browser-walkthrough videos with ElevenLabs voiceover, music, click SFX, and burned-in subtitles.

지원 대상Claude Code~Codex CLICursor
npx skills add piyush140104/screen-record-skills

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

/screen-record — Scripted browser-walkthrough videos

When to use

The user wants a polished video of a website doing something — typically for LinkedIn, Twitter, a sales deck, an investor update, or onboarding docs. They describe what should be on screen, what the cursor does, and roughly what the narration should say. You script it; the engine handles cursor animation, TTS via ElevenLabs, music, click SFX, and subtitle burn-in.

Do not use this skill for: real screen recordings of an already-running session (use macOS Screenshot or QuickTime), GIFs (use a GIF tool), or videos that need a real human voiceover (you can still author the script, then have them re-record it).

How it works

The engine is at engine.mjs. It exports runDemo(config) which:

  1. Generates one TTS clip per cue with ElevenLabs.
  2. Generates a soft ambient music bed + a UI click SFX with ffmpeg.
  3. Launches headless Chromium at 1920×1080, records video.
  4. Runs your scenes(helpers) function which drives the browser.
  5. Mixes audio (narration loud + music quiet + clicks subtle) and burns ASS-styled subtitles.
  6. Returns paths to the final .mp4, .webm, .srt, .ass.

Workflow

Step 1 — read the user's request

Pin down: URL to record, flow (what the cursor should do), tone (formal/casual), length target. If anything is missing, ask one focused question rather than guessing.

Step 2 — check the API key

Look for ELEVENLABS_API_KEY in this order:

  1. $CLAUDE_PLUGIN_ROOT/.env.local (or the skill folder's own .env.local)
  2. The current project's .env.local
  3. process.env

If absent, tell the user:

I need an ElevenLabs API key. Get a free one at https://elevenlabs.io/app/settings/api-keys (10,000 chars/month, enough for ~15 demos). Either paste it in chat and I'll save it to the skill's .env.local, or add it yourself.

If a key is present but the engine throws ApiKeyDeadError (HTTP 401/402/403), tell the user:

Your ElevenLabs key got rejected (revoked, expired, or out of quota). Rotate it at https://elevenlabs.io/app/settings/api-keys, then update .env.local. I won't retry until you do.

Do NOT silently retry, do NOT fall back to macOS say unless the user explicitly asks for it.

Step 3 — pick a voice

Default is Liam (TX3LPaxmHKxFdv7VOQHJ) — energetic American male, modern. Lighter than George (British) and Sarah (American female). Other free-tier voices in engine.mjs under FREE_TIER_VOICES. If the user says "use X voice" find that in the catalog. If they say "deep/heavy/light/warm/female" pick accordingly.

Library voices (Charlotte, Rachel, Adam, etc.) require a paid ElevenLabs subscription. If the user asks for one and they're on free tier, you'll get HTTP 402 — fall back to the closest free-tier voice and tell the user.

Step 4 — probe the target page (only if needed)

If you don't know the page's structure, use Playwright or curl to inspect the DOM and find selectors. Don't guess — selectors matter, ambiguous matches will time out the recording.

For an SPA with dynamic content, prefer aria-label, role, or text selectors over class names. Use page.locator(s).first() semantics (the engine's helpers already do this).

Step 5 — write a scene file

Create a file at /tmp/screen-record-<name>.mjs (don't pollute the user's repo) that imports the engine and calls runDemo. Template:

import { runDemo } from '/Users/<you>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// or, if installed via symlink:
// import { runDemo } from '/Users/<you>/.claude/skills/screen-record/engine.mjs';

await runDemo({
  baseUrl: 'http://localhost:3000',
  outputDir: './output/onboarding-demo',
  outputName: 'onboarding',

  splash: {
    label: 'Visit us at',
    url:   'www.yourapp.com',
    footer: 'YourApp — onboarding flow',
  },

  cues: [
    { id: 'intro',   text: 'Welcome to YourApp. Let me show you around.' },
    { id: 'signup',  text: 'Tap Sign Up to create an account.' },
    { id: 'form',    text: 'Enter your email and a password.' },
    { id: 'done',    text: 'And that is it — you are in.' },
  ],

  // Optional: mock backend so the demo is deterministic.
  routes: [
    { match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
  ],

  // Optional: pre-seed an authed user so the demo skips login.
  // initialAuth: { id: 'u1', email: '[email protected]', name: 'Demo User' },

  scenes: async ({ page, baseUrl, sleep, speak, awaitCue,
                   clickAt, typeInto, moveCursorTo, hideSplash, typeSplashUrl,
                   configSwitch, showSwitch, hideSwitch, setAuth }) => {

    // Scene 0: URL splash (engine already navigated to baseUrl off-camera).
    speak('intro');
    await typeSplashUrl('www.yourapp.com');
    await awaitCue('intro');
    await hideSplash();
    await sleep(750); // matches the splash fade

    // Scene 1: click sign up
    speak('signup');
    await clickAt('button:has-text("Sign Up")');
    await awaitCue('signup');

    // Scene 2: fill the form
    speak('form');
    await typeInto('input[type="email"]', '[email protected]');
    await typeInto('input[type="password"]', 'CorrectHorseBattery');
    await awaitCue('form');

    // Scene 3: submit
    speak('done');
    await clickAt('button[type="submit"]');
    await sleep(1000);
    await awaitCue('done');
  },
});

Step 6 — run it

cd /Users/<you>/Desktop/Projects/skill/screen-record-skills
# First time only:
npm install --silent
# Every time:
node /tmp/screen-record-<name>.mjs

Each run takes about as long as the final video plus ~10s overhead.

Step 7 — report

Tell the user:

  • Path to the final .mp4 (LinkedIn-ready, 1920×1080, H.264 + AAC)
  • Path to .srt (LinkedIn will auto-import this as closed captions)
  • Offer to play with afplay <path> on macOS

Helper reference (what your scenes can call)

HelperWhat it does
pageRaw Playwright Page if you need it
baseUrlThe configured base URL
sleep(ms)await sleep(800)
moveCursorTo(x, y, durationMs?)Smooth move to coords
moveToSelector(selector, durationMs?, offset?)Smooth move to element center
clickAt(selector, durationMs?)Move + pulse + click (logs click time for SFX)
typeInto(selector, text, perChar?)Click + clear + type character-by-character
recordClick()Log a click timestamp manually (for SFX)
speak(cueId)Mark cue start. End is start + audioDuration + 0.35s
awaitCue(cueId)Sleep until the cue's narration finishes
typeSplashUrl(text, perChar?)Animated typing into the splash URL bar
hideSplash()Fade out the splash (always sleep ~750ms after)
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub })Set the account-switch card content
showSwitch() / hideSwitch()Toggle the account-switch overlay
setAuth(user)Set localStorage[authStorageKey] to a new user (paired with showSwitch for account-swap demos)

Pacing rules of thumb

  • One sentence per cue. Keep it under ~12 words.
  • For UI actions, speak(cue) BEFORE the action so narration plays while the cursor moves.
  • For typing-heavy scenes, the typing takes longer than the narration — use sleep instead of awaitCue to pace.
  • Total video length is usually sum(audioDurations) + sum(sleeps). Aim for 30–75s for LinkedIn.

Common selectors that break

  • text=Foo on its own can match many elements. Use page.locator(...).first() which is what the engine already does, OR use button:has-text("Foo") to scope to a tag.
  • Tailwind class names are fragile. Prefer aria-label, role, semantic tag + text.
  • Modals/dialogs: wait for [role="dialog"] or the modal's heading text before clicking inside.

What to NOT do

  • Don't write actual narration text into the scene file — keep narration in cues[].text so subtitles and audio stay in sync.
  • Don't burn the cursor into the page CSS — the engine injects it.
  • Don't await page.screenshot() mid-scene — you'll capture the cursor in odd positions; use extractFrames post-hoc if needed.
  • Don't commit .env.local (it's in .gitignore).

End of skill instructions.

관련 스킬