Communitygithub.com

johnwhoyou/screen-video

キャプション付き、オプションで音声ナレーション付きの画面収録をPlaywrightで実行中のWebアプリに対して行います。滑らかなカーソル、キーボードとクリック音、ズームとパン、2倍~4Kの高精細な動画を生成し、プレーンな.mp4、GitHubのPRやIssue、Linear Issue、Slack、お客様向けチュートリアルなど、必要な場所に届けます。デモ動画、画面収録、UI/UXの変更のウォークスルー、ナレーションツアー、PRやチケットの動画、リリースやバグ再現のクリップ、お客様のオンボーディングやハウツー動画(字幕、Gmailのメールシーン)が求められたときに使用します。

screen-video とは?

このスキルは、Chromeのスクリーンキャスト(screencast)を基に、60fpsのH.264 `.mp4`動画を作成します。キャプション、カーソル、ズーム、効果音は後から追加されるため、ページの動作に関わらず動画は常に滑らかです。 主な特徴: - **高品質**: デフォルトの2倍のピクセル密度(2560×1600)で録画し、テキストをより鮮明にします。3倍の4K(3840×2400)も可能。 - **ナレーションと効果音**: Google Gemini TTSを使用したAI音声ナレーションと、マウスクリックやキーボードの打鍵音(メカニカルキーボードのサンプル音)を追加できます。 - **スマートな操作**: スキル専用の`click()`や`hover()`関数は、カーソルを滑らかに移動させ、`zoom()`関数で拡大表示、`type()`関数で人間らしいタイピング速度を実現します。 - **配信先**: プレーンなファイル、GitHub(インライン再生)、Linear Issue(MCP経由)、Slack(MCP経由)に直接アップロード可能。 このスキルは、`playwright`、`ffmpeg`、そしてGoogle AI StudioのAPIキー(音声ナレーション用)に依存します。コードは`node_modules`から`playwright`を解決し、スキルの`scripts/`ディレクトリ内のスクリプトを利用します。動画はステップバイステップで作成され、各ステップにキャプションとナレーションのIDが割り当てられます。このスキルは、デモ動画、UI変更のウォークスルー、PR説明、バグ再現クリップ、お客様向けチュートリアルなど、あらゆる種類の画面収録に適しています。

対応~Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/johnwhoyou/screen-video/tree/HEAD/plugins/screen-video/skills/screen-video

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

プレビュー

スキル README より

walkthrough.mp4

ドキュメント

画面収録

Chromeのスクリーンキャストがブラウザ自体を録画し、60 fpsのH.264 .mp4として再構築します。キャプション、カーソル、ズームと効果音は後から追加されるため、ページの動作に関わらず動画は滑らかです。

スクリプトは、カレントリポジトリのnode_modulesからplaywrightを解決するため、リポジトリルートから実行してください。 <skill-dir>はこのスキルのフォルダです。

0. 事前に確認する

リクエストに明記されていない限り、1つのメッセージで以下を確認します。

  1. どんなストーリー? どのアプリとURLか、どのページか、視聴者が見るべき3~6の場面。誰が見るのか(チームメイト、レビュワー、お客様)?
  2. 音声は? キャプションのみか、ナレーション付き(AI音声)か。お客様向け動画は常にナレーション付きです。
  3. どこに送る? .mp4ファイルのみ(デフォルト)、GitHubのPRまたはIssue、Linear Issue、Slack、またはその他。PR/Issue/チャンネルを尋ねてください。他人の代わりにチケットを作成しないでください。
  4. どのデータ? どのローカルDBまたはアカウントで録画するか。本番環境や実際の顧客データは絶対に使用しないでください。

1. 録画する

  1. アプリをローカルで起動し、各ページを最初に1回開いてください:そうしないと、開発サーバーの最初のコンパイルが動画に含まれてしまいます。

  2. scripts/record-lib.mjsをベースにフロースクリプトを作成します(examples/playwright-site/flow.mjsをコピーすると、公開サイトの実行可能なテイクになります)。 各ステップの字幕を1行画面に表示するには、step(id, caption, action)(または引数なしのcaption())を使用します。字幕は画面下部中央に表示され、プレーヤーの再生バーの上に浮かび、半透明です(captionStyle: 'top'でバナー表示)。動画の長さは約1分を目安にしてください。 クリックは、フローのclick(locator) / hover(locator)を使用し、locator.click()は使用しないでください。これらは描画されたカーソルを最初にターゲットまで滑らかに移動させます。裸のlocator.click()も動作しますが、カーソルはジャンプします。 zoom(locator, { scale })でターゲットにズームインし、unzoom()で元に戻します。click(locator, { zoom: true })は、クリック前に拡大します。ズームは移動するまでそのまま維持され、ズームしたビューの外側をクリックすると、そこにパンします。

    ズームの予算:人が身を乗り出すように感じさせるべきで、反射的な動きではありません。

    • 小さなテキスト自体が見せたい場合にのみズームします:差分、変更された値、ダイアログのメッセージなど。ナビゲーション、メニューを開く、誰でも見えるボタン押下では絶対にズームしないでください。
    • 約15~20秒に1回のズームに抑えます。連続する2回のズーム:その間はパンで移動し(次のターゲットにzoomを呼び出す)、元に戻ってから再び拡大するのではなく、そのままにします。
    • 同じエリアで次のアクションが発生する間はズームしたままにし、ストーリーが別の場所に移動したらunzoom()します。
    • 倍率は1.3~1.5。単一のアイコンのような非常に小さいものにのみ2倍を使用します。最低2秒間保持します。
    • ズームよりもナレーション付きの一時停止を優先します:フルサイズで問題なく読める行なら、ズームしないでください。

    type(locator, text, { clear })で入力をシミュレートします:人間のペースで入力します。clickとtypeは各クリックとキーストロークを<video>.sfx.jsonに記録し、narrate.mjsが録音されたサウンドを音声の下に配置します:sounds/からの録音されたマウスクリックとメカニカルキーボードのサンプルです。SFX_VOLUMEで音量を調整(デフォルト1)、SFX=0で無効にします。page.keyboard.typeと裸のlocator.click()は音を出しません。

  3. node flow.mjsで.mp4のパスが出力されます。ffmpegが必要です($FFMPEGまたはPATHに存在するもの。ffmpegがない場合はnpm i --prefix <scratch> ffmpeg-static)。

品質

record(flow, { scale })は、ページをsize(デフォルト1280×800 CSS px、UIは通常表示)でレイアウトし、scale倍のピクセル密度でレンダリングします:2(デフォルト)は2560×1600、3は3840×2400。テキストはよりシャープになり、小さくなりません。2で約15~40 MB/分、3でその2~3倍を見込んでください。サイズ制限のある配信先(GitHub無料プラン10 MB、Slackプレビュー)では、scale: 1またはCRF=24を使用します。

ナレーション(音声が求められた場合)

  1. script.jsonを作成します:[{ "id": "intro", "say": "…" }, …]、各stepのIDに対応する1つの音声行です。耳で聞くために書きます:短い文、数字は単語に(「1500」は「せんごひゃく」に)。
  2. GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir>で、各行に対応するWAVファイルとdurations.jsonが生成されます。Gemini 3.8 Flash TTS(TTS_MODEL、TTS_VOICEでオーバーライド可能)は、OpenAIのgpt-4o-mini-ttsよりも自然に聞こえました。APIキーはaistudio.google.comで取得してください。
  3. { narration: '<audio-dir>/durations.json' }を指定して録画します。各stepは、対応する音声行が終了するまで待機し、開始時間は<video>.mp4.marks.jsonに記録されます。
  4. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4>で、クリップとサウンドを多重化します(CRFで再エンコード、デフォルト18)。<out.mp4>を配信します。

TTSの落とし穴:

  • 行だけを送信してください。「温かく言ってください:…」のようなプレフィックスは読み上げられ、systemInstructionは400エラーを返します。
  • GeminiはWAV全体と来歴チャンクを返します。それを2番目のヘッダーでラップすると、各行の両端で静音が再生されます。tts.mjsはこの理由からデータチャンクを抽出します。そのままにしておいてください。
  • 同期を確認するには、mp4の音声をタイムスタンプ付きで文字起こしし(OpenAI whisper-1、response_format=verbose_json)、マークと比較します。

撮り直しになる落とし穴

  • 入力があらかじめ入力されているフィールドは、.fill('')でクリアしてからpressSequentiallyを使用します。pressSequentiallyは追記のみ行います。
  • 各テイクはデータを変更します(公開、承認、保存)。テイクの間はデータをリセットしてください。
  • 次のステップの前に、自動保存やバックグラウンドのAI入力が完了するのを待ちます。そうしないとUIがジャンプします。
  • ホイールスクロールは折りたたみヘッダーを折り畳み、次のボタンを隠す可能性があります。locator.evaluateを介したel.scrollIntoView()を推奨します。
  • ページをブラウザの全画面表示に決してしないでください:フレームのサイズが変わり、動画が左上にトリミングされます。全画面表示ボタンの前に、document.documentElement.requestFullscreen = undefinedを実行します。

デモデータ

  • ストーリーに必要な状態だけをローカルDBにシードします。example.comのメールアドレスと555の電話番号を持つ、信頼できる人物を使用します。実際の顧客、プレビューDB、本番DBは決して使用しないでください。
  • 履歴、監査、イベントテーブルは追記専用です:撮り直しのたびに行が追加され、次のテイクがステージングされたように見えます。テイクごとに新しいレコードを使用するか、痕跡を残さないアクションにストーリーを限定します。
  • 最初にドライランを行います:フロー内のDRY=1ブランチは、すべての破壊的な確認でキャンセルをクリックします。いくつかのステップマークでフレームを取得し、ロケーターとフレームを修正してから、実際のテイクを1回行います。
  • テイク後、DBの最終状態がキャプションやナレーションの内容と一致することを確認します。
  • データを変更するということは、それを説明する行に対してtts.mjsを再実行する必要があるということです。
  • zshは$VARを単語分割しません。フレーム時間はframes.mjsにそのまま渡すか、bashの下でループします。

顧客向けチュートリアル動画(オンボーディング、ハウツー)

  • 常にナレーション付き。キャプションは短いフレーズに留めます。詳細は音声が伝えます。
  • メールのステップはGmailで行います:scripts/gmail-scene.mjsからmountGmail(page, { emailHtml, subject, sender, me })を使用します。アプリの実際のレンダリングされたメールを渡します。これにより、ポップアップがテイクを間違ったページで待機させるため、target="_blank"が_topに書き換えられます。返信は組み込まれています:#reply-btn、#reply-text、#send-btn。複数の受信トレイがある場合はmailbox: 'sam'を渡します(/samおよび/sam/messageで提供)。返信はアプリに到達しないため、その効果を直接ローカルDBに書き込みます。
  • waitForURLではなく、要素を待機します:認証リダイレクトチェーンはナビゲーションを中断し(ERR_ABORTED)、一部のページではloadが発生しないことがあります。ナビゲーションクリックには{ noWaitAfter: true }を指定します。
  • 開発サーバーを(再)起動した後、テイクがアクセスするすべてのページおよびAPIルートをウォームアップします。
  • 各テイクの前に再シードします。マジックリンクトークンは1回限りなので、失敗した試行ではそれらが消費されます。

2. 確認する

node <skill-dir>/scripts/frames.mjs <video.mp4> 10 30 50で、動画の横にframe-<t>.jpgが書き出されます。配信前にそれらを確認します。ナレーションがある場合は、ユーザーに一度聞いてもらいます:フレームでは音声の欠陥をキャッチできません。実際の顧客データ(名前、メール、電話)がないか確認します:アップロードすると、配信先を表示できるすべての人に公開されます。

3. 配信する

ファイルのみ(デフォルト)

最終的な.mp4の絶対パスを出力します。完了です。

GitHub PR、Issue、コメント(インライン再生)

GitHubは、https://github.com/user-attachments/assets/<uuid>というURLの動画を、単独の行に配置した場合にのみインラインで再生します。そして、そのURLはGitHubのWebアップロードからのみ取得できます。gh、REST API、コミットされたファイルはすべて代わりにダウンロードリンクを提供します。アップロードはリポジトリメンバーのみにプライベートで、コミットされることはありません。

node <skill-dir>/scripts/attach-video.mjs <GitHub PR or Issue URL> <video.mp4>
  • 表示可能なChromiumウィンドウを開きます。保存されたサインインが期限切れの場合、GitHubのサインインページが表示されます:ユーザーがサインインします。パスワードを尋ねたり、ブラウザのクッキーを読み取ったりしないでください。
  • ページのコメントボックスからアップロードします。コメントは投稿されません。ATTACHMENT_URL https://github.com/user-attachments/assets/<uuid>を出力します。
  • サインインは~/.cache/screen-video/gh-profileに記憶されます。FORGET_SESSION=1でアップロード後に削除します。同時アップロードはプロファイルを待ちます(WAITING_FOR_PROFILE)。

URLを本文またはコメントに単独の行として配置します(gh pr edit <n> --body-file <file>、gh issue comment <n> --body-file <file>)。確認するには、PRのbody_htmlに<video src=…private-user-images…>タグがないか確認します。curl -sIでアセットURLを確認しても、再生できる場合でも404になることがあります。サイズ制限:無料プランでは10 MB、有料プランでは100 MB。

Linear Issue

Linear MCPは直接アップロードするため、ブラウザは必要ありません。

  1. prepare_attachment_uploadをissue、filename、contentType(video/mp4)、size(正確なバイト数:macOSではstat -f%z <file>、Linuxではstat -c%s)で呼び出します。
  2. 60秒以内に、curl -X PUT --data-binary @<file> "<uploadRequest.url>"を、uploadRequest.headersのすべてのエントリをそのまま送信して実行します。ヘッダーの大文字小文字を変更したり、1つでも省略すると403エラーになります。一度に1ファイルずつアップロードします。
  3. create_attachment_from_uploadをissueとassetUrlで呼び出します。
  4. スレッドで再生させるには、save_commentを![walkthrough.mp4](<assetUrl>)と1行のキャプションで呼び出します。Issueを一度開いて、プレーヤーとしてレンダリングされることを確認します。
  5. 動画を置き換える:新しい動画をアップロードし、同じコメントを編集します(idを指定してsave_commentを呼び出す)。その後、古い添付ファイルをdelete_attachmentで削除します。

Slack

Slack MCPを使用する場合、ユーザーがチャンネルを確認した後(共有は元に戻せません):

  1. slack_get_file_upload_urlをfilenameとcontent_length(正確なバイト数)で呼び出します。
  2. URL='<upload_url>' && curl -X POST "$URL" -H 'Content-Type: video/mp4' --data-binary @<file>。URLはバイト単位でそのまま保持します。401エラーはURLが破損または期限切れであることを意味するため、新しいものを取得します。
  3. slack_complete_file_uploadをfile_id、channel_id(C…/D… ID。名前ではなく、slack_search_channelsで検索)、オプションのthread_tsとinitial_commentで呼び出します。

MCPがない場合は、ユーザーにファイルパスをドラッグ&ドロップで渡します。

その他の場所

ファイルパスとサイズを渡します。配信先にサイズ制限がある場合は、scale: 1 / CRF=24について言及します。

関連スキル