画面収録
Chromeのスクリーンキャストがブラウザ自体を録画し、60 fpsのH.264 .mp4として再構築します。キャプション、カーソル、ズームと効果音は後から追加されるため、ページの動作に関わらず動画は滑らかです。
スクリプトは、カレントリポジトリのnode_modulesからplaywrightを解決するため、リポジトリルートから実行してください。
<skill-dir>はこのスキルのフォルダです。
0. 事前に確認する
リクエストに明記されていない限り、1つのメッセージで以下を確認します。
- どんなストーリー? どのアプリとURLか、どのページか、視聴者が見るべき3~6の場面。誰が見るのか(チームメイト、レビュワー、お客様)?
- 音声は? キャプションのみか、ナレーション付き(AI音声)か。お客様向け動画は常にナレーション付きです。
- どこに送る?
.mp4ファイルのみ(デフォルト)、GitHubのPRまたはIssue、Linear Issue、Slack、またはその他。PR/Issue/チャンネルを尋ねてください。他人の代わりにチケットを作成しないでください。 - どのデータ? どのローカルDBまたはアカウントで録画するか。本番環境や実際の顧客データは絶対に使用しないでください。
1. 録画する
-
アプリをローカルで起動し、各ページを最初に1回開いてください:そうしないと、開発サーバーの最初のコンパイルが動画に含まれてしまいます。
-
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()は音を出しません。 -
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を使用します。
ナレーション(音声が求められた場合)
script.jsonを作成します:[{ "id": "intro", "say": "…" }, …]、各stepのIDに対応する1つの音声行です。耳で聞くために書きます:短い文、数字は単語に(「1500」は「せんごひゃく」に)。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で取得してください。{ narration: '<audio-dir>/durations.json' }を指定して録画します。各stepは、対応する音声行が終了するまで待機し、開始時間は<video>.mp4.marks.jsonに記録されます。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は直接アップロードするため、ブラウザは必要ありません。
prepare_attachment_uploadをissue、filename、contentType(video/mp4)、size(正確なバイト数:macOSではstat -f%z <file>、Linuxではstat -c%s)で呼び出します。- 60秒以内に、
curl -X PUT --data-binary @<file> "<uploadRequest.url>"を、uploadRequest.headersのすべてのエントリをそのまま送信して実行します。ヘッダーの大文字小文字を変更したり、1つでも省略すると403エラーになります。一度に1ファイルずつアップロードします。 create_attachment_from_uploadをissueとassetUrlで呼び出します。- スレッドで再生させるには、
save_commentをと1行のキャプションで呼び出します。Issueを一度開いて、プレーヤーとしてレンダリングされることを確認します。 - 動画を置き換える:新しい動画をアップロードし、同じコメントを編集します(
idを指定してsave_commentを呼び出す)。その後、古い添付ファイルをdelete_attachmentで削除します。
Slack
Slack MCPを使用する場合、ユーザーがチャンネルを確認した後(共有は元に戻せません):
slack_get_file_upload_urlをfilenameとcontent_length(正確なバイト数)で呼び出します。URL='<upload_url>' && curl -X POST "$URL" -H 'Content-Type: video/mp4' --data-binary @<file>。URLはバイト単位でそのまま保持します。401エラーはURLが破損または期限切れであることを意味するため、新しいものを取得します。slack_complete_file_uploadをfile_id、channel_id(C…/D…ID。名前ではなく、slack_search_channelsで検索)、オプションのthread_tsとinitial_commentで呼び出します。
MCPがない場合は、ユーザーにファイルパスをドラッグ&ドロップで渡します。
その他の場所
ファイルパスとサイズを渡します。配信先にサイズ制限がある場合は、scale: 1 / CRF=24について言及します。