Demo Video Subtitler
問題
スクリーンレコーディングによる製品デモは、製品自体とは無関係な3つの理由で視聴者に伝わりません。UIテキストが間違った言語である、文字が小さすぎて読めない、そして動画にナレーションがない(無音録画)ことです。視聴者はクリックが発生しているのを見ても、その理由を理解できません。字幕はそれを解決します——ただし、それらが複雑なUI上でも読みやすいスタイルで、デモ中の部分を隠さない場所に配置され、画面切り替えで途切れないタイミングで表示されている場合に限ります。
このスキルは、困難な方法で発見すると何時間も費やすことになる、明白でない障害モードを含む完全なパイプラインです。
使用するタイミング
- 「このデモ動画に字幕を追加して」/「この録画に韓国チーム向けのキャプションを付けて」
- 無音のスクリーンレコーディング(
.mp4、音声ストリームなし、またはBGMのみ)で、ストーリーをキャプションテキスト自体で伝える必要がある場合 - 英語のみの製品デモを非英語圏の視聴者に見せる場合
- 既存の字幕があるが、中央下部に固定されていてデモパネルを覆っている場合——フォーカス追従型の配置が必要
- 動画に合わせてプレゼンターが読むための読み上げ台本を生成する場合(「関連ユースケース」参照)
このスキルを使用しないでください:合成音声による吹き替え(それはTTSパイプライン)、またはプラットフォーム上に既に存在するキャプションの抽出。
インストール(1コマンド)
scripts/bootstrap.sh --dry-run # 確認のみ:不足しているものを正確に表示
scripts/bootstrap.sh # 不足しているものをインストール(1回確認あり)
scripts/bootstrap.sh --yes --lang ja # 無人実行、日本語フォントも準備
bootstrap.sh は冪等です——既に準備が整っているマシンでは何もしません。プラットフォームを検出し、適切なインストーラー(Homebrew / apt / dnf / pacman / winget / scoop)を選択し、パッケージマネージャーが利用できない場合は静的ffmpegを ~/.local/bin に配置します。キャプションを焼き付けるセッションの最初のアクションとして実行してください。スキルにはインストール時のフックがないため、このスクリプトがインストーラーとなります。
前提条件
最初に組み込みチェックを実行します——マシンを検査し、不足しているものを正確に表示します。
scripts/doctor.sh "Hiragino Sans GB" # 使用予定のCJKフォントファミリーを指定
チェック内容と、不足している場合のインストールコマンド:
| 要件 | 理由 | インストール方法 |
|---|---|---|
libass 付きでビルドされた ffmpeg | .ass キャプションを焼き付ける(-vf ass=...) | macOS brew install ffmpeg; Windows: gyan.dev full ビルド; Debian apt install ffmpeg |
ffmpeg ビルドの --enable-fontconfig | フォントファミリー名で解決する。これがないと無言でフォールバックフォントになる | 上記と同じビルドに含まれている |
| システムが解決できるCJKフォントファミリー | キャプションテキストそのもの | macOS: Hiragino Sans GB(プリインストール)· Windows: Microsoft YaHei(プリインストール)· Linux: apt install fonts-noto-cjk |
python3(任意) | export_cues.py のみ。標準ライブラリのみ、pipインストール不要 | macOS: xcode-select --install; Linux: apt install python3 |
bash, zip(任意) | スクリプト、および配布用のパッケージング | ほぼどこでもプリインストール済み |
Node、ImageMagick、pipパッケージ、APIキーは不要です。視覚認識可能なモデル(エージェント自身)がフレームを読み取ります——それはインストールするものではありません。
厄介な2つの環境上の注意点:
ffmpegをPATHに通す、またはスクリプト用にその場所をエクスポートする:FFMPEG=/path/to/ffmpeg scripts/burn.sh ...。- Windows:libassはフォント解像にfontconfig/DirectWriteを使用します(ビルドによります)。キャプションが間違った書体でレンダリングされる場合は、fontconfigをインストールし(
scoop install fontconfigまたはMSYS2)、fc-match "Microsoft YaHei:bold"で再確認してください。
パイプライン
上から下へ作業します。フェーズ2をスキップしないでください——以降のすべてのフェーズはUIジオメトリを知っていることに依存します。
フェーズ1——インベントリ(2分)
scripts/probe.sh INPUT.mp4
記録:再生時間、解像度、fps、ビットレート、音声ストリームの有無。スクリプト内で音声チェックを実行します(平均音量+無音マップ)。
音声チェックからの判断:
| 音声の状態 | 字幕の種類 |
|---|---|
| 音声ストリームなし | ナレーション字幕——あなたがストーリーを執筆する(「何が起きているか/なぜそれが重要か」) |
| 音声はあるが発話なし(音楽のみ) | 同上。10秒間のサンプルを文字起こしして確認:大きな音声で空またはほぼ空を返すASRツールは、音楽であって音声ではないことを意味する |
| 発話あり | 会話字幕——まずタイムスタンプ付きで文字起こしし、その後クリーンアップする。コピーを創作しない |
フェーズ2——視覚分析とジオメトリ(人々がスキップするフェーズ)
2 fpsでフレームを抽出します(60秒のクリップ→約120フレーム。まず4フレームごとに全体の流れを確認し、その後テキストにズームイン):
scripts/split_frames.sh INPUT.mp4 frames/ 2
キャプションを1つ書く前に、3つの成果物を作成します。
-
ビートシート ——
| 時間 | 画面上の変化 | 画面上のテキスト(原文まま) |。「ビート」とは任意の変化:通知の表示、パネルのスライドイン、ボタンのクリック、スクロール、ページ切り替え、トランジション。トランジションの種類(白フラッシュ、カット、黒)をメモする——キャプションはそれらをまたいではならない。 -
UIテキスト、原文まま、ソース解像度から読み取る。 ダウンスケールされたプレビューフレームは小さなUIテキストをぼかし、誤って書き写すことになる。元のファイルからクロップする:
ffmpeg -i IN.mp4 -ss 12.5 -frames:v 1 -vf "crop=W:H:X:Y,scale=2*iw:-1" out.jpg(フレーム正確な抽出のために-ssは-iの後。トラップ参照)。 -
ジオメトリマップ ——「ライブ」パネル(デモ対象)のピクセル境界をソースピクセルで測定する。目視ではなく、実際に測定する。
scripts/find_edge.sh INPUT.mp4 30 2880 300 # t=30s、x=2880から300px幅のストリップ次に返されたストリップ画像を読み取り、パネルの左端がストリップ内のどこに収まるかを確認する。そのオフセットをストリップのxに加算して絶対的な端を得る。レイアウトが変わる場合は、他のシーンでも繰り返す(例:後でチャットパネルが開くなど)。
ジオメトリマップに従うルール: キャプションはデモパネルに隣接する自由領域に配置され、決してその上には置かれない。フェーズ5参照。
フェーズ3——キャプション原稿の作成
2つのルールがすべてを支配します。
- 1つのキューにつき1つのアイデア。 最大2~3行の短い行。キューは段落ではなく文です。
- 読む速度の予算 CJK:毎秒3.5~5.5文字が快適。6以上は慌ただしく、3未満は間延びします。ラテン文字:毎秒12~18文字。計算し、推測しないでください。
scripts/export_cues.pyは.assファイルができればキューごとの密度を表示します。その数値に照らして繰り返し調整します。
キューのタイミングルール:
- キューは視覚的なビートが着地するのと同時に開始します。通知が1.0秒にスライドインする場合、キューは1.0秒に開始します。
- キューはトランジション(白フラッシュ/カット)の前に終了し、少なくとも0.8秒の無音時間を挟みます。トランジションをまたぐキャプションはグリッチのように読み取られます。
- 適切な間隔を空けます。1分あたり3~5秒の空白があると、動画に呼吸が生まれます。壁一面のキャプションは視聴者を疲弊させます。
- ナレーション字幕の場合、キャプションには理由を入れ、仕組みは入れません:「エージェントが自発的に到達した」は「右上隅に通知が表示された」よりも優れています。
ここで一旦停止し、コピーを承認してもらいます。 テキスト承認→スタイリング。スタイリング後にテキストが却下されると、無駄な再レンダリングが発生します。
フェーズ3b——キャプション言語の選択
パイプラインは言語に依存しませんが、スタイリング前に言語ごとに3つのことを決定する必要があります。
1. フォントファミリー ——プラットフォームが解決できるものを選択します(scripts/doctor.sh <lang> で確認):
| キャプション言語 | macOS | Windows | Linux |
|---|---|---|---|
| 中国語 (zh) | Hiragino Sans GB / PingFang SC | Microsoft YaHei | Noto Sans CJK SC |
| 日本語 (ja) | Hiragino Sans | Yu Gothic / Meiryo | Noto Sans CJK JP |
| 韓国語 (ko) | Apple SD Gothic Neo | Malgun Gothic | Noto Sans CJK KR |
| ラテン文字 (en/fr/de/es…) | Helvetica Neue | Segoe UI | Inter / DejaVu Sans |
2. 読む速度 ——予算はスクリプトに依存します。scripts/export_cues.py は自動的に適切なものを選択し(--lang で上書き可能)、超過したキューにフラグを立てます。
| スクリプト | 快適 | 速すぎとしてフラグ |
|---|---|---|
| 漢字/仮名 (zh, ja) | 毎秒3.5~5.5文字 | > 6.0 |
| ハングル (ko) | 毎秒4.0~6.0文字 | > 6.5 |
| ラテン文字 | 毎秒12~18文字 | > 19 |
3. 行の折り返し ——中国語と日本語には単語間のスペースがありません。句読点(U+FF0C, U+3001, U+3002, U+FF1A)で区切り、製品名は決して分割しません。韓国語とラテン文字はスペースで区切ります。韓国語の助詞は名詞に付けたままにします。行が収まらない場合は、フォントを縮小する前に折り返します——小さな書体は、行を追加するよりも可読性を大きく損なう可能性があります。
4. スクリプトごとの行幅 ——グリフの幅はスクリプトによって異なるため、同じ Fontsize で異なる行長になります。行の予算は floor(available_px / (fontsize * k)) で計算します。ここで k は、漢字で約1.0、ハングルと仮名で約1.05~1.1(これらは幅広に設定されます)、ラテン文字で平均0.5です(単語によって変わります)。
4K、Fontsize 165 で測定:13ハングル文字≈2,150 px(2,450 pxのスロット内で安全)。一方、18文字≈2,970 px——これはオーバーフローし、行がフレームの左端からはみ出します。行が収まらない場合は折り返し、収まるようにフォントを縮小しないでください。
二言語キャプション(原文+翻訳):両方の行が短い場合のみ。2つの言語の行は読む負荷を倍増させ、デモではUIと競合します。クライアントが別途要求しない限り、ファイルのバージョンごとに1言語を推奨します。必要な場合は、原文を上に、翻訳を下に重ね、ブロック全体を翻訳の読む速度に対して予算を組んでください。
右から左へのスクリプト(アラビア語、ヘブライ語):libassはRTLをサポートしていますが、シェーピング品質はビルドのharfbuzzに依存します。コミットする前にプレビューフレームをレンダリングします——文字が正しく結合し、配置アンカーが意味をなすことを確認します。
フォントの太さはスクリプトによって異なります。 日本語と韓国語のフォントファミリーは、同じ Fontsize でも中国語のものよりも視覚的に大きく表示される傾向があります。1つのプレビューフレームで確認し、ルールではなく目視で調整します。
フェーズ4——スタイル(2層のグロー)
単一のASSスタイルでは「白い文字に太い色付きの輪郭とソフトな色付きハロー」を生成できません——Outline は1つのリングしか描画しません。2つのレイヤーでキューごとに2つのイベントを使用します。
| レイヤー | 塗りつぶし | 輪郭 | ぼかし | 目的 |
|---|---|---|---|---|
| 0 | 透明 (&HFF000000) | 太い、半透明の紫 | \blur 20–30 | ハロー |
| 1 | 白 | 細く、不透明な紫 | なし / \blur1 | 読みやすいテキスト |
開始点(assets/caption-template.ass を参照)。色は &HAABBGGRR です——アルファ 00 は不透明、FF は完全に透明で、バイト順は BGR です。
紫色 RGB(124,46,240) → &H00F02E7C
ハロー、35%不透明 → &H55F02E7C
PlayResでスケーリングし、感覚で行わない:4Kレンダリングでは fontsize 180 / outline 16 / halo 28 / blur 24 を使用。1080pで同じ見た目は 80 / 7 / 12 / 12。PlayResX/Y を出力解像度に設定し、ScaledBorderAndShadow: yes を維持します。
フェーズ5——フォーカスに従った配置
中央下部はデモでは間違いです。下部は通常UIであり、デモパネルは可視でなければならない唯一のものです。各キューを表示中のものに隣接する自由領域にアンカーします。
| このキューのフォーカス | アンカー | 理由 |
|---|---|---|
| サイドパネル/チャットパネルが開いている | \an6\pos(<panel_left - 40>, <panel_vertical_middle>) | 右端がパネルに密着し、ブロックは左方向に成長。幅が変わっても再計算不要 |
| 右上のトースト/通知 | \an3\pos(<toast_right>, <toast_bottom + 80>) | 右下アンカーがブロックをトーストのすぐ下に配置 |
| 全幅ページ、パネルなし | \an5\pos(<visual_center_x>, <y between two content blocks>) | 中央揃えだが、計測された隙間に配置——幾何学的中心ではない |
| 大きな組み込みタイトルがあるタイトルカード | \an5\pos(center_x, BELOW the title) | 幾何学的中心は動画自身の見出しと衝突する |
書き込む前に最大行幅を計算します:max_line_px ≈ fontsize × characters。行が自由領域を超える場合は、折り返します(1つのキューのためだけにフォントを縮小しない)——句読点で分割し、製品名は分割しないでください。
右アンカーのブロックでは、平坦な背景に数十ピクセルはみ出しても問題なく、意図的に見えます。デモパネルにはみ出してはいけません。
アンカーをフレーム端から離して配置します。 輪郭とハローは、グリフボックスからおおよそ outline + blur ピクセル(4Kで outline 15 / blur 22 の場合、約40 px、さらにアンチエイリアス)拡張します。したがって、右端から50 pxに配置された \an3 アンカーはハローがクリップされ、壊れた単語のように読み取られます。アンカーはフレーム内部から少なくとも150 px離して配置し、プレビューフレームで結果を確認してください——クリッピングは .ass ファイルでは見えず、画面で明らかです。
フェーズ6——焼き込み
scripts/burn.sh INPUT.mp4 subs.ass OUT.mp4 [crf]
これは -i IN -vf "ass=subs.ass" -c:v libx264 -crf 17~18 -pix_fmt yuv420p -movflags +faststart で、元の音声トラックを保持します。ハード字幕はデモに適切なデフォルトです——すべてのプレイヤー、アップロード、画面共有で生き残ります。要求された場合のみ .ass/.srt を一緒に提供します。
フェーズ7——検証
まず安価な静止画をレンダリングし、次にエンコードされたファイルを検証します。
scripts/preview_frame.sh INPUT.mp4 subs.ass 16.0 /tmp/check1.jpg
各フレームでチェック:デモパネル上のテキスト、フレーム外のテキスト、予期しない行折り返し、終了すべきだったキュー、明るい背景でベタになってしまったハロー。次に、完成したファイルから3フレームを再抽出し(字幕は焼き込まれ、品質は最終版)、再確認します。
トラップ(それぞれ実際に発見に時間がかかったもの)
-ssを-iの前に置くとassフィルターが無効になる。 入力シークはタイムスタンプをゼロにリセットするため、フィルターはすべてのフレームで t≈0 の字幕状態をレンダリングします。結果:動画に字幕がまったく焼き込まれず、ffmpegはエラーを出力しません。常に-ssを-iの後(出力シーク)に置くか、-copytsを追加します。バンドルされているスクリプトは既にこれを行っています。- プレビューフレームの座標は嘘をつく。 1280×720にスケーリングされたが、あなたに表示されるフレームは……(翻訳のため省略)