字幕動畫 是做什麼的?
將逐字稿或旁白轉換為逐字計時、引人注目的字幕:每個字詞會隨著音節彈出,現正播放的字詞會高亮,文字會緊貼旁白。建立那種能帶動 TikTok、Reels 和 Shorts 觀看時長的字幕——易於閱讀、與節奏同步,並且完全位於安全區域內。
使用時機
- 在短格式垂直影片(9:16)上建立逐字或卡拉OK風格的字幕。
- 將旁白/敘述音軌根據逐字稿燒錄成開放式字幕(open captions)。
- 將 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 渲染 / 檔案輸出 |
兩個不容妥協的規則
- 逐字時間,而非逐行時間。 只有當每個字詞都精準地落在音節上時,卡拉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 模型的兩倍,且對於乾淨的語音音訊來說準確度足夠。
逐字彈入效果(Remotion)
每個字詞的縮放與透明度均由一個錨定在其自身 startMs 上的彈簧(spring)動畫驅動。微小的過衝正是讓它感覺「活靈活現」的關鍵。
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 狀態。將高亮效果保持為單一的強調色,或是在當前字詞後面加上一個填滿的「方塊」——切勿同時讓每個字詞的顏色都發生變化。
頁面(Pages),而非文字牆
一次顯示 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 行。
與旁白/敘述同步
逐字稿已經包含了確切音訊的時間資訊。將相同的音訊放在合成影片下方,並保持時間戳記處於音訊的時間基準中——字幕和語音將完美鎖定,無需手動調整偏移量。如果重新錄製 VO,請重新進行轉錄;切勿手動移動偏移量。
燒錄嵌入 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)為預設值。
驗證循環——渲染靜止畫面 → 檢查 → 編碼。 字詞時間是最容易出問題的地方;在花費時間進行完整編碼之前,先在精確的影格上檢查它。
# 在起始影格 / 一個取樣的現正播放字詞影格 / 結束影格 渲染靜止畫面 — 使用已交付的 props(真實令牌 + 音訊)
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()/ 計時器。 - 已交付的 props(真實令牌,不僅是
defaultProps)能正確渲染;已輸出 MP4 + 側載 SRT/VTT,GIF 為選項。
參考檔案
references/word-timed-captions.md— 端到端建構指南:包含 Whisper 轉錄和Caption類型、完整的 SRT 解析器、附帶現正播放字詞高亮的 Remotion 逐字計時元件、手動分頁功能、SRT/VTT 輸出器、各平台的安全區域對照表,以及一份可讀性排版的規格說明頁。