Communitygithub.com

yazelin/promo-video-skill

ゼロコストでプロジェクトからプロモ動画を作成するパイプライン:スクリーンキャプチャ、Ken Burns、ターミナルナレーションモード。Claude Code用スキルで、API費用をかけずにWebプロジェクトのローンチ広告を生成します。

promo-video-skill とは?

このスキルは、Webアプリ、ゲーム、画像ギャラリー、CLIツールなど、あらゆるリポジトリからプロモーションビデオ(トレーラー、ソーシャル広告、説明動画)を無料で作成するためのパイプラインです。スクリーンキャプチャ(MODE A)、Ken Burnsモンタージュ(MODE B)、ターミナルアニメーション解説(MODE C)の3つの主要モードを備え、16:9、9:16、1:1、4:5のマルチアスペクト比をサポートします。edge-ttsによるナレーション生成、Geminiによる音声検証と楽曲選定、ffmpegによる合成を組み合わせ、すべて無料のツールで完結します。特に、アプリの自動演出をそのまま録画する「画面が語る」アプローチや、HTML/CSSを活用した直感的な縦型動画(MODE E)の生成に優れています。GOTCHAセクションには、画角維持、Ken Burnsのブレ防止、ヘッドレスWebGLの落とし穴、音声同期の検証方法など、実践的なノウハウが詳細に記録されており、高品質なプロモーション動画を確実に生成できます。

対応✓Claude Code✓Codex CLI~Cursor✓Gemini CLI
npx skills add yazelin/promo-video-skill

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

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

ドキュメント

promo-video — ゼロコストプロモ動画 / 広告パイプライン

Webプロジェクトをプロモ動画やソーシャル広告に変えます。核心となる洞察:アプリ自体が映画のように動作するなら、それをスクリーンキャプチャすればよい。「生成」する必要はありません。画像を出力するプロジェクトなら、厳選した画像をダウンロードしてKen Burnsにします。 その他(ナレーション / 音声検証 / 合成 / ブランド / CTA)は両方で共通です。実戦検証済み:battlefield-editor/tools/trailer(スクリーンレコード)+ catime広告「世界を読む猫」(Ken Burns + ニュース→画像表示)+ 『グリッチとミスター・ブラックホール』全7章完全プレイ(スクリーンレコードの音声付きバリアント、60分、700のセリフを自動進行:https://youtu.be/J9OMebCjr9Y)。

決定木:captureモードを選択

  • 動くアプリ(3D/canvas/自動カメラワーク、オートプレイ)→ MODE A スクリーンキャプチャ(capture-app.mjs)。
  • 画像出力 / 画像ライブラリ / 写真フィード(AI生成画像、ギャラリー)→ MODE B Ken Burnsモンタージュ(厳選画像をダウンロード + kenburns.sh)。
  • アプリ自身が発する音を録音する場合(MODE Aの音声付きバリアント;何を、どれだけ再生するかはアプリがその場で決定し、完全な音声トラックは手元にない) → recordVideoを放棄し、ffmpegで画面と音声を同時にキャプチャします。詳細は「アプリ自身が発する音が必要な場合:本当の録音」を参照。完全な音声トラックが手元にある場合はこのパスを避け、後でmuxする方が音質が良い(GOTCHA 24)。
  • 非視覚的なリポジトリ(CLI / ライブラリ / API / バックエンド / データパイプライン、録画する画面がない)→ MODE C ターミナル / 解説(term-player.html + capture-term.mjs):「コマンド + 実際の出力」をタイピングアニメーションで再現するターミナル録画。Devツールは「クイックカット予告編」ではなく「ナレーション付きチュートリアル」にすべき(GOTCHA 22参照):構成 = ペインポイントのフック → ステップバイステップ(インストール→実行→出力の理解→高度なコマンド)+ edge-ttsナレーション + ステップ字幕 → CTA。ナレーションのタイミングはterm-player.htmlのwindow.__STEPLOG(各ステップの出力が表示された秒数)を使用し、composeは各ナレーションを対応するステップにadelayします。実戦:WIP クロスリポジトリ状態CLIチュートリアル動画(〜60秒、BGMは0.13に抑えてナレーションをメインに)。
  • ライブ録画のベストショート動画(9:16 Reels/Shorts)→ MODE E テンプレート録画(short-9x16.html + capture-short.mjs): course-pipelineでトランスクリプトからベストセリフを選び、16:9のクリップを切り出し、それを9:16のHTMLテンプレートに埋め込んで録画します。 なぜffmpegフィルターではなくHTMLを使うのか:切り抜きはffmpegでも可能ですが、キーワードのアニメーション効果はCSSでは10行で済むのに対し、ASS字幕形式で同じ効果を実現するのは地獄です。レイアウトの調整もCSSを一目見るだけで、filtergraphを盲目的に変更する必要はありません。 境界:course-pipelineがカットし、promo-videoがパッケージ化する、インターフェースは「カット済みのmp4 + 完成品タイムライン上のワード単位のタイムスタンプ」。
  • ハイブリッド:スクリーンレコードを主体に、Ken Burns / ターミナルを補足として使用することも可能。

4つのフェーズ

  1. キャプチャ — MODE A:xvfb-run でヘッド付きChromiumを実行(WebGLに必要)、PlaywrightのrecordVideoで自動再生のハイライト部分を録画。MODE Eはヘッドレスで実行(GOTCHA 47参照)。MODE B:ソースリストを取得し、厳選し、ダウンロード。
  2. ナレーション(オプション)— edge-tts zh-TW-YunJheNeural --rate=-5% でナレーションを生成。
  3. 検証(オプション)— Gemini Proが代わりに聞いてトーンの検証。Gemini Flashが楽曲を選定。
  4. 合成 — brand.pyでブランド素材(タイトルカード/ウォーターマーク/CTA/QR)を生成、ffmpegで字幕を重ね/結合/ミックス、マルチアスペクト比 + 凝縮版を出力。

ここにある6つのテンプレート(トップのconfigを変更 / パラメータを渡すだけで使用可能)

  • brand.py — PIL:cover/containでアスペクト比を維持する合成機、タイトルカード、常時表示ウォーターマーク、CTAカード、ミニギャラリー、QR(API経由)。
  • kenburns.sh — ブレのないKen Burnsクリップ + テキストが画像の端に接する + concat + ミックス(MODE Bの主力)。
  • capture-app.mjs — Playwright+xvfbでのスクリーンキャプチャ(MODE A);内蔵httpサーバー、HUD非表示、ノードをクリックしてジャンプ / キーボード駆動ゲーム。
  • term-player.html + capture-term.mjs — MODE C ターミナル / 解説:HTMLターミナルプレーヤー(タイピングアニメーション+カラー出力)+ xvfbで録画。asciinema/xtermは不要。
  • short-9x16.html + capture-short.mjs — MODE E:9:16テンプレート(スライドを中央に、上下に大きな字幕、 キーワードを大きく表示して跳ねさせる、常時表示ウォーターマーク、プログレスバー)+ xvfbで録画 + 後で元の音声をmux。 スライド部分は自動カメラワーク:画面自体から文字の位置を計測、2〜3秒ごとに構図を変更、キーワードが読み上げられた瞬間にさらにズームイン、明るくする(GOTCHA 32-37)。再録する場合は--tag v2でファイル名を変更し、古い成果物を上書きしない——古いものは比較の基準となります。録画後は「輝度イベントの発生回数/秒」と「成果物のラウドネス」を自動でチェックし、別途検証は不要です。
  • relevel.py — すでに出荷済みの動画のラウドネスを後から調整する場合に使用:音声のみを変更、-c:v copy、実測では前後の動画ストリームのmd5が同一で、1本あたり1秒で完了。新規録画では不要(capture-short.mjsがmux時に調整済み)。
  • audition.py — Geminiが代わりに聞く:music "<用途>" *.mp3(楽曲を選定し0-10で評価)、tone "<テキスト>" v.mp3(ナレーションのトーンを検証)。

アプリ自身が発する音が必要な場合:本当の録音

GOTCHA 24は完全な音声トラックが手元にある場合について述べています(音楽動画、edge-ttsナレーション): 元のファイルを後でmuxするのが最も音質が良いです。しかし、それができない場合があります——何を、どれだけ再生するかはアプリがその場で決定する場合です。実戦例:ビジュアルノベルの完全プレイスルー、700の音声ファイルがストーリーの順序に従って再生され、 「完全な音声トラック」と呼ばれるものは存在せず、位置合わせに使用できません。

その場合は、PlaywrightのrecordVideoを完全に放棄し、ffmpegで画面と音声を同時にキャプチャします:

# 1. 独立した音声受信端を開き、ブラウザの音声をそこに導き、デスクトップの他の音声と混ざらないようにする
MOD=$(pactl load-module module-null-sink sink_name=cap)
# 2. 独自のXスクリーン
Xvfb :77 -screen 0 1280x808x24 -nolisten tcp &
# 3. 画面 + 音声を同時に録画、セグメントファイルに分割(クラッシュしても1セグメントのみ失われる)
ffmpeg -f x11grab -draw_mouse 0 -framerate 25 -video_size 1280x808 -i :77.0 \
       -f pulse -i cap.monitor \
       -vf crop=1280:720:0:88 \
       -c:v libx264 -preset veryfast -crf 24 -pix_fmt yuv420p \
       -c:a aac -b:a 160k \
       -f segment -segment_time 600 -reset_timestamps 1 seg/part-%03d.mp4 &
# 4. ブラウザを起動(DISPLAY=:77 PULSE_SINK=cap)、実行後concatで連結

5つの実戦で遭遇したポイント:

  1. --kioskはPlaywrightでは機能しない、Playwright自身がウィンドウを管理します。タブバーとアドレスバーで固定で88px取られるため、 スクリーンを88px大きく開き、cropで切り取ります。カーソルは-draw_mouse 0で非表示に。
  2. ページめくりに固定秒数を使ってはいけません。 各セリフの長さは異なり、短すぎると語尾が切れ、長すぎると動画全体の長さが2倍になります。 new Audio()をインターセプトし、音声が再生し終わってから次のスライドをクリックします(それらの要素はDOM内に存在せず、 querySelectorでは見つかりません)。
  3. 「コンテンツが進んでいるか」のチェックは必須です。 これがないと、クリックできないカードに遭遇した場合、上限まで空クリックを続け、ffmpegは一切エラーを報告しません:実測では58分間録画され、ファイルは正常で、最後に気づくと全体の4分の1しか進んでいませんでした。判断基準は「画面のテキストが変わったか」であり、 テキストが空の場合も進んでいないとみなす——終了画面にはテキストがなく、これをチェックしなかったためにさらに33分が録画されました。
  4. セグメントファイルに分割(-f segment)、途中でクラッシュしても1セグメントのみ失われ、最後にconcat -c copyでロスレス結合。
  5. 終了時に4つの項目を検証する必要があり、ファイルが生成されたかどうかだけを見てはいけません:総尺、通過したカード数、サンプリングフレームの 標準偏差(0に近い = 空のフレーム)、volumedetectの平均音量(無音はnull sinkが接続されていないことを示し、これもエラーを報告しません)。

実戦例:glitch-vn/tools/capture/(record.sh + play_through.mjs)。成果物は 『グリッチとミスターブラックホール』全7章完全プレイスルー、60分、700のセリフ、一度も人間が操作することなく完了: https://youtu.be/J9OMebCjr9Y

GOTCHAS —— これが最も価値ある資産であり、毎回これに従ってチェックすること

  1. アスペクト比の維持:決して異なるアスペクト比の画像に対してscale=W:Hを使用しない(伸縮・変形する)。背景はcover(アスペクト比を維持して拡大後、切り抜き)、主体はcontain(アスペクト比を維持して縮小して配置)。ffmpegのscaleは整数倍(2x)またはforce_original_aspect_ratioの付加が必要。終了前に「元のアスペクト比 vs 表示アスペクト比」の監査を実行(誤差<0.1%で合格)。

  2. Ken Burnsのブレ:元画像をまず2xに拡大してから縮小(整数ピクセルのブレがサブピクセルになる)+ ズームは線形フレーム番号 z='1+K*on/(D-1)'を使用し、決して累積丸め誤差が生じる z='min(zoom+inc,...)'を使用しない。

  3. zoompanのフレーム爆発:使用しない -loop 1 -t(各入力フレームが×d倍される);単一画像の場合は -frames:v D + zoompan d=D。

  4. テキストが画像の端に接する:画像をまず垂直方向に中央揃え。テキスト位置 = 中心 ± (画像高さ × zmax)/2 ± gap、画像ごとに計算(PILで各画像の高さを出力し、サイドカーとして保持)。これにより、横長の画像でテキストが離れすぎたり、押しつぶされたりしない。

  5. file://のfetchがブロックされる:アプリがfetch(?pkg=)/JSONをロードする場合 → python3 -m http.serverを起動してhttp経由でアクセス(またはcapture-app.mjsに内蔵されたサーバーを使用)。

  6. ヘッドレスWebGLでswiftshaderがクラッシュ / 真っ黒になる → xvfb-run -a --server-args="-screen 0 WxHx24" を使用してヘッド付きChromiumを実行。

  7. ESモジュールアプリ:gotoPhaseがwindow上に存在しない → DOMを使用してタイムラインノード #tlNodes .node[i].click()をクリックし、evaluate(gotoPhase)は使用しない。

  8. QR:PEP668でqrcodeをインストールできない → api.qrserver.com/v1/create-qr-code/?data=<公開リンク>(公開リンクのみ使用);公開前にスマートフォンで一度スキャンして確認。

  9. edge-ttsの破音字:まずは回避(「沒 mò」→他の言葉に置き換える);声調の検証には Gemini Proでピッチの傾向を質問し、whisper STTでは声調を検証できない(読み間違えても同じ文字に変換されるため)。

  10. BGM:Gemini FlashでCC0楽曲を選定、雰囲気に合うもの(戦争のBGMを猫の動画に合わせないこと);CC0の出典を記録。

  11. マルチアスペクト比:16:9=YouTube、9:16=Reels/Shorts/LINE、1:1 & 4:5=FB/IGフィード。MODE Aは各アスペクト比に対してネイティブで再録画(縦型ビューポート);MODE Bは各アスペクト比に対して再合成。

  12. 常時表示ウォーターマーク(チャンネルロゴ):ブランド+URLを動画全体の隅に常時表示(最後のカードだけでなく)、動画全体に1枚の透明PNGを重ねる。

  13. 広告は行動を促すべき:最初の2秒でフック(ミュートでの自動再生、最初のフレームから画像/大きなテキストを表示)、字幕を焼き付ける(FBミュート)、CTAで「今すぐ行動する」理由を提示。

  14. プラットフォームの詳細:FB動画はフィードに投稿、リンクは最初のコメントに配置(リーチが抑制されるのを避ける);+movflags +faststart;LINEの埋め込み再生には、アルバム/動画ボタンで送信し、「ファイル」として送信しない。

  15. スクリーンショットは読み込み完了を待ち、1枚ずつ空でないことを検証:MODE A/ギャラリーの各項目のスクリーンショットは固定秒数を使用しない(遅い画像は「読み込み中」の空のフレームをキャプチャする)。ローディングインジケータが消えるまでポーリングしてからキャプチャ。キャプチャ後、1枚ずつターゲット領域のstdevを測定(≈0 = 空/未レンダリング)、自動で漏れを検出して再キャプチャ。実戦例:roll-formosa苗栗で固定1.8秒では空白をキャプチャしていたため、load-wait + stdevスキャンで修正。

  16. 実際に拡大して確認し、サムネイルだけを信じない:検証時は必ずフル解像度の単一フレームを細かく確認(切り抜き/ぼやけたエッジ/サムネイルでは見えないカット)。座標を測定する際、元の画像にY/Xのグリッド線を引く。実戦例:ランドマークの下端が55pxカットされ、下半分がぼやけていたが、どちらもサムネイルでは見えず、拡大してグリッド線を引いて初めて発覚。

  17. インタラクティブゲームにオートプレイがない場合 → キーボード入力で自力でプレイ(MODE Aのサブモード):塊魂/アクションゲームはPlaywrightのkeyboard.down/upで操作(方向キーで転がす)。ゲームはロード後約10秒(precache/PREPARING)してから実際に開始することが多い:まずwait-until-START(ボタンの準備完了をポーリング)→ STARTをクリック → キー入力。盲目的なボットではアイテムを取得できず/成長できない場合がある(roll-formosa横モードで2cmで停止)——最も「映える」ウィンドウ(ボールが十分大きい/オブジェクトが多い)をキャプチャするか、次のshowcaseを使用する。

  18. ゲームプレイの撮影が難しい場合 → 自動showcase/ギャラリーを素材として使用:多くのアプリには「オブジェクト図鑑/プレビュー」の自動ページ(例:preview.html?city=X)があり、プレイせずにすべてのコンテンツをレンダリングするため、信頼性の高いモンタージュソースとなる(roll-formosa 20都市ランドマークはこれに該当)。これらの暗い背景のオブジェクトには完全な暗い背景を使用し、ぼかし効果は使用しない(ぼかしでゴーストが発生するため)。

  19. 「準備完了」画面は録画の後半に現れる:recordVideoはロード時から録画を開始するため、PREPARING→STARTの準備完了は後半に現れる。十分な長さを録画し(STARTまで待機 + さらに3秒停止)、後半から準備完了の部分をトリミングし、先頭からトリミングしない(loadingをキャプチャしてしまう)。

  20. モバイルの縦型ビューポートの方が良い場合が多い:ゲームのモバイルレイアウトはフルスクリーンになることが多く、動作が異なる場合もある(roll-formosaの縦型ゲームプレイではボールが成長するが、横型では停止する)。各アスペクト比でネイティブ再録画する際、どのビューポートが最も良いパフォーマンスを示すかを同時に確認する。

  21. コピーライティングは正確に:主人公/オブジェクトの紹介は性格/癖を使用し、外見は使用しない(catimeキャスト)。広告の「ニュース→画像」の一行はソースデータのidea/storyカラムから推測し、勝手に補完しない(間違ったニュースを関連付けると恥ずかしいことになる)。

  22. 予告編 ≠ チュートリアル、形式を間違えない:ビジュアルプロジェクト(ゲーム/アプリ)は15-20秒のクイックカット予告編(tease、注意を引く)が適切。しかしCLI/devツールをクイックカット予告編にすると非常に退屈で、チュートリアル感がない——〜60-90秒のナレーション付きチュートリアルにする:ペインポイントのフック→インストール→実行→出力の理解→高度なコマンドをステップバイステップで + edge-ttsでゆっくり説明(各ステップで十分に停止)+ ステップ字幕 → CTA、BGMは低く抑える(〜0.13)ナレーションをメインに。実戦例:WIPの最初のバージョンであるクイックカットは退屈だと評価され、ナレーション付きチュートリアル版に変更したところ、「理解できた、使える」という実感が得られた。ナレーションのタイミングはterm-playerの__STEPLOGを使用。term-playerは各ステップで画面をクリアする必要がある(GOTCHA 16と同様の問題:コンテンツが蓄積されると画面外に書き出され、説明されているコマンドが押し出される)。

  23. 最初に完全なREADME/--helpをスキャンして機能リストを作成し、その後スクリプトを作成する:ツールのチュートリアル/説明動画を作成する場合、最初の部分だけを読んで撮影を開始してはいけない——看板機能を見逃す可能性がある(WIPではdashboard.pyビジュアルダッシュボード、wip done、--root、install-skill、Codexサポート、see: プランニングファイル検出を見逃した)。まずすべての機能をリストアップし、次に実演するものを選択する。TTSでアカウント名/ハンドル/珍しいスペルを無理に読ませない(例:yazelinは実際には中国語名「亞澤林」のピンインであり、一字ずつ読むと奇妙)。画面にURLがすでにある場合は、ナレーションで「リンクは下にあります」と流す。つっかえる可能性のある冒頭の言葉を避ける(「最初に…」→「…は簡単です」)。edge-ttsは一般的な英語の専門用語(cargo/markdown/Claude Code)を読むのは問題ないが、Gemini Flashで一文確認する。

  24. MODE D 音楽動画:曲全体 + インタラクション、録音せず、後で元のファイルとmux。PlaywrightのrecordVideoは音声トラックもカーソルも含まない(手元に完全な音声トラックがない場合は〈本当の録音〉のセクションを参照)。したがって、(a) 音声は元のmp3を後でmuxする(音質が最も良く、xvfbのnull sinkの影響を受けない);(b) フェイクカーソルを注入する(画像を貼り付け、pointermoveに追従させる。録画専用でリポジトリにはコミットしない)。そうしないと、動画内の「マウスインタラクション」は、エフェクトが自動で動いているようにしか見えない。アクションのタイムラインは audio.currentTime のポーリングにバインドする(waitForFunction(t => au.currentTime >= X))、waitForTimeoutの累積は使用しない——録画中にフレームドロップが発生すると、壁掛け時計はドリフトするが、オーディオクロックはドリフトしない。

  25. タイミングの同期は画面内の時計に依存し、Node.jsのDate.now()には依存しない。webmの最初のフレームとnewContext()の間には不確実なラグがあり、さらにPlaywrightは最後に大量のフリーズフレームを追加する(実測では160秒のコンテンツが239秒のファイルとして書き込まれる)。ffprobeのdurationは長さとして使用できない。方法:プログレスバーの領域をcropし、フレームごとにスライダーのx位置を測定する(ピンクのピクセルが最も多いカラムをピークとして取得し、重心を計算——漂っているパーティクルも同色であるため、単純に平均を取ると引きずられる)、フレーム時間 → オーディオ時間の線形フィッティングを行い、音声の開始点を解く。3つの推定方法(スライダーフィッティング、歌詞の改行時刻、クリック時の壁掛け時計)をクロス検証し、中央値を採用;歌詞のラインはCSSトランジションにより〜0.1秒遅れるため、差し引くこと。仕上げ前に相互相関を使用して、成果物の音声トラックと元のファイルとの遅延を検証する(AACプライミングにより固定で約40msの遅延が発生するが正常)、そして「改行から少し離れた」3つのフレームを抽出して、正しい行が強調表示されていることを確認する。

  26. 縦型(9:16)の解像度向上のためにブラウザのズームに依存しない。recordVideoは「CSSサイズの画面」を指定されたフレームサイズに貼り付ける:viewport 405×720 に recordVideo.size 1080×1920 を指定した場合、結果は405×720のコンテンツが左上隅に貼り付けられ、残りは灰色で塗りつぶされる。deviceScaleFactorを使用してデバイスピクセルを増やそうとしても、PlaywrightのviewportシミュレーションはDSFを1にリセットする。代わりに--force-device-scale-factor + --window-sizeを使用するが、xvfbにはウィンドウマネージャがないため、--window-size、Browser.setWindowBounds、--kioskはすべてウィンドウを変更できない(innerWidthが501に固定されていることが確認される)。html{zoom:...}も効果がなく、メディアクエリは元の幅を読み取る。実用的な解決策:viewportをネイティブの1080×1920(DSF 1)に直接設定し、次に「拡大された縦型スタイル」を注入する(モバイル版のレイアウト + フォントサイズ/コントロールのサイズを比例して拡大、!importantでデスクトップ版のgridを上書きする)。録画専用のスタイルはリポジトリにコミットせず、HIDEと同じ場所に注入する。フォントのぼやけは拡大して確認する(GOTCHA 16):405から1080に引き伸ばされた文字のエッジはソフトだが、ネイティブレンダリングではハードである。

  27. 可能であれば「画面自体が語る」ようにする。インタラクティブな録画では、実演したい操作をタイムラインに編成し(クリック、ドラッグ、停止)、間に意図的に何も動かない期間を設け、自動非表示のUIが消えるのを待つ——連続して動き続けると、かえってユーザーは何が重要なのかわからなくなる。録画時はrecordVideoではカーソルが含まれないため、フェイクカーソルを注入する(画像をpointermoveに追従させる)。そうしないと、視聴者はエフェクトが自動で動いているようにしか見えない。

  28. タイミングのアンカーには「ホワイトフラッシュ」を使用し、「ブラックアウト」は使用しない。アイデア:「ページをまず全面黒にし、動画が実際に再生を開始したときだけ表示する。最初に明るくなったフレームをt=0としてスキャンする」——実測では成立しない:PlaywrightのrecordVideoは最初のフレームからキャプチャを開始するわけではなく、 録画全体で最も暗いフレームは24.4/255であり、識別可能な黒フレームは存在しない。代わりに、再生開始前に400msの全面白を挿入し、再生開始時にそれを削除する。「ホワイトフラッシュが終了した次のフレーム」をスキャンする。白は250に対し、コンテンツは24-37であるため、誤認識はない。 オフセットは全体で±0.05秒以内であり、ドリフトはないことを確認。

  29. metadata=printの出力はデフォルトでstderrに書き込まれる。ffmpeg -vf "...,metadata=print:key=X"の出力はstdoutではなく、 スクリプトがstdoutのみをキャプチャすると、常に空の結果を読み取り、「読み取り不能」を「フレームが黒である」と誤解釈する。:file=-を追加する必要がある。

  30. 内蔵httpサーバーは、ファイルを読み取ってからヘッダーを送信する必要がある。res.writeHead(200)をawait readFileの前に記述すると、 ファイルが存在しない場合でも200がすでに送信されており、catchで404を返そうとするとERR_HTTP_HEADERS_SENTに衝突する—— 真の原因(パスの誤り)がこの偽のエラーで隠蔽される。このリポジトリのcapture-app.mjsには元々このバグがあったが、修正済み。

  31. A/V同期の検証には識別力のある測定方法を使用する。「同じ時刻のフレームを抽出して相関を比較する」は理論上は妥当だが、スライド形式のコンテンツでは 数十秒間フレームが変わらないため、ネガティブコントロール(意図的に6秒ずらす)でも0.996の相関となり、検証として機能しない。実用的な方法:テンプレートにプログレスバーを配置する(幅 = currentTime / duration)。完成した動画のフレームからプログレスバーの幅を測定することで、プレーヤーの内部時間を読み戻し、出力時間と直接比較する。このプログレスバーは完成した動画の視覚要素としても機能するため、テストのためにのみ追加する必要はない。

  32. 静止スライドを使用した9:16のショート動画は、画面が静止している——そしてそれが測定可能である。スライド部分のフレーム間のグレースケール差は 実測でわずか0.072 / 0.092(凍結状態の対照は0.000)であり、ユーザーからのフィードバックは「平坦すぎる、パンチがない」というものだった。 しきい値は感覚で決めてはいけない。実際に出荷した9:16の作品を同じ尺度で測定し、使用可能な範囲を決定する。 neko-tensei音楽動画0.233、k-rider予告編1.045、battlefield-editor 1.293、 roll-formosa 1.912、catime広告3.965、catime予告編6.168(active_ratio 0.636 = クイックカット)。 トーク形式の名言ショート動画はクイックカットにすべきではない。目標範囲 0.30 から 1.05、active_ratio 0.10 から 0.40 ——このしきい値セットは無効:meanとactive_ratioはどちらもフレームレートで正規化されておらず、自身の作品を振り返って自身のしきい値を設定するのは循環論法である。現在の判断基準はpunch-score.pyの motion.slide.per_s ≥ 5.5 (1秒あたりのグレースケール変化量、mean × fps、フレームレートに依存しない唯一の値)、上限は設定せず、 危険な「フリッカー」はGOTCHA 35の輝度イベントでブロックする。根拠:ユーザーが「画面が死んでいる」と評価したv1は1.79と2.30であり、 一方、最も静かで出荷済みの作品であるneko-tenseiは5.82であり、これを通過する必要がある。

  33. 静止スライドのカメラワークでは、画角は画面自体から計測し、固定の比率でコードに記述してはいけない。960×720のスライドでは、実測で 250のスキャンラインにのみテキストが含まれ、残りは空白である。ハードコードされた上/中/下の切り抜きでは、ページ上部の小さなテキスト行や全面が空白の領域を切り取ってしまう。 方法:canvasで1フレームをキャプチャ → 各行のインク量(背景の中央値との差)→ 連続した帯に変換 → インク量が12%未満の帯を破棄 → 残った帯の左右の端が本文範囲SPとなり、カメラはどのように動いてもSPを切り取ってはならない。 計測時にサムネイルを使用しない:8倍に縮小(120×90)して計測すると、drawImageの縮小は面積平均ではなく、 細い線が直接消失する(本文右端の「的路。」が計測できず、SPが96px不足する)、さらに背景のグラデーションが全幅にわたるものとして扱われる。 しきい値をどのように調整しても一方を立てれば他方が立たず、元の解像度を直接使用すればこの問題は発生せず、「インク」は検収と同じ定義(背景の中央値を28上回る)を使用できる。960×720の全フレームスキャンはヘッドレスChromiumで数十ミリ秒で完了する。

  34. フレームの計測は「現在再生中のフレーム」に対して行い、事前にseekして計測してはいけない。「名言が語られているページ」のスライドを計測したい場合、 V.currentTime = 名言中盤 で drawImage を実行する——seekedはデコードの完了のみを保証し、フレームはまだ切り替わっていない。 実測では、quote-06全体が0秒目(前のページ)のレイアウトをフレーム計測に使用していた:SPが596ではなく696と計測され、 そのため「テキストを切らない保証」のあるフレームが最初から間違っており、フラッグシップキーワードの2.8秒間のフレームの半分で本文に欠けが生じた。 信頼性のある方法は再生中に計測すること:開始時は0秒目のフレームを使用(それがその時点で画面に表示されていたもの)、 その後、カメラワークが切り替わるたびに再度計測する。これにより、「一文が2ページにまたがる」問題も解決する——ページが切り替わった後の最初のカメラワークの変更で追従する。

  35. 輝度イベントには測定可能な上限を設定すべきであり、「ゆっくりやっている」と言うだけでは不十分。スライド部分のフレームごとのYAVGを計測し、 しきい値は相対10%に加えて絶対的な下限4/255を使用する——スライドの背景色Yは約20であり、絶対値のみをしきい値として使用すると、 明るさが9%増加しても検出できない;相対値のみを使用すると、暗部のノイズによって多数の誤検出が発生する。「最も密度の高い1秒間の発生回数」をカウントし、3回を上限とする (光感受性てんかんのリスクしきい値)。実測では完成品は2回/秒;ネガティブコントロールでは5 Hzフリッカーが26回/秒、 1 Hz ±6%の緩やかな脈動が6回/秒であり、どちらもブロックできる。このチェックはすでにcapture-short.mjsに記述されており、録画後に自動で表示される。

  36. ffmpegのeqフィルターは、brightness式がデフォルトで1回だけ評価される(eval=init)。「意図的なフリッカー」を作成するための ネガティブコントロールで:eval=frameを省略した場合、フィルターは実質的に機能せず、測定結果は完成品とまったく同じになる——優れた指標を「識別力がない」と誤って判断するところだった。ネガティブコントロール自体も、それが本当に壊れていることを事前に検証する必要がある。 そうしないと、ツールが2つの同一のファイルを区別できないことを証明するだけになる。

  37. フレーム間差分は「同期」に対して永遠に識別力がない。GOTCHA 31の内容を別のテンプレートで再度検証:words.jsonを 意図的に6秒ずらしても、スライド部分のフレーム間差分はまったく同じ0.718(words.jsonを一切参照しない)。 変化するのは「字幕が切り替わる時刻 vs 字幕領域の最大フレーム間差分時刻」のライン(0.091 → 0.567)。 動的指標は動的であることしか主張できず、同期は別途測定する必要がある。

  38. 字幕の分割において、「隣接する2つのキーワード」が同じブロックに結合される。分割は「収まらない」場合にのみブロックを切り替え、 別のキーワードに切り替わった時点でも収まっている場合が多い——実測では「很大的問號」「沒有被驗證過」の間に他の文字がなく、 11文字の1つのブロックとして結合され、ブロック全体が金色に表示され、2つの強調ポイントが1つに縮小された(ハイライトブロックが2つだけになった)。 修正方法は次のルールを追加する:k >= 0 かつ k !== 前の文字のk の場合、強制的に新しいブロックを開始する。修正後、ハイライトブロックは3つになり、 最大ブロックサイズは11文字から6文字に減少した。注意:punch-score.pyにも同じ分割ロジックが移植されているため、一緒に修正すること。

  39. 実行時にズームが密かに拡大されるため、フレームの幅を計算する際にはその余裕を差し引く必要がある。緩やかなズームイン(+3.5%)とキーワードのズームイン (+5%)を合わせると8.7%になる:「ちょうどいい」フレームの画角が、ズームインによって本文を画面外に押し出し、特にキーワードの数秒間に発生する——最も問題が発生してはならない場面である。2つのことを行う必要がある:(1) フレーム設定時 z = 幅の許容値 / 1.087、 (2) 各フレームで再度クランプする(z ≤ 幅の許容値、cxがSPの両端をフレーム内に収めるようにクランプする)。 不変条件は、フレームが画面から消える瞬間に成立している必要があり、「フレーム設定時に計算が正しかった」というだけでは不十分である。

  40. スライド自体のヘッダーとフッターがフレームの中央に配置されると、左右が必ず切り取られ、それが本文の切り取りとして誤ってカウントされる。 ヘッダーの小さなテキスト行(x 24 〜 932)とフッターのプログレスバー(x 0 〜 759)はどちらも本文範囲を超えて突き出ている。 カメラがズームインすると、それらは画面の両側からはみ出す。検収では、スライド部分の上下をそれぞれ10%ずつヘッダー/フッターとしてトリミングするが、 カメラワークが変更されると、家具がそのラインの内側に落ち、その結果、本文が切り取られたとカウントされる。 修正方法:「インクがSPの外側に及んでいる」すべての行を家具としてマークする(判断基準は「全幅にわたっている」ではない——フッターのプログレスバーは インクのしきい値の下では幅が79%しかなく、全幅で判定すると見逃される)。次に、カメラの中心を制限し、家具が 画面の上下端から10%以内にのみ存在するか、完全にフレームアウトするようにする。境界には数ピクセルの余裕を持たせる:ちょうど10%のライン上にある場合、 サンプリングの丸め誤差により、家具の最後の1〜2行が本文側に落ちる可能性がある(実測ではまだ15フレームあり、すべて81、82行目にあった)。

  41. フレーム設定の問題を診断するには、まずアルゴリズムをオフラインで再実行し、パラメータを変更して試行錯誤してはいけない。一度の録画は40秒で、 しきい値を盲目的に試すと「調整するほど悪化する」という状況に陥る:実測では、緩やかなズームインとキーワードのズームインをオフにした(これが主因だと思った)ところ、 テキストの切り取りが102フレームから189フレームに増加した。代わりに、ffmpegで同じ検出フレームを抽出し、Pythonで同じband/SPロジックを実行すると、 10秒で「ブラウザが計測したSPが0秒目のページと同じ」であることがわかり、主因が即座に明らかになった。 また、フレーム設定の前提条件をログに出力する(SP、幅の許容値のz、家具の位置)——画角が崩れたときに、 それらの3つの数値を見れば、幅の計測ミスか、通路が狭められたのかがわかる。

  42. 成果物の数値は「コンテンツ由来」と「フレーム由来」に分けて報告し、後者には必ずテンプレートのmd5を添付する。 コンテンツ由来のもの(フック、強調表示、ハイライトブロック、字幕ブロック、ミュート、話速)はwords.jsonにバインドされており、そのmd5が変わらなければ 数値は1ビットも変わらない。フレーム由来のもの(フレーム間差分、テキストの切り取り)は録画時のテンプレートのバージョンに対してのみ有効である。 両方を混ぜて報告すると、後でドキュメントに従って再録画した人が一致しない数値を手にすることになる:実測では、同じクリップを テンプレート変更前と変更後で録画した場合、1秒あたりの変化量は14.80から27.19に変化——1.8倍の差であり、コンテンツ由来の6つの項目は1つも変わっていない。 そのため、相手は存在しない回帰を調査するために1ラウンドを費やすことになる。capture-short.mjsは現在、毎回の録画時に テンプレート short-9x16.html md5 …を表示し、数値を報告する際にそれを添付する。

  43. 録画自体にブレがあるため、判断基準はそのブレを吸収できるものでなければならない。テンプレートと入力をmd5で固定して4回連続で録画した場合、 スライド部分の1秒あたりの変化量は21.27 / 21.33 / 20.82 / 21.3

関連スキル