vox-skill について
vox-skillは、ReactとRemotionでドキュメンタリー調のナレーション動画を制作するスキルです。台本と音声タイミングから、レイヤー構成のシーン、音声調整、品質確認、レンダリングまでを扱い、縦型動画にも対応します。
主な機能
- 01ナレーションをビート単位で設計
- 02音声とタイミングデータの生成
- 03Reactでレイヤー構成のアニメーションを作成
- 04コード描画のビジュアル素材を制作
- 05ナレーションと音楽のミキシング・調整
- 06横型・縦型動画のレンダリング
活用シーン
- 01テーマからナレーション付きドキュメンタリーを制作
- 02話された言葉に場面転換を同期
- 03アニメーション付き図表や文書の画面を制作
- 04全体のレンダリング前にシーン配置を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jaredcassoutt/ vox-editorial --skill vox-skillインストール後、Claude Code で/vox-skillを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEREADME.mdexample/.prettierrc.jsoncues.jsonpackage.jsonremotion.config.tsscript.jsontiming.jsontsconfig.jsonexample/src/index.tsRoot.tsxexample/src/scenes/AnnexationScene.tsxBaylorFoundingSequence.tsxCloserScene.tsxFoundersScene.tsxNamingScene.tsxOlderThanStateScene.tsxPetitionScene.tsxRepublicScene.tsxSigningScene.tsxexample/src/shared/fonts.tspieces.tsxsystem.tsxtexas.tstiming.tsreferences/scene-patterns.mdvertical.mdvisual-system.mdscripts/captions.pycutout.swift詳細
Remotionの紙面Bロール
1つのトピックから、Reactで作る1920×1080のナレーション付き映像を1本。編集ドキュメンタリー調で、声を張り上げるのではなく、本当に興味深い内容で視聴者を引きつけます。
すべてのアニメーションはコードで動かします。After Effectsでの作業も、ドラッグして調整するタイムラインもありません。コンポーネントを書き、propsを調整して映像を演出します。
まず適切なスキルを選ぶ
| 作りたいもの | 使用するスキル |
|---|---|
| 横長の1920×1080、React/Remotion、コードで描画するグラフや新聞 | このスキル |
| TikTok、Reels、Shorts向けの縦長1080×1920 | このスキル、--vertical — references/vertical.mdを参照してください |
| パイプラインの選択が自由な、ナレーション付きの解説動画全般 | explainer-studio |
| サウンドデザイン付きのテンポの速い商品広告 | fastcut-ad |
品質を左右する要素とその優先順位
- 土台となる事実。 本当に意外で、検証可能な逆転の事実です。これがなければ映像を作る理由がなく、どれほど技巧を凝らしても補えません。
- ナレーション。 時間を刻み、映像を支える軸です。カットはすべて、発話される言葉に合わせます。
- 奥行き。 フレーム内に、互いに異なる動きをする3つのレイヤーがあるかどうかです。
- その他すべて。
この順番で労力をかけてください。
パイプライン
10のフェーズで構成されています。フェーズNの成果物ができる前に、フェーズN+1を始めないでください。
1. 逆転の事実を見つけ、ビートを書く
視聴者の予想と正反対の結末になる事柄を見つけます。統計ではなく、矛盾です。
1行につき1つのアイデアで、8〜12個のビートを書きます。スクリプトがタイムラインになります。1つのビートは、1つの画像を担える1文または1節です。
フックはビート1で、主題を伏せます。 固有名詞は使いません。国、人、機関、年は出しません。あり得ないと思えることを述べます。最後のビートで、その問いに正確に答えます。下書きを2つの質問で確かめてください。視聴者はフックから主題を言い当てられますか? そうなら、書き直してください。最後の行は、フックが生んだ期待に応えていますか? そうでなければ、どちらかが間違っています。
ビートをscript.jsonに書き込みます。
{"topic": "...", "beats": [{"id": 1, "narration": "This university is older than the state it stands in."}]}
2. まず音声を作り、その後にタイミングを決める
ナレーションの長さによってすべてのフレーム数が決まるため、アニメーションを作る前にナレーション音声を生成します。
python3 -m venv .venv && ./.venv/bin/pip install -q edge-tts
./.venv/bin/python ~/.claude/skills/vox-skill/scripts/gen_vo.py
これにより、vo/beat_NN.mp3、結合したpublic/audio/vo.mp3、およびビートごとの開始・終了・長さ、フレーム範囲、単語単位の境界を保持するvo/timing.jsonが作成されます。以降の長さはすべてこのファイルから読み込みます。フレーム数を推測してはいけません。
デフォルトの音声はen-GB-RyanNeural、速度は+4%です。VOICE=とRATE=で変更できます。音声を変更しても、映像の作成後なら、手順3を実施しておけば手間はほとんどかかりません。確定する前に、いくつか試聴しておくとよいでしょう。
| 音声 | 調子 |
|---|---|
en-US-AndrewMultilingualNeural | 新しいHDモデル。温かみがあり、自信に満ちています。最適なデフォルトです。 |
en-US-ChristopherNeural | 「信頼感があり、権威ある」と評価されています。定番のナレーターです。 |
en-GB-RyanNeural / en-GB-ThomasNeural | イギリス英語で、より淡々としています |
en-US-SteffanNeural | ニュースをそのまま読み上げるスタイルに最も近い音声です |
音声モデルより、マスタリングの方が重要です。 フェーズ6を参照してください。
3. タイミングを手入力せず、導出する
波形を見ながらフレーム番号を手作業で調整しても、一度しかうまくいきません。音声、速度、または単語を1つ変えるだけで、すべての数字が間違ってしまいます。
各要素を配置する単語を指定したcues.jsonを書き、そこからタイミングモジュールを生成します。
python3 ~/.claude/skills/vox-skill/scripts/sync_timing.py --check # inspect
python3 ~/.claude/skills/vox-skill/scripts/sync_timing.py # write src/shared/timing.ts
シーンでは、長さにDを、すべての開始フレームにCUEをインポートします。これで、音声の変更は午後いっぱいの作業ではなく、コマンドを3つ実行してレンダリングするだけになります。--checkを使うと、各キューがシーンの長さの範囲内にあるかも確認できます。
4. ビートをシーンに割り当てる
シーン数はビート数より少なくします。 2つか3つのビートを1つの静止画でまとめて見せることもよくあります。ナレーションが続く間、シーンを維持すると自信のある印象になります。10個のビートに対して7つのシーンが目安です。
シーンの長さは、そのシーンに含まれるビートのフレーム範囲を合計して求めます。値はvo/timing.jsonから取得します。コンポーネントを書く前に、割り当てを記録してください。
5. 素材を作成する
3種類の素材を、次の優先順位で用意します。
コードで描画(最適)。 新聞、チャート、文書、地図、帳簿、タイプライター風のテキスト、取り消し線付きのリストなどです。ファイルは不要で、キーイングも必要なく、無限に調整でき、ネイティブにアニメーション化できます。まずはこの方法を検討してください。references/scene-patterns.md を参照してください。
透過PNGの切り抜き画像。 被写体の切り抜き処理はVisionを使ってローカルで実行でき、無料でオフラインでも利用できます。
swiftc -O ~/.claude/skills/vox-skill/scripts/cutout.swift -o /tmp/cutout
/tmp/cutout raw/portrait.jpg public/scene-04/portrait.png
python3 ~/.claude/skills/vox-skill/scripts/halftone.py public/scene-04/portrait.png
halftone.py は紙の質感を加えます。彩度を下げ、コントラストを上げ、網点を適用して、インクに温かみを加えます。現代の写真を19世紀の紙面になじませます。
Visionで被写体が見つからない場合は、PILに切り替えてください。4隅から許容値約55でフラッドフィルを行い、内部の塗りつぶし(襟など)を残せるよう、連結した不透明領域のうち最大のものだけを保持します。中間の辺をフラッドフィルの起点にしてはいけません。幅の広い被写体に漏れ込むおそれがあります。
透過動画。 アルファ付きのVP9にトランスコードし、<OffthreadVideo transparent /> を使います。
ffmpeg -i in.mov -c:v libvpx-vp9 -pix_fmt yuva420p -vf scale=1920:-2 -b:v 0 -crf 32 out.webm
紙の背景は、正確に1920×1080で5 MB未満に保ってください。28 MBの背景ではStudioでフレーム落ちが発生し、調整中に音声との同期もずれてしまいます。
6. 固定したビジュアルシステムに沿ってシーンを構築する
システムを一度決めたら、各シーンはそのバリエーションとして作成します。カラーパレット、文字サイズ、モーションのルールについては references/visual-system.md を参照してください。必ず守るべき点は次の3つです。
すべてのシーンを3層にします。
- 背景 — すべてのシーンで同じ紙のテクスチャを全体に敷き、
zIndex 0に設定して、その上にソフトライトのウォッシュを重ねます。背景は変えません。この一貫性によって、7つのシーンが1本の映像としてまとまります。 - 中景 — 被写体を切り抜いて配置し、それぞれに少しずらした赤いマーカーの輪郭線を付けます。
- 前景 — 被写体の下半身を隠し、ショットを安定させる要素を配置します。
前景がないシーンはスライドのように見えます。被写体を隠すことで奥行きが生まれます。
赤いマーカーの輪郭線が特徴です。切り抜き画像の後ろに単色のシルエットを描画し、数ピクセルずらします。片側に見えるよう、主に水平方向へずらします。
<div aria-hidden style={{
...sameBoxAsImg,
backgroundColor: "#E04329",
maskImage: `url(${assetUrl})`, WebkitMaskImage: `url(${assetUrl})`,
maskSize: "100% 100%", WebkitMaskSize: "100% 100%",
maskRepeat: "no-repeat", WebkitMaskRepeat: "no-repeat",
transform: `translate(calc(-50% + ${strokeX}px), ${rise + strokeY}px)`,
}}/>
<Img src={assetUrl} style={{ ...sameBoxAsImg, transform: `translate(-50%, ${rise}px)` }} />
strokeX はおよそ±26、strokeY は8にします。画面左側の被写体には左向き、右側の被写体には右向きになるよう、符号を反転させてください。
要素はフェードせず、登場させます。 {damping: 18, mass: 0.75, stiffness: 110} を指定した spring() で約390pxから上昇させ、16フレームの不透明度ランプを重ねます。チャートと署名は、strokeDashoffset に interpolate() を適用して描画します。どの要素も同じフレームに登場しないよう、開始タイミングをずらしてください。
すべてのシーンで、調整可能なpropsを持つzodスキーマを使います。 各要素の位置、スケール、開始フレームと終了フレームを指定します。こうすることで、少し調整するたびに再コンパイルせずに、Studioで映像を調整できます。
7. 音声を採点して仕上げる
スペクトルのグラフではなく、ノートPCのスピーカーからどう聞こえるかで音楽のベッドを判断してください。
実際のドキュメンタリー用キューでは、エネルギーの93%が250 Hz未満にあります。この目標をそのままコピーすると、ウーファーのないスピーカーでは音楽のベッドが聞こえなくなります。スマートフォンやノートPCのスピーカーは、約200 Hz未満の音を大きく減衰させるためです。250 Hz以上の音楽の音量を、250 Hz以上の音声と比べてどれくらいにするかを確認してください。この数値だけで、誰かに音楽が聞こえるかどうかが決まります。
# 音声がある箇所とセリフの合間を、それぞれハイパス処理して比較
ffmpeg -ss <speech> -t 1.5 -i mix.wav -af "highpass=f=250,volumedetect" -f null /dev/null
ffmpeg -ss <gap> -t 0.3 -i mix.wav -af "highpass=f=250,volumedetect" -f null /dev/null
| 250 Hz以上の音量差 | 判定 |
|---|---|
| 30 dB以上 | 聞こえない。リスナーは「音楽がない」と言います |
| 18〜22 dB | 聞こえるが邪魔にならない。目標値です |
| 8 dB未満 | ナレーションとぶつかっています |
score.pyは、これを小型スピーカーでのペナルティと、作品内の4箇所での可聴レベルとして出力します。そのため、レンダリング前に無音区間を見つけられます。
python3 ~/.claude/skills/vox-skill/scripts/score.py music/bed.wav --seconds <SEQ_SECONDS>
bash ~/.claude/skills/vox-skill/scripts/mix_audio.sh \
public/audio/vo.mp3 music/bed.wav /tmp/mix.wav -20
score.pyは、スペクトルバランスも目標値と比較して出力します。音楽のベッドは音声の長さではなく、シーケンスの長さに合わせてカットしてください。そうすれば、最後の余韻まで音楽が続きます。
音楽のベッドにはmusic_dbオフセットを設定し、さらに音声がある間はサイドチェインで約6 dB下げます。これにより、音楽が一定の音量にとどまらず、セリフの合間に再び大きくなります。-18〜-22 dBは教科書どおりの数値ですが、フルレンジのミックスを前提としています。 低音に偏った音楽のベッドでは、重要な帯域で30 dB以上も低くなります。慣例に頼らず、測定した音量差をもとにオフセットを設定してください。音楽のベッドに中音域がしっかり含まれている場合は、-12 dB前後が一般的です。
短編映像では、アレンジのカーブに注意してください。 カーブの値は作品全体に対する割合なので、0.20で最大になるパートは、65秒の映像なら最初の13秒間は聞こえません。冒頭がサブベースのドローンだけだと「音楽がない」と受け取られます。そこは誰もが最初に判断する箇所です。音楽の各パートが冒頭から鳴るよう、最低限の音量を保ってください。
mix_audio.shはナレーションのマスタリングも行います。これが「テキスト読み上げ」と「ドキュメンタリーのナレーション」の違いの大部分を生み出します。
| 処理 | 理由 |
|---|---|
| 85 Hzのハイパス | 声には不要な低周波のノイズを除去します |
| 350 Hzで-2 dB | ニューラルTTS特有のこもりを抑えます |
| 3 kHzで+2.5 dB | プレゼンスを加え、子音を明瞭にします |
| 6.8 kHzで-3 dB | ディエッサー処理。TTSの歯擦音は、人の口から出る音より耳につきます |
| 3:1のコンプレッション | 小さな声量の文節を、大きな声量の文節と同じレベルにします |
| loudnorm -16 LUFS | スマートフォンやノートPCで聞くのに適した音量です |
コンポジションには、未加工のVOではなく、ミックス済みファイルを指定してください。
8. レンダリング前に静止画でQCする
本番のレンダリングを始める前に、シーンごとに静止画を1枚レンダリングし、コンタクトシートで確認してください。 70秒の映像をレンダリングするには10分かかりますが、静止画なら数秒です。重なり、改行、サイズの不具合はすべてここで見つかります。
for f in 200 541 695 1056; do
npx remotion still src/index.ts <SequenceId> out/qc/f$f.png --frame=$f --log=error
done
ffmpeg -y -i out/qc/f200.png -i out/qc/f541.png -i out/qc/f695.png -i out/qc/f1056.png \
-filter_complex "[0:v]scale=720:-1[a];[1:v]scale=720:-1[b];[2:v]scale=720:-1[c];[3:v]scale=720:-1[d];\
[a][b]hstack[t];[c][d]hstack[u];[t][u]vstack" out/qc/sheet.png
各フレームは、シーンの最初ではなく、要素が最も多く表示されるタイミングで選んでください。この確認で確実に見つかる問題は次のとおりです。
| 症状 | 原因 |
|---|---|
| スタンプやラベルが被写体に重なって読めない | インクのような赤色です。スタイルは変えず、文字のない紙の上に移動してください。 |
| 2つの要素が接触している | 絶対位置がずれています。両方を縮小せず、片方を移動してください。 |
| トラッキングされたキャプションがフレーズの途中で折り返される | whiteSpace: "nowrap"を追加してください。 |
| 見出しが複数行に折り返される | 同じ修正を行い、さらにサイズを15〜20%小さくしてください。 |
| コードで描画したSVGアートが極端に小さく表示される | viewBoxがパスより大きすぎます。パスの実際の範囲に合わせてください。 |
| 図形が別の物体に見える | シルエットには幅が必要です。肩幅が狭いと、胸像ではなく鍵穴に見えます。 |
9. 組み立ててレンダリングする
各シーンを<Series>で連結し、シーンごとに<Series.Sequence>を1つずつ配置して、ステップ3で設定した時間にします。
各<Composition>のdefaultPropsを反映して小道具を配置します。**タイミング用のプロパティはシーケンス内で調整してください。**単独で調整したシーンは、ほとんどの場合、収める必要のあるビートの時間より長くなるためです。
フラグで音声を制御し、デフォルトではコンポジションが無音でレンダリングされ、ミキシング用の各ステムが別々に出力されるようにします。
const BAKE_AUDIO = false; // true → VO at 1.0, music ducked to ~0.2, 24f fade in, 50f fade out
npx remotion studio # preview and tune
npx remotion render src/index.ts <SequenceId> out/final.mp4 # render
ffmpeg -i out/final.mp4 -c:v copy -an out/video.mp4 # strip the silent track
**Studioでプロパティの編集内容をディスクに保存するには、**各<Composition>のdefaultPropsがインポートした変数ではなく、インラインのオブジェクトリテラルであり、さらにプロジェクトのルートに.prettierrc.jsonが存在する必要があります。どちらも必須です。ここを間違えると、調整作業がすべて再読み込み時に失われます。
10. ポスターフレームを選ぶ
**デフォルトのサムネイルをそのまま使わないでください。**この方法で作成した動画の0フレームは、何も登場していない紙の背景だけです。ファイルの中で最悪のフレームであり、すべてのプレーヤーが選ぶフレームでもあります。
python3 ~/.claude/skills/vox-skill/scripts/pick_thumbnail.py \
out/film.mp4 out/thumbnail.png --embed --report
候補をすべてスコア付けし、最も情報量が多く、構図が整った瞬間を選びます。「面白さ」とはフレーム全体に複数の異なる要素が広がっていることであり、インクの占める面積そのものではありません。暗い前景の帯が下3分の1を埋めるショットは、占有面積は大きくても面白さはありません。そのため、主な評価項目はグリッド全体への空間的な広がりです。エッジ密度、上限を設けた占有面積の項目、赤いアクセントの有無が、残りの評価を構成します。
--embedは、mp4にカバーアートとして埋め込みます。--reportを指定すると候補一覧が表示されるので、好みに応じて次点のフレームを選べます。縦型の動画では--grid 5を渡してください。そうしないと、プラットフォームのインターフェースの背後にコンテンツが隠れるフレームにスコアが偏ります。
クイックリファレンス
| 項目 | 値 |
|---|---|
| 出力 | 1920×1080、30fps、MP4 |
| 紙のベース色/インク/赤いアクセント | #DAD9D5/#1A1A1A/#E04329 |
| 見出しの書体 | Arial Black、文字間隔を詰める、大文字 |
| セリフ体 | PlayfairDisplay、PTSerif(新聞、文書) |
| タイプライター体 | SpecialElite(締めくくりの行) |
| 登場時のスプリング | {damping: 18, mass: 0.75, stiffness: 110} |
| 上昇距離 | 下から360~390px |
| 不透明度の変化 | 16フレーム、Easing.out(Easing.cubic) |
| 線のオフセット | strokeX ±26、strokeY 8 |
| ドロップシャドウ | drop-shadow(0 28px 26px rgba(55,45,31,0.28)) |
| プロジェクトのひな形を作成 | scripts/scaffold.sh <dir> <SequenceName> |
| 音楽のベッド | scripts/score.py out.wav --seconds N |
| マスター音量+ダッキング | scripts/mix_audio.sh vo.mp3 bed.wav out.wav -20 |
| 音声の下に流す音楽 | 250 Hz超で測定した分離度に基づいて設定します。目安は約18~22 dBです。多くの場合、-12 dBです。 |
| 音楽が平坦に聞こえる | 音量ではなく--motifを上げます |
| 縦型動画 | scaffold.sh <dir> <Name> --verticalを実行し、続けてreferences/vertical.mdを参照します |
| 字幕(縦型動画では必須) | scripts/captions.pyを実行し、続けて<Captions />を追加します |
| ポスターフレーム | scripts/pick_thumbnail.py out.mp4 --embed |
| 「音楽が聞こえない」 | フェーダーに触る前に250 Hz超を測定します |
| 最終ラウドネス | 統合ラウドネス-16 LUFS |
よくある間違い
| 間違い | 修正方法 |
|---|---|
| フレーム数を推測して、あとから同期を調整する | すべての長さを vo/timing.json から読み込みます。基準となるのはナレーションです。 |
| ビートごとにシーンを分ける | ビートをまとめます。ナレーションが続く間、画像を見せ続けることで自信が伝わります。 |
| シーンごとに背景を変える | 背景は変えません。固定したテクスチャが映像全体を統一します。 |
| 前景レイヤーがない | 視界を遮る要素を追加します。それがないと、ショットではなくスライドになります。 |
| コードで描けるものに画像を使う | グラフ、新聞、文書、地図、リストは、常にコードで作るほうが適しています。 |
| すべての要素が同じフレームに登場する | 20〜30フレームずつずらします。同時に登場するとテンプレートのように見えます。 |
| 要素をフェードインさせる | 弾むように表示します。フェードはありきたりに見えます。 |
定数から defaultProps をインポートする | オブジェクトリテラルを直接記述してください。そうしないと、Studio が調整内容を黙って破棄します。 |
| props ではなく位置をハードコードする | すべての位置、スケール、フレームを zod スキーマに含めます。 |
| 28 MB の紙の背景 | トリミングして正確に 1920×1080 にリサイズし、5 MB 未満にします。 |
| シーンの props にフレーム番号を直接入力する | cues.json で単語を指定して、フレームを生成します。ナレーションを録り直すたびに再調整する必要がなくなります。 |
| 曲調を基準に音楽を選ぶ | 周波数スペクトルを基準に選びます。200〜4000 Hz の範囲にある音楽は、どんな音量でも声とぶつかります。 |
| 音楽の音量を一定にする | 音量を下げます。無音の合間に音量が膨らむ音楽はきちんとミックスされて聞こえますが、一定の音量だと貼り付けたように聞こえます。 |
| 未加工の TTS をそのまま納品する | マスタリングします。選んだ声よりも、EQ とコンプレッションの処理が重要です。 |
| デフォルトのサムネイルをそのまま納品する | フレーム 0 は空の背景です。pick_thumbnail.py を実行します。 |
| キャプションなしで縦型動画に切り抜く | 音声なしで見るのが標準です。キャプションを動画に焼き込みます。 |
| 縦型動画で被写体を横に3人並べる | 1人あたり340pxになります。縦に並べるか、複数のビートに分けます。 |
| 見た目を確認するために動画全体をレンダリングする | まず静止画をコンタクトシートにします。10分かけてレンダリングしても、静止画で分かることしか確認できません。 |
| パブリックドメインの肖像を使えない被写体 | 同じ枠内にインク画として描きます。他人の顔を借りてはいけません。 |
注意すべき点
vo/timing.jsonができる前にシーンコンポーネントを書こうとしている- グラフ、文書、地図のために画像生成ツールを開こうとしている
- フックで被写体の名前を挙げている
- 最後のビートが、フックで投げかけた問いに答えていない
- シーンのレイヤーが2つある
- 弾む動きが「弾みすぎて見える」という理由でフェードにしようとしている(フェードではなく、減衰を調整します)
- すべてのシーンの静止画を確認する前に、全編をレンダリングしようとしている
- レイアウトに必要だからという理由で、検証できない数字や引用を画面に表示しようとしている
参考資料
references/visual-system.md— 配色、文字、動き、レイヤーモデルの詳細references/scene-patterns.md— 実際に使えるコード付きの再利用可能なシーンの基本パターン7種類scripts/gen_vo.py— ナレーション、単語ごとのタイミング、フレーム範囲scripts/cutout.swift— Vision による被写体の切り抜きと、透過 PNG への変換scripts/halftone.py— 切り抜き画像用の紙/ハーフトーン加工scripts/sync_timing.py— cues.json と timing.json から TypeScript のタイミングモジュールを生成scripts/score.py— 緊迫感のあるドキュメンタリー調の音楽。声と周波数がぶつからないよう調整scripts/mix_audio.sh— ナレーションのマスタリング処理とサイドチェーンによるダッキングscripts/pick_thumbnail.py— 最も情報量の多いフレームを選び、ポスターとして埋め込むscripts/captions.py— ナレーションの単語境界に基づく字幕の焼き込みscripts/scaffold.sh— 実行可能な Remotion プロジェクトのひな形(9:16 は--vertical)references/vertical.md— TikTok、Reels、Shorts 向けに変わる点