caption-animation について
caption-animationは、文字起こしやナレーションから単語単位でタイミングを合わせたアニメーション字幕を作るスキルです。短尺動画向けに、RemotionやWebでの実装から字幕の書き出しまで扱います。
主な機能
- 01単語単位のタイムスタンプ処理
- 02単語ごとのポップ表示と強調
- 03字幕ページ分割と配置の指針
- 04焼き込み字幕とSRT/VTT出力
- 05フレーム単位のレンダリング確認
活用シーン
- 01縦型動画にカラオケ字幕を付ける
- 02ナレーション動画に字幕を焼き込む
- 03WhisperやSRTのテキストをRemotionでアニメーション化
- 04アクセシブルな再生向けに別字幕ファイルを用意
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ iart-ai/ tiktok-video-skills --skill caption-animationインストール後、Claude Code で/caption-animationを入力して読み込まれたか確認します。
同梱ファイルREADME.mdreferences/word-timed-captions.md
詳細
キャプションアニメーション
文字起こしやナレーションから、単語ごとにタイミングを合わせた、思わずスクロールを止めるキャプションを作成します。各単語が音節に合わせてポップアップし、話されている単語がハイライトされ、テキストがナレーションにぴったり追従します。TikTok、Reels、Shortsで視聴時間を伸ばすような、読みやすく、リズムに合い、セーフエリア内に収まるキャプションを作りましょう。
使用する場面
- 縦型の短尺動画(9:16)に、単語ごとのキャプションやカラオケ風キャプションを付ける場合。
- 文字起こしを基に、ナレーション/ボイスオーバーのトラックにオープンキャプションを焼き込む場合。
- Whisper/SRTの文字起こしを、Remotionやウェブ上でアニメーション付きテキストにする場合。
- 自動生成された字幕を、ブランドに合わせた「Hormozi風」の表示に変更する場合。
パイプライン
| 段階 | 役割 | ツール |
|---|---|---|
| 文字起こし | 音声 → 単語レベルのタイムスタンプ | Whisper(@remotion/install-whisper-cpp)、AssemblyAI |
| 正規化 | → { text, startMs, endMs }[] トークン | @remotion/captions の Caption 型 |
| ページ分け | 単語を1~4語の「ページ」にまとめる | createTikTokStyleCaptions() |
| アニメーション | 単語ごとのポップ表示+アクティブな単語のハイライト | Remotion の spring() / CSS |
| 配置 | セーフエリア、読みやすい文字 | 以下のレイアウトルール |
| 書き出し | 焼き込み(MP4)および/またはサイドカーのSRT/VTT | Remotionでのレンダリング/ファイル出力 |
絶対に守るべき2つのルール
- 行単位ではなく、単語単位のタイミングを使います。 カラオケ風の表示が魔法のように見えるのは、各単語が音節にぴったり合っているときだけです。必ず単語単位のタイムスタンプで文字起こししてください。行の長さを均等に分割してタイミングを作ってはいけません。ずれはすぐに分かります。
- スタイルより読みやすさを優先します。 キャプションは、日差しの下で音声をミュートにしたスマートフォンでも読める必要があります。太字のサンセリフ体、太い縁取りまたは影、まずは高いコントラストを確保し、装飾はその後です。読めないキャプションは、キャプションではなく装飾です。
文字起こしから単語のタイミングを取得する
Whisperは単語単位のタイムスタンプを直接出力します。レンダラーが単語の取得元を気にしなくて済むよう、すべての入力を1つのフラットなトークン形式に正規化します。
// Caption token — the one shape everything downstream consumes
type Token = { text: string; startMs: number; endMs: number };
// Parse an SRT cue block "00:00:01,200 --> 00:00:01,640" into ms
const toMs = (t: string) => {
const [h, m, rest] = t.split(":");
const [s, ms] = rest.split(",");
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
};
文単位のタイミング情報しかない場合は、補間せず、単語単位のタイミングで文字起こしし直してください。Whisperの medium.en は large の約2倍高速で、音声がクリアであれば十分な精度があります。
単語ごとのポップ表示(Remotion)
各単語固有の startMs を基準にしたスプリングで、単語のスケールと不透明度を制御します。わずかなオーバーシュートが、表示に「生き生きとした」印象を与えます。
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = (token.startMs / 1000) * fps; // word's own entrance frame
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // pop from 60% → 100% (overshoots)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // active-word highlight
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
各トークンの [startMs, endMs] に frame が含まれるかどうかを判定して、active を計算します。ハイライトは単色のアクセントカラーにするか、表示中の単語の背後に塗りつぶした「ボックス」を置きます。すべての単語の色を同時にアニメーションさせてはいけません。
テキストの壁ではなく、ページで表示する
一度に表示する単語は1~4語にします。Remotionの createTikTokStyleCaptions() を使うか手動でグループ化し、combineTokensWithinMilliseconds を調整します。単語ごとに表示するなら約200~500ms、読みやすい短いフレーズにするなら約1000~1500msです。
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 words per page
});
ウェブ/CSSでのポップ表示(Remotionを使わない場合)
DOM/ライブプレーヤーでは、各単語の animation-delay を startMs と同じ値に設定します。
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
.word.active { color:#FFE45E; }
words.forEach(w => { const el = mk(w.text); el.style.animationDelay = `${w.startMs}ms`; track.append(el); });
読みやすい文字とセーフエリアへの配置(9:16、1080×1920)
| プロパティ | 値 | 理由 |
|---|---|---|
| フォント | 太字/極太のサンセリフ体(Montserrat、Inter、Helvetica) | 小さく情報量の多い画面でも読みやすい |
| サイズ | 56~80px(フレーム高の約8%)、最小45px | ミュート中でもスマートフォンで読みやすい |
| 縁取り | 黒の縁取り線を2~6px | どんな背景でも読みやすさを保てる |
| 影 | 縁取りを補う柔らかなドロップシャドウ | 明るいフレームでも背景から分離できる |
| 大文字小文字 | すべて大文字または文頭のみ大文字。塗りは高コントラスト | インパクトと読みやすさを高める |
| 垂直位置 | 中央の帯、上から約62~70% | UIより上、アクションより下 |
| 下部セーフエリア | 下から約280px/15%を空ける | キャプション/CTA/音声UIを避ける |
| 左右セーフエリア | 幅の中央80%以内に収める | 右側のアイコンを避ける |
キャプションは中央の帯に配置します。プラットフォームのUI(ユーザー名、音声、ボタン)が表示される最下部には固定しないでください。テキストは最大約2行に収めます。
ナレーションに同期する
トランスクリプトには、実際の音声の正確なタイミングがすでに反映されています。同じ音声をコンポジションの下に配置し、音声のタイムベースでタイムスタンプを維持してください。キャプションと音声はずれずに同期するため、手動で位置を調整する必要はありません。ナレーションを録り直した場合は、必ず再度文字起こししてください。オフセットを手動でずらすことは避けてください。
焼き込みとサイドカー SRT/VTT
| 焼き込み(オープン) | サイドカー SRT/VTT(クローズド) | |
|---|---|---|
| 場所 | MP4 のピクセル内 | 独立した .srt/.vtt ファイル |
| ソーシャル(TikTok/Reels) | 必須 — 確実に表示され、スタイル設定も可能 | プラットフォームに無視されることがよくあります |
| アクセシビリティ(ADA/WCAG) | これだけでは要件を満たしません | 必須(表示の切り替えが可能) |
| ベストプラクティス | ソーシャル向けにアニメーションキャプションを焼き込みます | ウェブ/SEO 向けにサイドカーも同梱します |
両方を出力します。ソーシャル向けにはアニメーションキャプションを焼き込み、アクセシビリティ/SEO に対応した再生用に、同じトークンからプレーンな SRT/VTT を書き出します。
出力チェックリスト
- 単語単位のタイムスタンプ(文字起こしに基づく実際のタイミング)— 均等分割によるごまかしは避けます。
- 各単語を少しオーバーシュートさせてポップ表示し、アクティブな単語にはアクセントカラーを 1 色使用します。
- 1 ページあたり 1~4 語にし、テキストを画面いっぱいに表示しないようにします。
- 太字のサンセリフ体、56~80px、2~6px の縁取りとシャドウ、高コントラスト。
- 画面中央の帯状の領域に配置し、下部の約 280px と左右の領域を避けます。
- 音声をコンポジションの下に配置し、音声のタイムベースでタイムスタンプを維持します。
- ソーシャル向けには焼き込み版、アクセシビリティ向けにはサイドカー SRT/VTT を同梱します。
納品と検証(レンダリングした静止画 → MP4)
同梱ヘルパー(
scripts/):scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.pngで静止画をタイル状に並べ、scripts/probe-mp4.sh out.mp4 [WxH] [fps]でエンコードを検証します。scripts/README.mdを参照してください。
キャプションは Remotion コンポジション(<Composition> + zod の schema + defaultProps)として納品します。単語の動きはすべて useCurrentFrame() を使ったフレーム駆動にし、Date.now() / Math.random() / タイマーは使用しません。納品物は out/*.mp4(焼き込み済み)+ プロジェクト + サイドカー SRT/VTT です。デフォルトは 9:16 の縦型(1080×1920)です。
検証ループ — 静止画をレンダリング → 確認 → エンコード。 単語のタイミングは不具合が起きやすいため、エンコードに時間をかける前に正確なフレームで確認してください。
# Stills at start / a sampled active-word frame / end — WITH SHIPPED PROPS (real tokens + audio)
npx remotion still Captions out/f-start.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-mid.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-end.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
# Inspect each PNG:
# - the word highlighted at frame 90 is the word whose [startMs,endMs] contains 90/fps (no drift)
# - burn-in legible: bold sans, stroke+shadow holds, no clipping
# - 9:16 safe area: caption sits in the center band, clear of top ~12% and bottom ~20-35% (captions/CTA/audio UI) and the right action rail
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # encode once stills are right
npx remotion render Captions out/demo.gif --codec=gif # README proof clip
npx remotion compositions を使って durationInFrames/fps を確認し、アクティブな単語のフレームと終了フレームを選びます。
完了前に確認すること:
- フレーム 0、単語がアクティブな中間フレーム、最後のフレームで、静止画が正しくレンダリングされる(フォント/音声の欠落がない)。
- サンプリングしたフレームで正しい単語がハイライトされている(frame/fps がそのトークンの範囲内にある)。均等分割によるごまかしはありません。
- 焼き込みキャプションが読みやすく(縁取り+シャドウ)、確認したすべてのフレームで 9:16 のセーフエリア内に完全に収まっている。
- フレーム駆動のみ —
Date.now()/Math.random()/ タイマーは使用しません。 - 実際のトークンを含む納品用 props(
defaultPropsのみではありません)が正しくレンダリングされる。MP4 とサイドカー SRT/VTT を出力し、GIF は任意です。
参考ファイル
references/word-timed-captions.md— エンドツーエンドの構築ガイド:Whisper による文字起こしとCaption型、完全な SRT パーサー、アクティブな単語をハイライトする Remotion の単語単位タイミングコンポーネント、手動のページ分割、SRT/VTT エミッター、プラットフォームごとのセーフエリアマップ、読みやすい文字組みの仕様書。