/screen-record — スクリプトベースのブラウザウォークスルー動画
使用するタイミング
ユーザーがウェブサイトの動作を示す洗練された動画を必要としている場合 — 典型的にはLinkedIn、Twitter、営業資料、投資家向けアップデート、オンボーディング資料など。ユーザーは画面に表示すべき内容、カーソルの動作、ナレーションの大まかな内容を説明します。あなたがスクリプトを作成し、エンジンがカーソルアニメーション、ElevenLabsによるTTS、BGM、クリック効果音、字幕の焼き込みを処理します。
このスキルを使用しない場合:実際の実行中のセッションのスクリーンレコーディング(macOSのスクリーンショットまたはQuickTimeを使用)、GIF(GIFツールを使用)、または実際の人間の声によるナレーションが必要な動画(スクリプトを作成して、ユーザーに再録音してもらうことは可能)。
仕組み
エンジンは engine.mjs にあります。runDemo(config) をエクスポートし、以下の処理を行います:
- 各キューに対してElevenLabsで1つのTTSクリップを生成。
- ffmpegでソフトなアンビエントBGMとUIクリック効果音を生成。
- ヘッドレスChromiumを1920×1080で起動し、動画を録画。
- あなたの
scenes(helpers)関数を実行し、ブラウザを操作。 - 音声(ナレーション大+BGM小+クリック小)をミックスし、ASSスタイルの字幕を焼き込み。
- 最終的な
.mp4、.webm、.srt、.assのパスを返す。
ワークフロー
ステップ1 — ユーザーのリクエストを読む
以下の項目を確定する:記録するURL、フロー(カーソルが何をするか)、トーン(フォーマル/カジュアル)、長さの目標。不足がある場合は、推測せずに1つの焦点を絞った質問をする。
ステップ2 — APIキーを確認する
以下の順序で ELEVENLABS_API_KEY を探す:
$CLAUDE_PLUGIN_ROOT/.env.local(またはスキルフォルダ自身の.env.local)- 現在のプロジェクトの
.env.local process.env
存在しない場合、ユーザーに伝える:
ElevenLabs APIキーが必要です。https://elevenlabs.io/app/settings/api-keys から無料のキーを取得できます(月10,000文字、約15デモ分に十分)。チャットに貼り付けてスキルの
.env.localに保存するか、ご自身で追加してください。
キーは存在するがエンジンが ApiKeyDeadError(HTTP 401/402/403)をスローした場合、ユーザーに伝える:
ElevenLabsキーが拒否されました(取り消し、期限切れ、またはクォータ超過)。https://elevenlabs.io/app/settings/api-keys でローテートし、
.env.localを更新してください。更新されるまで再試行しません。
ユーザーが明示的に要求しない限り、静かに再試行したり、macOSの say にフォールバックしたりしないでください。
ステップ3 — 音声を選択する
デフォルトは Liam(TX3LPaxmHKxFdv7VOQHJ) — 活発なアメリカ英語の男性、モダン。George(イギリス英語)やSarah(アメリカ英語女性)よりも軽め。他の無料枠の音声は engine.mjs の FREE_TIER_VOICES にあります。ユーザーが「X音声を使って」と言った場合、カタログからその音声を探す。「深い/重い/軽い/暖かい/女性」と言った場合は、それに応じて選択する。
ライブラリ音声(Charlotte、Rachel、Adamなど)は有料のElevenLabsサブスクリプションが必要です。ユーザーが無料枠でこれを要求した場合、HTTP 402が返る — 最も近い無料枠の音声にフォールバックし、その旨をユーザーに伝える。
ステップ4 — 対象ページを調査する(必要な場合のみ)
ページの構造がわからない場合は、Playwrightまたは curl を使用してDOMを調査し、セレクタを見つける。推測しない — セレクタが重要で、あいまいなマッチは録画をタイムアウトさせる。
動的コンテンツを持つSPAの場合は、クラス名よりも aria-label、role、テキストセレクタを優先する。page.locator(s).first() のセマンティクスを使用する(エンジンのヘルパーは既にこれを行っている)。
ステップ5 — シーンファイルを作成する
/tmp/screen-record-<name>.mjs にファイルを作成し(ユーザーのリポジトリを汚染しない)、エンジンをインポートして runDemo を呼び出す。テンプレート:
import { runDemo } from '/Users/<you>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// または、シンボリックリンクでインストールした場合:
// 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.' },
],
// オプション:デモを決定論的にするためのモックバックエンド。
routes: [
{ match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
],
// オプション:認証済みユーザーを事前設定してログインをスキップ。
// 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 }) => {
// シーン0: URLスプラッシュ(エンジンは既にオフカメラでbaseUrlに移動済み)。
speak('intro');
await typeSplashUrl('www.yourapp.com');
await awaitCue('intro');
await hideSplash();
await sleep(750); // スプラッシュフェードに合わせる
// シーン1: サインアップをクリック
speak('signup');
await clickAt('button:has-text("Sign Up")');
await awaitCue('signup');
// シーン2: フォームに入力
speak('form');
await typeInto('input[type="email"]', '[email protected]');
await typeInto('input[type="password"]', 'CorrectHorseBattery');
await awaitCue('form');
// シーン3: 送信
speak('done');
await clickAt('button[type="submit"]');
await sleep(1000);
await awaitCue('done');
},
});
ステップ6 — 実行する
cd /Users/<you>/Desktop/Projects/skill/screen-record-skills
# 初回のみ:
npm install --silent
# 毎回:
node /tmp/screen-record-<name>.mjs
実行時間は最終的な動画の長さに約10秒のオーバーヘッドを加えた程度です。
ステップ7 — 報告する
ユーザーに以下を伝える:
- 最終的な
.mp4のパス(LinkedIn対応、1920×1080、H.264 + AAC) .srtのパス(LinkedInがクローズドキャプションとして自動インポート)- macOSで
afplay <path>で再生する提案
ヘルパーリファレンス(シーンで呼び出せるもの)
| ヘルパー | 説明 |
|---|---|
page | 必要に応じて使用する生のPlaywright Page |
baseUrl | 設定されたベースURL |
sleep(ms) | await sleep(800) |
moveCursorTo(x, y, durationMs?) | 座標へスムーズに移動 |
moveToSelector(selector, durationMs?, offset?) | 要素の中央へスムーズに移動 |
clickAt(selector, durationMs?) | 移動 + パルス + クリック(クリック時間をログに記録して効果音用) |
typeInto(selector, text, perChar?) | クリック + クリア + 1文字ずつタイプ |
recordClick() | クリックタイムスタンプを手動でログに記録(効果音用) |
speak(cueId) | キューの開始をマーク。終了は start + audioDuration + 0.35s |
awaitCue(cueId) | キューのナレーションが終了するまでスリープ |
typeSplashUrl(text, perChar?) | スプラッシュURLバーへのアニメーションタイピング |
hideSplash() | スプラッシュをフェードアウト(その後は常に約750msスリープ) |
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub }) | アカウント切り替えカードの内容を設定 |
showSwitch() / hideSwitch() | アカウント切り替えオーバーレイの表示/非表示を切り替え |
setAuth(user) | localStorage[authStorageKey] を新しいユーザーに設定(アカウント入れ替えデモ用にshowSwitchと組み合わせて使用) |
ペーシングの経験則
- キューごとに1文。約12語以内に抑える。
- UI操作では、アクションの前に
speak(cue)を呼び、カーソルが動いている間にナレーションが再生されるようにする。 - タイピングが多いシーンでは、タイピングがナレーションより長くなる —
awaitCueの代わりにsleepを使用してペーシングを調整する。 - 動画の合計時間は通常
sum(audioDurations) + sum(sleeps)。LinkedInでは30~75秒を目指す。
壊れやすい一般的なセレクタ
- 単体の
text=Fooは多くの要素にマッチする可能性がある。エンジンが既に行っているpage.locator(...).first()を使用するか、button:has-text("Foo")のようにタグに範囲を絞る。 - Tailwindのクラス名は脆弱。
aria-label、role、セマンティックタグ+テキストを優先する。 - モーダル/ダイアログ:内部をクリックする前に、
[role="dialog"]またはモーダルの見出しテキストを待つ。
やってはいけないこと
- 実際のナレーションテキストをシーンファイルに書かない — ナレーションは
cues[].textに保持し、字幕と音声の同期を保つ。 - カーソルをページCSSに焼き付けない — エンジンが注入する。
- シーン途中で
await page.screenshot()しない — カーソルがおかしな位置でキャプチャされる;必要な場合は後処理でextractFramesを使用する。 .env.localをコミットしない(.gitignoreに含まれている)。