字幕動畫 (Caption Animation)
將逐字稿或旁白轉換為逐詞精準同步、引人注目的動態字幕:每個詞語在音節處彈入,當前朗讀詞語高亮顯示,文字緊貼旁白節奏。打造能在 TikTok、Reels 和 Shorts 上提升觀看時長的字幕——易讀、節拍精準、並安全地置於畫面構圖內。
使用時機
- 為短影音垂直影片 (9:16) 製作逐詞或卡拉 OK 風格字幕。
- 將逐字稿燒錄成開放式字幕,與旁白/配音軌道同步。
- 將 Whisper/SRT 逐字稿轉換為 Remotion 或網頁上的動畫文字。
- 將自動產生的字幕重新設計成有品牌風格的「Hormozi 式」揭示效果。
處理流程 (The Pipeline)
| 階段 | 工作 | 工具 |
|---|---|---|
| 轉寫 | 音訊 → 逐詞時間戳記 | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| 正規化 | → { text, startMs, endMs }[] 代幣 | @remotion/captions Caption 型別 |
| 分頁 | 將詞語分組為 1–4 個詞的「頁面」 | createTikTokStyleCaptions() |
| 動畫 | 逐詞彈入 + 當前詞高亮 | Remotion spring() / CSS |
| 放置 | 安全區域、易讀字型 | 下方排版規則 |
| 輸出 | 燒錄 (MP4) 及/或外部 SRT/VTT | Remotion 渲染 / 檔案輸出 |
兩條不可妥協的規則
- 逐詞時間,不是逐行時間。 卡拉 OK 字幕只有在每個詞語精準落在音節上時才顯得神奇。務必使用逐詞時間戳記;絕不要透過將行持續時間平均分配來偽造——時間偏移會立刻被察覺。
- 可讀性優先於風格。 字幕是在手機、陽光下、靜音狀態下閱讀的。粗體無襯線字型、厚重描邊或陰影、高對比是首要考慮;裝飾其次。沒人能讀懂的字幕只是裝飾,而不是字幕。
從逐字稿取得逐詞時間
Whisper 可以直接輸出逐詞時間戳記。將所有來源正規化為一個統一的代幣形狀,讓渲染器永遠不需要關心詞彙來自何處:
// 字幕代幣 — 下游所有元件消費的唯一形狀
type Token = { text: string; startMs: number; endMs: number };
// 解析 SRT 提示區塊 "00:00:01,200 --> 00:00:01,640" 為毫秒
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; // 該詞的入場影格
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // 從 60% → 100% 彈入 (會超調)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // 當前詞高亮
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
透過檢查 frame 是否落在每個代幣的 [startMs, endMs] 區間內來計算 active 狀態。將高亮保持為單一強調色,或是在當前詞後方放置一個填滿的「方塊」——千萬不要同時讓所有詞的顏色都變化。
分頁,而非文字牆
一次顯示 1–4 個詞。使用 Remotion 的 createTikTokStyleCaptions() (或手動分組) 並調整 combineTokensWithinMilliseconds:約 200–500ms 用於真正的逐詞顯示,約 1000–1500ms 用於簡短可讀的短語。
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // 每頁約 2–3 個詞
});
網頁/CSS 彈入 (無 Remotion)
對於 DOM/即時播放器,為每個詞設定與其 startMs 相等的 animation-delay:
.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 行。
與旁白/配音同步
逐字稿已經包含了確切音訊的時間。將相同的音訊放置在合成下方,並保持時間戳記在音訊的時間基準內——字幕和語音會保持鎖定,無需手動微調。如果重新錄製了配音,請重新轉寫;切勿手動偏移時間。
燒錄 vs 外部 SRT/VTT
| 燒錄 (開放式字幕) | 外部 SRT/VTT (封閉式字幕) | |
|---|---|---|
| 位置 | MP4 中的像素 | 獨立的 .srt/.vtt 檔案 |
| 社群平台 (TikTok/Reels) | 必須——保證顯示、可設定樣式 | 平台通常忽略 |
| 無障礙 (ADA/WCAG) | 單獨使用不符合規範 | 必須 (可切換開啟/關閉) |
| 最佳實踐 | 為社群平台燒錄動畫字幕 | 同時為網頁/SEO 提供外部字幕檔 |
兩者都輸出:為社群平台燒錄動畫字幕,並從相同代幣寫入一份純文字 SRT/VTT 以滿足無障礙/SEO 播放需求。
輸出檢查清單
- 逐詞時間戳記 (真實的,來自轉寫)——沒有均分偽造。
- 逐詞彈入,帶有微小超調;當前詞使用一個強調色。
- 每頁 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)。
驗證循環 — 渲染靜態圖 → 檢查 → 編碼。 逐詞時間是最容易出問題的地方;在花費時間進行完整編碼之前,先在確切的影格上檢查它。
# 在起始 / 取樣的中間當前詞影格 / 結束處渲染靜態圖 — 使用已提供屬性 (真實代幣 + 音訊)
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
# 檢查每個 PNG:
# - 在影格 90 高亮顯示的詞語,其 [startMs,endMs] 應包含 90/fps (無時間漂移)
# - 燒錄的字幕清晰可讀:粗體無襯線、描邊+陰影有效、無裁切
# - 9:16 安全區域:字幕位於中央區域,離頂部約 12% 和底部約 20-35% (字幕/CTA/音訊 UI) 及右側動作欄有足夠距離
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # 靜態圖確認無誤後再編碼
npx remotion render Captions out/demo.gif --codec=gif # README 示範剪輯
使用 npx remotion compositions 讀取 durationInFrames/fps 並選取當前詞與結束影格。
完成之前:
- 靜態圖在影格 0、中間當前詞影格和最後一幀都能乾淨渲染——無缺少字型/音訊。
- 在取樣的影格上,正確的詞被高亮顯示 (影格/fps 落在其代幣區間內);無均分偽造。
- 燒錄字幕清晰可讀 (描邊+陰影) 且字幕在每個檢查的影格都完全位於 9:16 安全區域內。
- 僅由影格驅動——無
Date.now()/Math.random()/ 計時器。 - 已提供的屬性 (真實代幣,不僅是
defaultProps) 能正確渲染;MP4 + 外部 SRT/VTT 已輸出,GIF 為可選。
參考檔案
references/word-timed-captions.md— 端到端建置:Whisper 轉寫和Caption型別、完整的 SRT 解析器、帶有當前詞高亮的 Remotion 逐詞時間元件、手動分頁、SRT/VTT 輸出器、各平台安全區域對照表,以及可讀字型規格表。