Remotion による製品ビデオ
コードで構築された顔出しなしの製品デモ(short-video-formats のフォーマット3)。音声がタイミングを決定します。各スクリプト行が発話され、テイクが連続して配置され、ビデオのシーンとキャプションはそれらのタイムスタンプに従います。UIは製品自身のスタイルからReactで再構築されるため、どのズームレベルでも鮮明で、製品やスクリプトが変更されると再レンダリングされます。レンダリングされたMP4以外のすべてがgitで管理されるため、過去の任意のバージョンはチェックアウトしてレンダリングするだけで復元できます。
入力条件:Node 22、ffmpeg、TTS(下書き用は edge-tts、公開前はライセンスされた音声)、製品のCSSトークンとマークアップ(ランディングページの製品デモが最適なソースであることが多い)、およびスクリプト。
ワークフロー
1. セットアップ
npx create-video@latest --blank reel && cd reel && npm i @remotion/google-fonts
フレーム(1080×1920、30fps)、レイアウトバンド、製品のCSSからコピーしたカラートークンを持つ theme.ts を1つ保持します。バンドの構成:タイトルストリップはy=118に中央配置、白いUIバンドはy=220から高さ1250px(高さの65%、幅全体)、キャプションはy=1640に中央配置。
2. スクリプトをデータとして記述
script.json、キャプション行ごとに1エントリ。*stars* で強調する単語をマークします。
{ "voice": { "name": "en-US-AndrewMultilingualNeural", "rate": "+10%" }, "gap": 0.16,
"lines": [
{ "scene": "wall", "text": "I stopped *prompting* Claude Code." },
{ "scene": "wall", "text": "I *hired* it.", "pause": 0.5 },
{ "scene": "end", "text": "Hire your first *AI employee*.", "say": "Crewmark. Hire your first AI employee." }
],
"post": { "tiktok": { "caption": "…", "hashtags": [] }, "instagram": { "caption": "…", "hashtags": [] } } }
say は音声がキャプションと異なる場合に使用します。pause はその行の後にシーンを長く保持します。
3. 音声→タイムライン
小さなNodeスクリプトが各行を個別のWAVに発話し、無音をトリミングし、gap/pause を間に挟んで連結し、それらを public/voice.wav にミックスし(adelay + amix、次にソーシャルラウドネス用に loudnorm=I=-14)、各行の from/to(フレーム単位)を public/timeline.json に書き出します。
{ "durationInFrames": 959, "lines": [ { "scene": "wall", "text": "I stopped *prompting* Claude Code.", "from": 12, "to": 51 } ] }
各テイクをテキストと音声でキャッシュすることで、1行を編集しても残りの行が再発話されないようにします。コンポジションの durationInFrames はこのファイルから取得します。
4. シーンはタイムラインに従う
同じ scene を持つ連続した行を1つの <Sequence> にグループ化します。各シーンにその行のオフセット(「ビート」)を指定し、アニメーションが単語に合うようにします。キャプションは現在の行を読み取る1つのコンポーネントです。
重要な部分はUIバンド上のカメラです。UIを実際のCSSピクセルでレイアウトし、拡大・移動します。
export function Cam({ zoom, x = 0, y = 0, children }: { zoom: number; x?: number; y?: number; children: React.ReactNode }) {
return (
<div style={{ position: "absolute", inset: 0, overflow: "hidden", background: "#fff" }}>
<div style={{ position: "absolute", width: 1080 / zoom + x, height: STAGE_H / zoom + y,
transform: `scale(${zoom}) translate(${-x}px, ${-y}px)`, transformOrigin: "0 0" }}>{children}</div>
</div>
);
}
zoom = 1080 / 520 の場合、幅520pxのレイアウト(248pxのカード2枚)がちょうど幅を埋めます。zoom、x、y をイージング補間でビート間でアニメーションさせ、クリックされるボタンに押し込みます。
うまく機能するシーンのアイデア:カードのマーケットプレイス、自身で入力するフォーム(キャレット付きのタイプ文字)、カードが列を移動するボード、アクティビティスレッド、自身で描画するチャート(「デモデータ」とラベル付け)、エージェントが質問してあなたが承認するチャット、ランディングヒーロースタイルのエンドカード。
5. レンダリングして確認
node voice.mjs && npx tsc --noEmit
npx remotion still src/index.ts Reel /tmp/f0.png --frame=0 # カバー画像
npx remotion render src/index.ts Reel out/reel.mp4
ffmpeg -v error -y -i out/reel.mp4 -vf fps=1,scale=200:-1,tile=8x5 -frames:v 1 out/sheet.jpg
コンタクトシートと各シーンの静止画を確認します。UIバンドが完全に表示され、バンドの下部が途切れていないこと、キャプションが下部ストリップ内にあること、各行の下に正しいシーンがあること、フレーム0が空白でないことを確認します。
6. バージョン管理
ソース、スクリプト、voice.wav/timeline.json をコミットします。out/ とダウンロードファイルは .gitignore に保持します。古いカットを復元するには、そのコミットをチェックアウトして再レンダリングします。
チェックリスト
- 色、フォント、コピーは製品自身のCSSとテキストから取得
- UIバンドは高さの50%以上、幅全体。ストリップは上下のみ
- フレーム0をレンダリングし、カバーとして確認済み
- すべてのシーンが製品が実際に持つ機能を表示
- デモ番号にラベル付け
- レンダリングごとにコンタクトシートを確認
- ソースと音声をコミット済み。MP4はコミットしない
遭遇した落とし穴
- 最初のシーンのフェードインにより、フレーム0(カバー)が空白になりました。最初のシーンのエンターアニメーションをスキップしてください。
- カメラでコンテンツが切り取られる。 あるズームレベルに合わせたチャートが、そのシーンのズームでバンドの下部を超えてしまいました。各カメラ移動の開始時だけでなく、終了時の静止画も確認してください。
- 下半分がまばら。 低ズームでは、短いシーンで白いバンドの下部が空になります。さらにズームインするか、何か(メッセージ入力、フッター)を下部に固定して、バンドが実際の画面のように見えるようにしてください。
- .srt が必要なレビュースクリプト。 以前のffmpegパイプラインのレビューツールは、Remotionが書き出さない字幕タイミングを読み取っていました。
fps=1,tileを使用したコンタクトシートで同じ役割を果たせます。 - 1つのスタイルオブジェクト内での
transformの重複(押下アニメーションとエンターアニメーション)は型チェックに失敗します。要素をラップして、各レイヤーに1つのtransformを設定してください。 - 最終版でのEdge TTSの使用。 無料で下書きには適していますが、人間らしいものと比較するとロボット的であり、商用ライセンスが付属していません。大規模公開の前にTTS関数を差し替えてください。
関連スキル
- short-video-formats: レイアウトルールと構築するフォーマット。
- social-publish:
script.jsonに保持される投稿コピー。 - video-retro: 次のレンダリングで何を変更するか。