kikuta@dev:~$cat ./notes/spex-promo-video-as-code.md
·17 min·kikuta

Playwright + Remotion + TTSでプロモ動画を再生成可能にする

実画面撮影、EN/JA TTS、Remotion、FFmpeg検証までを一つの再現可能な動画生成pipelineにする。

Chrome拡張 Spex のChrome Web Store公開に向けて、30秒程度のプロモ動画を作りました。

最初は、かなり単純に考えていました。

  1. 実際にSpexを操作して画面収録する
  2. Remotionでタイトルや説明を載せる
  3. 音声を付ける
  4. MP4を書き出す

30秒なら、それほど大きな制作ではありません。

ところが実際に何度か作り直すと、映像編集そのものより、**「画面・字幕・ナレーション・現在のプロダクト状態を一致させ続けること」**の方が難しいと分かりました。

たとえば、

  • narrationでは「次のキーが見える」と言っているのに、画面は別のKeyMapになっている
  • slideの説明と実際に映っている操作が一致していない
  • 初回Tutorialが録画に映り込む
  • KeyTipsは正しいが、設定画面だけ一つ前のバージョン
  • 日本語音声で SpexKeyTips の読みが不自然
  • 音声を自然に直したら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としてレビューする

ことができます。

この分離はかなり効きました。

字幕と音声は完全に同じ文字列である必要はありません。

しかし、別々に管理して意味がズレるのは困る

その中間として、displaytext を一つの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に使っています。