Playwright + Remotion + TTSでプロモ動画を再生成可能にする
実画面撮影、EN/JA TTS、Remotion、FFmpeg検証までを一つの再現可能な動画生成pipelineにする。
Chrome拡張 Spex のChrome Web Store公開に向けて、30秒程度のプロモ動画を作りました。
最初は、かなり単純に考えていました。
- 実際にSpexを操作して画面収録する
- Remotionでタイトルや説明を載せる
- 音声を付ける
- MP4を書き出す
30秒なら、それほど大きな制作ではありません。
ところが実際に何度か作り直すと、映像編集そのものより、**「画面・字幕・ナレーション・現在のプロダクト状態を一致させ続けること」**の方が難しいと分かりました。
たとえば、
- narrationでは「次のキーが見える」と言っているのに、画面は別のKeyMapになっている
- slideの説明と実際に映っている操作が一致していない
- 初回Tutorialが録画に映り込む
- KeyTipsは正しいが、設定画面だけ一つ前のバージョン
- 日本語音声で
SpexやKeyTipsの読みが不自然 - 音声を自然に直したらscene尺を超える
- spoken copyだけ直したら、画面のcopyと意味がズレる
といったことが起きます。
この種のズレは、動画が長いから起きるわけではありません。
30秒でも、複数のSource of Truthがあると普通に壊れます。
そこで、Spexのプロモ動画では、撮影・音声生成・映像合成・検証までを再実行できるpipelineにしました。
現在のproduction flowは概念的に次の通りです。
current Spex production build
↓
Playwright + real Google Sheets
↓
locale-specific product captures
↓
TTS narration generation
↓
Remotion composition
↓
FFmpeg / ffprobe validation
↓
Store Promo EN / JA + LP short demo
Marketing mockupを撮らない
まず決めたのは、動画専用の偽UIを作らないことでした。
Store動画で見せるGoogle Sheets操作は、production Spexを読み込んだ実Google Sheetsから撮ります。
Spex source
-> WXT production build
-> Playwright launches Chromium with .output/chrome-mv3
-> real Google Sheets
-> actual Spex key sequence
-> browser recording
実画面を使う理由は、単に「本物っぽい」からではありません。
Marketing mockupを別実装すると、製品が変わったときに動画だけ古くなるからです。
SpexではKeyTipsの配置、Sheets準拠 / Excel互換キーマップ、Tutorial、Toast、設定画面などがリリース直前まで変わりました。
動画専用UIを持っていたら、そのたびに二重更新になります。
撮影シナリオをコードにする
動画素材も「手でSpexを操作して録画」にはしていません。
Playwrightが操作シナリオを実行します。
現在の代表的なcaptureは次のようなものです。
01-keytips-command
Altを押してSheets-native root KeyTipsを見せる
02-keyboard-flow
Alt -> Row -> live row command
実際に行操作へ進む
03-keymaps-settings
Excel-compatible / Sheets-native keymapを切り替える
人間が録画すると、
- キーを押すタイミング
- マウス位置
- capture開始位置
- Toastが消えるタイミング
が毎回少し変わります。
Playwrightなら、少なくともproduct interaction部分は同じ手順で再現できます。
撮影用Sheetのcleanupは、映像より慎重にする
Store画像と同じく、動画も一時Sheetを作って撮影します。
ただし動画では、cleanupのタイミングに別の問題があります。
撮影が終わった直後に一時Sheetを削除すると、削除確認dialogがrecordingの末尾へ映り込むことがあります。
そこでcleanupはexport対象のtrim windowより後ろへ遅らせます。
また、削除対象もprefixで限定しています。
SPEX_PROMO_...
production pipelineが実データへ触る以上、
撮影に余計なdialogが映らない
よりも、
想定外のSheetを絶対に消さない
を優先しています。
Remotionは「編集する場所」ではなく「構成をコードにする場所」
raw captureを撮っただけではStore動画にはなりません。
動画では、
- hook
- product footage
- headline
- body copy
- key overlay
- end card
- narration
をscene単位で組みます。
SpexのStore promoは30秒で、現在は5 sceneです。
hook 4.0s
keytips 7.0s
flow 6.5s
workflow 6.0s
close 6.5s
ここをRemotion compositionとして持っています。
この構造にして良かったのは、「何秒目に何を見せるか」がsource code上でレビューできることです。
動画編集ソフトのtimelineだけに存在すると、Git diffで意味のあるレビューがしづらくなります。
一番面倒だったのは日本語音声だった
英語音声は現在Kokoro-82Mを使っています。
provider: Kokoro-82M
voice: af_heart
日本語はVOICEVOXを使い、最終的に春日部つむぎ / ノーマルを採用しました。
ただし、音声エンジンを決めれば終わりではありませんでした。
英語のproduct nameやUI用語を日本語TTSへそのまま渡すと、不自然になることがあります。
例えば画面には、
Alt / Option を押して、Spex の KeyTips をたどるだけ。
と出したい。
しかし読み上げは、
オルト、またはオプションキーを押して、スペックスのキーチップスをたどるだけ。
のようにした方が自然です。
ここで表示用copyと読み上げ用copyを別ファイルへ分けると、今度は内容がズレます。
display copyとspoken copyを同じJSONに置く
そこで、sceneごとに同じcanonical JSONへ両方を持たせています。
{
"keytips": {
"display": {
"title": "次のキーが、見える。",
"body": "Alt / Option を押して、Spex の KeyTips をたどるだけ。"
},
"text": "次のキーが、見える。オルト、またはオプションキーを押して、スペックスのキーチップスをたどるだけ。"
}
}
これで、
- 画面では正式なproduct spellingを使う
- TTSには発音しやすい文字列を渡す
- ただし意味上は同じsceneのsourceとしてレビューする
ことができます。
この分離はかなり効きました。
字幕と音声は完全に同じ文字列である必要はありません。
しかし、別々に管理して意味がズレるのは困る。
その中間として、display と text を一つのscene recordに入れています。
音声は「自然さ」だけでなくscene尺で落ちる
TTS比較で自然なvoiceを選んでも、動画には時間制約があります。
7秒のsceneに8.5秒の音声は入りません。
そこで日本語ではsceneごとに、
- speed
- pitch
- intonation
- punctuation pause scale
を持っています。
例えば、現在のKeyTips sceneは概ね次のようなparameterです。
speed: 1.09
pitch: -0.01
intonation: 0.91
pauseScale: 0.68
重要なのは、これを「耳で良ければOK」にしていないことです。
生成時に各WAVの長さを測り、対応するRemotion sceneへ収まるか検査します。
つまり、音声生成もbuildの一部です。
比較用voiceは捨てずに残した
日本語音声は最初から一発で決まったわけではありません。
VOICEVOX Nemoやずんだもんを含めて比較用scriptを作り、最終的に春日部つむぎへ寄せました。
さらに同じ声でも、intonationやpauseを変えた比較音声を生成できるようにしています。
これも理由は同じで、
前回どういう設定を聞いて、なぜ今の設定にしたか
を再現できるようにするためです。
生成物そのものをGitへ大量に置くのではなく、比較を再生成するscriptとparameterを残す方針です。
final MP4を検査する
動画制作では、raw captureが綺麗でもfinal compositionが正しいとは限りません。
Remotionでcaptionを重ねた結果、
- 文字が小さい
- scene transitionで一瞬古いframeが見える
- 余計なUIがcrop内に入る
- narrationが切れる
ことがあります。
そのためrelease reviewでは、source captureだけでなくfinal rendered MP4を対象にします。
pipelineではffprobeを使い、
- resolution
- fps
- duration
を検査します。
さらにStore用EN/JA動画についてrepresentative frameのcontact sheetを作り、全体を見ます。
動画は「再生して何となく見る」だけだと、30秒でも細かいズレを見逃します。
contact sheetは静止画としてscene間の一貫性を見るのに便利でした。
LP用12秒demoも同じpipelineから作る
Store動画とは別に、Spex公式サイトには短い12秒demoがあります。
これも別撮影にはしていません。
同じcapture / Remotion projectから、別compositionとして出します。
StorePromoEn — 30s
StorePromoJa — 30s
LpDemo — 12s
production build後、LP用MP4を
website/public/media/spex-demo.mp4
へコピーし、poster imageも生成します。
つまりWebサイトのdemoも、最新のvalidated production footageへ追従します。
最終的に一つのcommandへまとめる
現在はproduction rebuildを一つのcommandから実行できます。
npm run video:production
概念的には、
1. current extension build
2. EN / JA real-product recapture
3. asset preparation
4. narration generation / timing validation
5. Remotion render
6. ffprobe validation
7. contact-sheet generation
8. LP demo copy / poster generation
まで行います。
動画制作をCIのように毎commit回す必要はありません。
ただ、公開前に「現在のsourceから作り直せる」という状態にしておくと、最後の修正が怖くなくなります。
コード化しても、人間のレビューは減らなかった
ここはStore画像と同じです。
動画をコード化したから、クリエイティブ判断が自動化されたわけではありません。
むしろ、
- このsceneは何を言うべきか
- この画面は本当に価値が伝わるか
- 日本語のイントネーションは自然か
- 5秒見たときに何が残るか
は人間が何度も見ます。
自動化したのは、その周囲の
古い画面を使っていないか localeが食い違っていないか 音声がscene尺を超えていないか final MP4の仕様が違っていないか
という再現可能な部分です。
制作自動化の目的は、クリエイティブをなくすことではなく、クリエイティブ以外のズレを減らすことだと思っています。
Spex
このpipelineは、Google Sheetsをキーボード中心で操作するChrome拡張SpexのStore動画とLP demoに使っています。
- Product: https://spex.kikuta.dev/ja
- Chrome Web Store: https://chromewebstore.google.com/detail/gahnolkboklnjhnaahjdkingoejbepcc