TiAb Review Plugin の操作解説動画(YouTube 公開用)を、Playwright による自動収録 + VOICEVOX による自動音声合成 + ffmpeg による自動合成で作るためのパイプライン。要件定義は REQUIREMENTS.md を参照。
video/
├── REQUIREMENTS.md 要件定義書
├── README.md 本書
├── scenes/ Playwright シーンスクリプト(video/scripts/record.mjs が読み込む)
├── narration/ ナレーション原稿(日本語、チャプターごとに1ファイル)
├── subtitles/ 英語字幕ソース(narration と対になるチャプターごとに1ファイル)
├── assets/ サムネイルテンプレート等の静的素材
├── scripts/ パイプライン本体(Node.js ESM, .mjs)
│ ├── config.mjs 共通設定(パス・解像度・VOICEVOX/ffmpeg 接続先 等)
│ ├── record.mjs 収録(シーン → video/build/scenes/<NN-slug>/)
│ ├── tts.mjs 音声合成(原稿 → video/build/audio/<NN-slug>/)
│ ├── assemble.mjs 合成(build/ 一式 → 最終動画・チャプター・字幕・説明文・サムネイル)
│ ├── setup.sh 環境セットアップ(冪等)
│ └── lib/ パーサ・ffmpeg ラッパー等の共通ユーティリティ
└── build/ 生成物(git 管理外。.gitignore 済み)
- Node.js 18 以上(
package.jsonのengines参照) - Linux + xvfb(拡張機能を読み込んだ Chromium
をヘッド付きで動かして収録するため。Linux の収録コマンドは
xvfb-run経由で実行する) - Python3 + py7zr(
pip install py7zr) (VOICEVOX エンジンの配布形式が 7z のため、video/scripts/setup.shの展開に使用) - ネットワーク到達性(初回セットアップ時のみ。Playwright の Chromium、ffmpeg、VOICEVOX エンジンをダウンロードする)
Windows では xvfb は不要で、node video/scripts/record.mjs をそのまま実行する。
video/scripts/setup.sh は Linux 向けなので、VOICEVOX エンジンは Windows 版
(voicevox_engine-windows-cpu-<版>.7z.001)を video/tools/ に展開し、
展開先で run.exe --host 127.0.0.1 --port 50021 を実行して起動する。
ffmpeg / ffprobe は PATH にあれば使われる。
# 0. 環境セットアップ(初回のみ。以後は冪等なので再実行しても安全)
npm run video:setup
# 1. デモ用拡張機能ビルド(Playwright 収録用。実データ・実アカウントを使わない)
npm run build:demo
# 第9章が映す Web 版デモ(dist-web-demo/ に出力)
npm run build:web:demo
# 2. シーン収録(Playwright + xvfb。LANGUAGE=ja を必ず付与する)
LANGUAGE=ja xvfb-run -a -s "-screen 0 1920x1080x24" npm run video:record
# 3. ナレーション音声合成(VOICEVOX エンジンが起動していること)
npm run video:tts
# 4. 最終合成(動画結合・チャプター・字幕・説明文・サムネイル生成)
npm run video:assemble生成物は video/build/ 配下にまとまる(後述)。
収録に使うビルドはリポジトリ直下の dist-demo/ と dist-web-demo/ に出力する
(どちらも git 管理外。配布しない)。
video/scripts/setup.sh が video/tools/ 配下に展開した場合、config.mjs は
環境変数 → PATH 上のコマンド の順でしか自動解決しないため、video/tools/ に置いた
バイナリを使うときは明示的に環境変数を指定する(setup.sh 実行時にも案内が表示される)。
export FFMPEG_PATH="$(pwd)/video/tools/ffmpeg-master-latest-linux64-gpl/bin/ffmpeg"
export FFPROBE_PATH="$(pwd)/video/tools/ffmpeg-master-latest-linux64-gpl/bin/ffprobe"既定では /opt/pw-browsers/chromium があればそれを使い、無ければ Playwright 標準の解決
(開発機で npx playwright install chromium 実行後に使われるパス)にフォールバックする。
別の場所を明示したい場合は PLAYWRIGHT_CHROMIUM_PATH を設定する。
既定は http://127.0.0.1:50021、話者は四国めたん(ノーマル・話者ID 2)。変更する場合は
VOICEVOX_URL / VOICEVOX_SPEAKER を環境変数で指定する。
機能追加時などにチャプター1本だけを更新したい場合、対象シーンの video/scenes/NN-slug.mjs
(と、必要なら video/narration/NN-slug.md / video/subtitles/NN-slug.md)だけを編集し、
以下のように収録対象を絞って再実行する。
LANGUAGE=ja xvfb-run -a -s "-screen 0 1920x1080x24" \
node video/scripts/record.mjs 05-ml-screening
npm run video:tts -- 05-ml-screening # 原稿を変更した場合のみ(未変更のcueは自動でスキップされる)
npm run video:assemble # 常に build/ 全体から再生成する引数はシーンファイルのフルネーム(05-ml-screening)でも、番号部分(05)だけでも一致する。
引数を省略すると、それぞれ video/scenes/ video/narration/ 配下の全ファイルが対象になる。
tts.mjs は原稿本文と話者IDのハッシュを video/build/audio/<key>/index.json に保存しており、
変更の無い cue は再生成をスキップする。assemble.mjs は毎回 video/build/ 全体から作り直す
(各シーン mp4・最終 mp4・チャプター・字幕・説明文・サムネイルをすべて再生成する)。
1チャプター = 1シーンスクリプト(video/scenes/NN-slug.mjs)+ 1ナレーション原稿
(video/narration/NN-slug.md)+ 1英語字幕ソース(video/subtitles/NN-slug.md)が基本単位。
NN はチャプター番号(2桁ゼロ埋め)、slug はシーン名。
---
scene: "NN"
slug: slug-name
title: チャプタータイトル # chapters.txt・description.txt に使われる
target_seconds: 90 # 目安秒数(パイプラインは参照のみ、強制はしない)
---
## cue 01
<!-- action: 画面操作の補足メモ(TTSには渡らない。HTMLコメントは自動で除去される) -->
実際にTTSで読み上げる本文。複数行に分けて書いても、合成時は半角スペースで
1つの発話として結合される。
## cue 02
...ナレーション原稿と同じ ## cue NN 形式。title 等の frontmatter は無くてもよい。
cue 番号(n)はナレーション原稿・シーンスクリプトの ctx.cue(n) と対応させる。
CONTRACT の全文と ctx API の詳細は video/scripts/record.mjs の先頭コメントに記載している
(実装時はそちらを一次情報として参照すること)。要点:
export default { id, slug, title, narration?, storageSeed?, pageQuery?, async run(ctx) {...} }narration(省略可): 使用するナレーション原稿・字幕ソースのキー。省略時は${id}-${slug}。 ナレーション無し(映像のみ)のシーンにしたい場合はnarration: nullを指定する。storageSeed(省略可): 収録前にchrome.storage.localへ流し込む初期状態。ログイン画面を スキップしたい場合は{ demo_signed_in: true }を指定する(video/scenes/examples/00-smoke.mjs参照)。run(ctx)の中でctx.cue(n)を呼んだタイミングが、そのナレーション cue の発声開始時刻の 目安として記録される。ctx.newSegment(page)を呼ぶと、以後のctx.page/ctx.cue()は 新しいタブ(例: フルテキストページ)を基準に切り替わる。
サンプル: video/scenes/examples/00-smoke.mjs(スモークテスト用。
narration を 01-intro に向けて音声付きで最後まで通す)。video/scenes/ 直下ではなく examples/
サブディレクトリに置いているのは、record.mjs のシーン列挙が拡張子 .mjs のファイルのみを
対象とし、サブディレクトリを無視するため(npm run video:record を引数無しで実行しても
スモークテストは収録対象に含まれない)。実際のチャプター01〜11のシーンは video/scenes/NN-slug.mjs
として追加済み。
ctx.cue(n)が記録するのは「その瞬間の壁時計時刻」を、アクティブなセグメント(ページ)が 作られた瞬間からの相対秒数に変換した値。sub秒(1秒未満)オーダーの誤差が生じうる。assemble.mjsは各キューの音声を「その cue の想定発声時刻」と「直前の cue 音声終了 + 最短間隔(MIN_CUE_GAP_SEC= 0.3秒)」の遅い方に配置するため、多少のタイミングのズレは 自然に吸収される(早口で操作してもナレーションが重ならない)。- ナレーション音声の合計がシーン映像より長くなった場合は、映像の最終フレームを複製して
引き伸ばす(
tpad)。逆に映像がナレーションより長い場合は、映像の自然な尺がそのまま使われる。
アプリ内の操作ツアー(src/lib/guide/tours/)を、ツアー1本につき1本の短い動画にする。
上の4ステップ(手書きのシーンと原稿から作る長い解説動画)とは別の経路で、映像も原稿もツアーそのものから作るので、
シーンスクリプトやナレーション原稿を書く必要がない。招待した参加者へ「最初に見てほしい動画」として渡す用途を想定している。
npm run build:demo
LANGUAGE=ja xvfb-run -a -s "-screen 0 1920x1080x24" npm run video:tours # 参加者向けの既定3本
LANGUAGE=ja xvfb-run -a -s "-screen 0 1920x1080x24" npm run video:tours -- ml-start # ツアー ID を指定
npm run video:tours -- --skip-capture ml-start # 撮り直さず、直前のスクリーンショットから作り直すWindows では xvfb は不要で、node video/scripts/tour-videos.mjs fulltext-page のようにそのまま実行できる。
- 映像:
scripts/guide-tour-check/のシナリオ(npm run check:toursと同じもの)がデモビルドの上でツアーを最後まで操作し、 手順ごとに残すスクリーンショットを使う。手順ごとに最初の1枚を1画面にし、縦長(サイドパネル)は左に画面・右に文言、 横長(全文の判定ページ)は上に画面・下に文言で並べる。シナリオで通らなかった手順(skipIfで飛ばされる分岐)は入れない。 最初の1枚がダイアログの開いた瞬間になる手順は、video/scripts/tour-videos.mjsのSHOT_OVERRIDESで、ツアー・手順ごとに使う画像のラベルを指定できる。 指定したラベルの最初の1枚を使い、画像が無い場合はエラーになる。fulltext-pageは動画用シナリオfulltext-page-videoで撮る(video/scripts/tour-videos.mjsのCAPTURE_SCENARIOS)。 確認用シナリオは判定を保存して PDF の無い次の候補へ進み、終盤が PDF の無い画面になるため、動画用では理由を選ばずに進めて 同じ文献の PDF を保つ。このため動画には手順reason-info(除外を押さなかったときの説明)は入らない - 原稿: ツアーの定義の手順の順番と、ja の
messages.jsonのカードの文言。英字は VOICEVOX が読めないので、video/scripts/tour-videos.mjsのREADINGS(PDF → ピーディーエフ など)で音声だけ読み替える(画面の文言はそのまま)。 ツアーの文言に新しい英字が入ったらREADINGSに足すこと(足さないとアルファベットを1字ずつ読む) - 追従: ツアーの手順や文言を変えたら作り直すだけで動画も追従する。
check:toursが落ちる変更は、動画の撮り直しも要る合図 - 出力:
video/build/tours/<ツアーID>.mp4と字幕<ツアーID>.srt(途中の画面・音声・区切りの MP4 はvideo/build/tours/<ツアーID>/)。 同じ文言・話者の音声は作り直さない --skip-captureが使えるのは直前に撮ったツアーだけ(check:toursは実行のたびにスクリーンショットを消すため)- Playwright 同梱の Chromium と版が合わない環境(Claude Code web のコンテナなど)では、
PLAYWRIGHT_CHROMIUM_PATH=/opt/pw-browsers/chromiumを付ける(check:toursも同じ変数を見る)
YouTube は公開済み動画のファイルを差し替えられないため、上げ直すと動画の ID が変わる。
- 長編:
docs/index.html・docs/help.htmlの埋め込みと YouTube リンク、src/lib/guide/topics.tsのGUIDE_VIDEO_IDを更新する。 - 章の時刻が変わった場合:
video/build/chapters.txtを出どころとして、GUIDE_VIDEO_CHAPTERSの開始秒を更新する。 - ツアー動画:
docs/help.htmlの#getting-started-toursの一覧で、該当するツアーの動画リンクと長さ(日英の両方)を更新する。
| パス | 内容 |
|---|---|
scenes/<NN-slug>/segment-K.webm |
収録した生の映像セグメント(record.mjs) |
scenes/<NN-slug>/meta.json |
セグメント・キュー時刻等のメタデータ(record.mjs) |
audio/<NN-slug>/cue-NN.wav |
合成したナレーション音声(tts.mjs) |
audio/<NN-slug>/index.json |
cue ごとの音声メタ・再合成スキップ用ハッシュ(tts.mjs) |
scenes/<NN-slug>.mp4 |
シーン単体の完成動画(映像+ナレーション、assemble.mjs) |
final.mp4 |
全シーンを結合した最終動画(assemble.mjs) |
chapters.txt |
YouTube 説明欄に貼るチャプタータイムスタンプ |
timeline.json |
シーンごとの尺・cue 配置時刻の記録 |
subtitles-en.srt |
英語字幕(YouTube の字幕トラックとしてアップロード) |
description.txt |
YouTube 説明欄用テキスト(チャプター・リンク・クレジット込み) |
thumbnail.png |
サムネイル(video/assets/thumbnail.html を撮影) |
tours/<ツアーID>.mp4 / .srt |
操作ツアーの解説動画と字幕(tour-videos.mjs) |
tours/<ツアーID>/ |
ツアー動画の途中のファイル(画面 PNG・音声 WAV・手順ごとの MP4) |
これらはすべて video/build/ から再生成可能なため git 管理しない
(.gitignore の video/build/ / video/tools/ を参照)。