コードでナレーション動画を作成する
概要
1本の動画 = 確定した台本 → 文ごとのTTS → 文ごとのASR検証 → タイムライン → HTMLアニメーション → フレームキャプチャ → ffmpeg合成。
核心原則:タイムラインは音声に基づく。 まず各文をTTSで生成し、検証し、各字幕セグメントの実際の開始・終了時間を測定します。その後、すべての画面アニメーションをこれらの時間に同期させます。画面に秒数をハードコードせず、ナレーションを変更したらタイムラインを再実行するだけです。レンダリングは、ページをフレームごとに __seek(t) でキャプチャし、結果は確定して再現可能です。
適用:解説・チュートリアル(2~5分)、製品プロモーション動画(20~40秒)、字幕とチャプター付きの知識系動画。 不適用:実写出演の編集、実写素材が必要な動画、純粋な音楽MV。
フロー
各ステップにはチェックポイントがあり、前のステップが完了していなければ次のステップに進みません。
| # | ステップ | 成果物 | チェックポイント | 参考 |
|---|---|---|---|---|
| 0 | 初期化 | 作業ディレクトリ | bash scripts/init.sh <dir> が成功 | 後述 |
| 1 | テーマ選定、検証、台本 | script.md、audio/make_script.py | 事実を検証済み(実験が必要なものは先に実行);ユーザーが台本に明示的に同意してからナレーション開始 | references/script.md |
| 2 | ナレーション | audio/raw/NN.wav → audio/clips/NN.wav(1.3倍速) | 各文をASRでチェック、問題があれば再録音 | references/voice.md |
| 3 | タイムライン | build/narration.wav、build/timeline.json | 字幕セグメントが音声と一致 | references/timeline.md |
| 4 | 素材 | 実際の画面キャプチャ、実際のターミナル出力 | ナレーション内容と一致。サンプルドメインにはexample.comを使用 | references/capture.md |
| 5 | 合成 | comp/index.html | コマ撮りとコラージュを1枚ずつ確認 | references/composition.md |
| 6 | レンダリング | dist/video.mp4 | 長さ=タイムライン、ラウドネス -14 LUFS | references/render.md |
| 7 | カバー・コピー | dist/cover-bili.png、dist/cover-douyin.png、dist/copy.txt | カバーが各プラットフォームのトリミングセーフゾーン内にある | references/publish.md |
| 8 | 納品・クリーンアップ | ユーザーに送付;ソースファイルをアーカイブ | ユーザーが保存を確認してからクリーンアップ | 後述 |
初期化
bash scripts/init.sh ~/work/<プロジェクト>/video-<エピソード名> で、template/ を作業ディレクトリにコピーし、scripts/ を tools/ にコピーし、playwright-coreをインストールします。テンプレートはそのまま実行可能な30秒のデモです。作業開始前に以下のサンプルコンテンツを置き換えてください:
script.md、audio/make_script.py:今回の台本。チャプターはmake_script.pyのCHAPTERSでのみ定義し、チャプター間のポーズ、チャプターバー、チャプタータイムスタンプはすべてそこから導出します。audio/direction.txt:TTSディレクションプロンプトの【シーン】。asr-prompt.txt:今回の用語集(正式な表記、必要に応じて読み方付き)。comp/index.htmlのシーンとアニメーション、comp/cover-*.htmlのカバー内容。
よく使うコマンド
作業ディレクトリ内で実行:
| コマンド | 効果 |
|---|---|
python3 audio/make_script.py | audio/script.json と audio/lines.json を生成 |
python3 tools/tts.py audio/lines.json audio/raw | 文ごとにナレーションを生成;テキストが同じ場合は既存の音声を再利用。台本を変更したらそのまま再実行 |
bash tools/speed.sh audio/raw audio/clips 1.3 | ピッチを変えずに速度変更 |
python3 tools/asr.py audio/clips --en | 文ごとに認識:プレーン / 用語プロンプト / 英語モード |
python3 tools/takes.py 3,7 3 | 3、7番目の文をそれぞれ3テイク録音してスコアリング |
python3 tools/takes.py audio/var.json 2 | 同じ文の複数の言い回しをそれぞれ2テイク録音 |
python3 tools/tts.py audio/lines.json audio/raw --adopt 3 audio/takes/03-0-1.raw.wav | 特定のテイクを採用 |
python3 tools/build_audio.py audio/clips build audio/script.json | ナレーションを連結し、タイムラインを生成 |
python3 tools/align_chunks.py audio/clips build/timeline.json audio/script.json | 字幕セグメントを実際の発音にアライメント |
node tools/render.mjs --timeline=build/timeline.json --stills=2,8.5,20 | コマ撮りを build/stills に出力(毎回空にしてから) |
bash tools/contact.sh build/stills build/sheets | 2x2コラージュ、Readで画像確認 |
bash tools/render-par.sh build/timeline.json build/narration.wav dist/video.mp4 8 | 8プロセスで並列レンダリングと音声ミックス |
node tools/render.mjs --html=comp/cover-4x3.html --width=1440 --height=1080 --png=dist/cover-bili.png | カバー画像出力 |
python3 tools/bili_kw.py "nginx 配置ssl证书" "宝塔 https" | テーマ調査:B站のキーワード上位6件の再生数 |
依存関係:ffmpeg、Node 18+、ローカルのGoogle Chrome、Python 3 + requests + openai-whisper(small モデル)。TTSはXiaomi MiMoを使用、APIキーは環境変数 MIMO_API_KEY または MIMO_ENV_FILE が指すファイルから読み取り、どちらもなければユーザーに問い合わせます。他のTTSに変更する場合は、各文が1つのWAVファイルになることを保証してください。
最も間違いやすいポイント
- 承認なしでナレーションを生成する。 台本はまずユーザーに送信し、「続けて」「開始」「OK」を同意とみなします。発音のために言い回しを変えた場合は、納品時に1つずつユーザーに伝えます。
- 字幕とナレーションを同じテキストで書く。 字幕は人が見るもの(
Nginx、443、1.3)、ナレーションはTTSが読むもの(Engine X、四四三、一点三)です。make_script.pyではP(字幕, 読み方)で分けて記述し、各セグメントの読み方を連結すると全文のTTSテキストと一致するようにします。 - 全体のミックスだけを聞く。 全体認識では英語の用語を誤認識するため、必ず文ごとに認識します。テイク選定ではプレーンスコアと
--enの結果のみを使用します。用語プロンプト付きのprimeは、読み間違いも「修正」してしまうため、判定ルールはvoice.mdを参照してください。 - 秒数をアニメーションにハードコードする。 最初の文以降のアニメーションはすべて
T(文, セグメント)で記述します。速度変更や再録音で時間は変わります。 - ページの先頭が空のまま。 カードが表示されるまで2~3秒間空で、止まっているように見えます。コンテンツはそのチャプターの最初の字幕セグメントで表示されるようにします。
- 同じ要素に複数の
fill: bothアニメーションや重複したID。 WAAPIはエラーを報告せず、要素が完全に消えたり、誤った状態で停止したりします。詳細はcomposition.mdを参照。 file://でページを開いてレンダリングする。 CSSマスクなどが機能しません。render.mjsは内蔵のローカルHTTPサーバーを起動します。- カバーがプラットフォームでトリミングされる。 B站は1枚の画像を4:3と16:9で同時にトリミングし、Douyinは9:16を3:4にトリミングします。コンテンツはセーフゾーン内に配置します。
- ルール違反の誘導。 画面、カバー、コピーに実際のサイトURL、QRコード、連絡先を入れず、「見てみて」「検索して」と書きません。
example.comなどの予約済みサンプルドメインは、デモのインターフェースやターミナルで使用できます。クロスプラットフォーム公開の動画では、特定のコンテンツプラットフォームを例示しません。プロモーション動画では価格に触れません。 - 成功を確認してから結果を見る。 進捗メッセージは、コマンドが完了し出力を確認した後に送信します。
納品とクリーンアップ
- 納品:完成動画、B站用4:3カバー、Douyin用9:16カバー、
dist/copy.txt(タイトル、タグ、概要、チャプタータイムスタンプ)。B站動画はデフォルトでDouyinなどのプラットフォームにも配信されるため、縦型カバーとDouyin用コピーも合わせて出力します。ユーザーが明示的に1つのプラットフォームのみと指定した場合は、そのセットのみ出力します。発音のために変更した言い回しをすべてリストアップし、自信のないタイミングポイントはユーザーに確認を依頼します。 - ファイルがチャットツールのサイズ上限(一般的に30MB)を超える場合は、まず
crf 22のプレビュー版を送信し、高画質版の場所を説明します。大容量ファイルの送信がタイムアウトして再送した場合は、ユーザーが2通受け取る可能性があることを伝えます。 - ユーザーが素材の保存を確認した後:再利用可能なソースファイル(
comp/と使用した素材、audio/make_script.py、バリアント仕様、script.md、copy.txt、direction.txt、asr-prompt.txt、キャプチャスクリプト、lab/)をユーザーのテンプレートライブラリにアーカイブします(取り決めがない場合は保存場所をユーザーに確認、例:~/work/<プロジェクト>/video-template/<エピソード名>/)。diff -rで1つずつ比較して一致を確認してから、作業ディレクトリ、一時的なブラウザプロファイル、コンテナ、開発サーバー(PIDで強制終了、事前にコマンドラインを確認)、およびキャプチャのためにのみインストールしたフロントエンド依存関係を削除します。whisperモデルキャッシュとDockerイメージは保持します。