ティーザー動画
実行中のアプリのローンチクオリティのティーザー(約30〜45秒)を制作します。ストーリーボードを作成し、実際のUIを操作して撮影し、ffmpegで編集して、MP4とREADME用のGIFを納品します。
出力は製品動画のように見えなければなりません。画面録画ではありません。違いは方向性です。演出、フレーミング、リズムであり、解像度ではありません。
リファレンス実装は reference/ にあります。これらは動作するESMモジュールであり、疑似コードではありません。プロジェクト内ではなく、スクラッチディレクトリにセットアップします。
cp -r <skill>/reference/* "$SCRATCH/" && cd "$SCRATCH" && npm install
export TEASER_BASE_URL="https://the-app.example.com"
reference/package.json は "type": "module" を宣言し、3つの依存関係があります。すべてのスクリプトは TEASER_BASE_URL を読み取るため、特定のアプリにハードコードされていません。
何よりも先に:2つの厳格なルール
1. 明示的かつ項目別の同意なしに、現実世界の副作用を決してトリガーしないこと。
実際のアプリを撮影するということは、実際のボタンをクリックすることを意味します。その中には、メールを送信したり、公開投稿したり、課金したり、サードパーティにフォームを送信したりするものもあります。アプリに触れる前に以下を行います。
- ストーリーボード内でマシンの外に出るすべてのアクションを列挙します。
- 各アクションが何をするのか、何回実行するのかをユーザーに正確に伝え、その回数について同意を得ます。
- 不可逆的なステップの手前で止まるモード(プレビュー、ドライラン、撮影してから拒否できる確認ゲート)を優先します。
ライブシステムに対する読み取り専用のプローブなど存在しません。既に実行中のセッションで着地する「診断」クリックは、実際に発火します。アクションが機能しなかった理由をデバッグしている場合、次のクリックは機能することを前提とし、その準備をしておきます。
意図しないものが発火した場合:プロセスをそのソースですぐに停止し(UIボタンだけではなく、セッション/ワーカーを強制終了します。UIボタンは効果がない場合があります)、読み返して停止状態を確認し、最終メッセージでユーザーにその回数も含めて明確に伝えます。
2. すべてのフレームが公開され、PIIで満たされていると想定すること。
実際のアカウントには、名前、メール、住所、書類が含まれます。事前にユーザーとポリシーを決定します。
- フレームで囲む(デフォルト) – アイデンティティを除外するショットを構成します。ffmpegの段階で永続的なクローム(サイドバー、アバター、アカウントメニュー)をクロップします。
- 撮影前に非表示にする – カメラが回転する前に、問題のあるカードに
page.evaluateでvisibility: hiddenを設定します。決して撮影して後で編集で回避しようとしないでください。 - シード済みデモアカウントを使用する – 存在する場合、最もクリーンです。
最終カットのすべてのフレームを納品前にフル解像度でレビューします。コンタクトシートではなく、フレームです。
ワークフロー
1. スカウトとストーリーボード
まず、すべての候補画面の認証済みスクリーンショットを撮影します(reference/scout.js)。それらを確認します。次に、ストーリーボードテーブルを作成します。ビート、ショット、視聴者が感じるべきこと、長さです。役割が割り当てられていないショットは、撮影前にカットされます。
効果的なアーク:緊張(問題)→ 明らかにする(製品)→ 証明(実際に動作する)→ 規模(結果)→ 信頼(実行環境)→ クロージング。
ストーリーボードに、どの画面にPIIが含まれていて、どの画面が安全かを記します。
2. セットの準備
撮影後にではなく、常に事前にキュレーションします。
- データが最も見栄えの良いビューを選びます。完全なチャート、アクティブなリスト、認識可能な名前、ゼロでないカウント。スピナー、空の状態、または半分レンダリングされたチャートは決して撮影しません。
- 初期化スクリプトまたはプリロールの
page.evaluateを介してlocalStorageを使用して、UIの状態(パネル幅、折りたたみ/展開、フィルター、スクロール位置)を意図的に設定します。 - トースト、バナー、Cookieバーはカメラ外で閉じます。
- データを落ち着かせます:
networkidleに加えて数秒待ちます。
3. 撮影
画面録画ではなく、CDPスクリーンキャスト(Page.startScreencast)を使用します。これは、画面上のページを約100fpsでキャプチャするため、ユーザーが行っていることは一切フレームに表示されず、マシンは使用可能なままです。Playwrightが操作をドライブし、CDPが撮影します。reference/rig.js がこれを実装しています。
- 1ショットにつき1つの被写体。「アプリ全体」ではなく、そのものをフレーミングします。
- カメラの動きは、ページ内のスクリプト化されたイージングスクロール(
cameraScroll)から生成され、フィルターからは生成されません。以下のzoompanの警告を参照してください。 - カーソルはアクターです。注入されたSVGカーソル(
cursorClick)を使用します。これはイージングでグライドし、押下時にディップします。非表示にすることもできます。アイドル時のドリフトは避けます。 - 各テイクをリハーサルし、フレームを抽出し、確認し、調整してから、本番撮影します。
外部ウィンドウの撮影(アプリ自体が開くブラウザ、デスクトップアプリ)は、OSキャプチャ(gdigrab/x11grab/avfoundation、リグ内の recordDesktop)が必要です。これにより、ユーザーの作業を含む画面上のすべてがキャプチャされます。ユーザーに警告し、席を離れるように依頼し、結果をフレームごとにレビューします。プライベートなものをキャプチャした場合は削除します。
4. タイトルカード
静的な画像ではなく、ブラウザ内で撮影されたアニメーションHTML/CSS。カードマークアップをアプリ自身のオリジン上の空白ページに注入して、Webフォントとパレットが解決されるようにしてから、スクリーンキャストします。ライブページから実際の表示フォントを取得し(getComputedStyle)、推測するのではなく、既知の良好なモノスタックを固定します。
アプリのモーション言語(独自のイージングカーブと期間)に合わせます。プロジェクトに確立されたカードスタイルがない場合は、デザインする前に frontend-design スキルを読み込みます。
5. 編集
reference/build.js は各セグメントを共通のサイズ/FPSに正規化し、チェーンをクロスフェードします。カットは約2.5〜4秒ごとに保ちます。ペイオフの瞬間には通常速度で余裕を持たせ、その周りのルーチンは圧縮します。
6. レビューしてから納品
- コンタクトシートと、1秒ごとのフル解像度フレームを生成します。
- PII、クリップされたパネル、読み取り不可能なテキストがないかフレームを読み取ります。
- プローブを使用して、期間と寸法を確認します。
- MP4と、README用にパレット最適化されたGIF(GitHubはコミットされたMP4をインライン再生しません)を納品します。GIFは約10MB未満に保ちます。
- 依頼がない限り、サイレントのままにします。音楽のライセンスは取得できません。その旨を伝えます。
苦労して得た知見
プッシュインに zoompan を使用しないこと。 フレームごとにズームを再計算し、クロップウィンドウを整数ピクセルに丸めるため、画像が前後にスナップします。地震のように見えます。ロックオフショットと実際のページ内スクロールを使用します。どうしてもプッシュインが必要な場合は、まずソースを4倍にプリスケールし、フレームデルタ測定(reference/jitter.js)で確認します。ロックオフされた静的なショットは、平均デルタが1.0を大幅に下回るはずです。目に見えるブレは3以上になります。
アニメーションが落ち着くと、スクリーンキャストはフレームの出力を停止します。 1.2秒後にアニメーションが終了するカードは、1.2秒のクリップになります。tpad=stop_mode=clone:stop_duration=N でパディングし、-t で希望の長さにカットします。
固定レートではなく、タイムスタンプ付きフレームをアセンブルします。 スクリーンキャストフレームは metadata.timestamp とともに到着します。実際のフレームごとの期間でffmpeg concatリストを作成し、その後、一定の60fpsにリサンプリングします。固定間隔を想定すると、ジャダーが発生します。
ffmpeg-static をdev依存関係として使用し、モジュールパスで解決します。ffmpegがPATHにあることに依存してはいけません。(ffprobe はそのパッケージに含まれていません。代わりに ffmpeg -i のstderrを解析します。)
認証付きキャプチャ: ヘッド付きで1度ログインし、storageState を保存し、すべてのテイクで再利用します(reference/login.js)。セッションは期限切れになります。同じ日に撮影を完了するか、再実行します。また、そこに localStorage のUI設定をシードします。
デプロイされたオリジンからのローカルホストは、Chromeのローカルネットワークアクセスポリシーによってブロックされます。 ページがローカルサービスに到達する必要がある場合は、--disable-features=LocalNetworkAccessChecks,PrivateNetworkAccessChecks,BlockInsecurePrivateNetworkRequests を指定して起動します。
永続的なクロームは、固定オフセットでクロップします。 ビューポート幅 1920 + railWidth で撮影し、crop=1920:1080:railWidth:0 でサイドバーを削除して、正確に1080pに着地します。
成果物
パス、期間、寸法を報告します。ユーザーからまだ必要なもの(音楽、GitHubネイティブの動画アップロード)を明確に述べ、撮影中にユーザーが知っておくべきことが発生した場合は開示します。