字幕动画 是做什么的?
将转录稿或旁白转换为逐字精准、引人注目的字幕:每个字在音节处弹出,当前词高亮,文字与旁白紧密同步。打造适合抖音、Reels 和 Shorts 的吸睛字幕——清晰、合拍、位于安全区域内。
使用场景
- 在 9:16 竖版短视频中创建逐字或卡拉 OK 字幕。
- 将旁白/解说音轨根据转录稿烧录为开放字幕。
- 将 Whisper/SRT 转录稿转换为 Remotion 或 Web 上的动画文本。
- 将自动生成的字幕重制为带有品牌风格的“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 快约 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 个词
});
Web/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 行。
与旁白/解说同步
转录稿已包含对应音频的精确时间。将该音频放置在合成层下方,并确保时间戳基于音频的时间基准——字幕和语音将完美同步,无需手动调整。如果重新录制了旁白,请重新转录;切勿手动偏移时间。
内嵌字幕 vs 独立 SRT/VTT
| 内嵌(开放字幕) | 独立 SRT/VTT(闭合字幕) | |
|---|---|---|
| 位置 | 嵌入 MP4 像素中 | 单独的 .srt/.vtt 文件 |
| 社交媒体(抖音/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:
# - 在 frame 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、某个中间当前词帧和最后一帧,静帧渲染清晰——无缺失字体/音频。
- 在采样帧处正确的字被高亮(帧/帧率对应的时间落在其令牌区间内);无平均分割的伪造。
- 内嵌字幕清晰可读(描边+阴影),且在检查的每一帧都完全位于 9:16 安全区域内。
- 仅帧驱动——无
Date.now()/Math.random()/ 定时器。 - 实际数据参数(真实令牌,不仅仅是
defaultProps)渲染正确;输出 MP4 + 侧边 SRT/VTT,GIF 可选。
参考文件
references/word-timed-captions.md— 端到端构建:Whisper 转录和Caption类型,完整的 SRT 解析器,带有当前词高亮的 Remotion 逐字组件,手动分页,SRT/VTT 输出器,各平台安全区域映射图,以及可读排版规格表。