Remotion 字幕导入实战:用 @remotion/captions 的 parseSrt() 将 .srt 字幕文件接入渲染流程
本篇指南聚焦一个具体场景:当你手上已经有一个现成的 .srt 字幕文件时,如何把它导入 Remotion 项目、解析为可渲染的字幕数据结构,并衔接到后续的字幕展示与动画流程。读完本文,你将掌握 @remotion/captions 的安装方式、parseSrt() 的完整调用范式(含 staticFile()、fetch() 与 useDelayRender() 的组合用法),并能从源码层面理解 SRT 解析的每一步逻辑及其在测试中被验证的行为边界。
背景:@remotion/captions 包与统一的 Caption 数据结构
Remotion 官方提供了 packages/captions 包(@remotion/captions),定位为"处理字幕的原语集合"(Primitives for dealing with captions)。它导出的核心成员可以在 packages/captions/src/index.ts 中确认:
parseSrt/serializeSrt:SRT 文件的双向转换(解析与序列化);createTikTokStyleCaptions:把逐字字幕组合成"抖音/短视频风格"的分页字幕;ensureMaxCharactersPerLine(通过CaptionsInternals导出):控制每行最大字符数;Caption类型:所有字幕处理流程的统一数据格式。
无论字幕来自转录、导入还是手写,最终都要落到同一个 Caption 结构上。其定义见 packages/captions/src/caption.ts,共 6 个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
text |
string |
字幕文本。注意文本对空白敏感,词前通常带空格 |
startMs |
number |
起始时间(毫秒) |
endMs |
number |
结束时间(毫秒) |
timestampMs |
number | null |
时间点(毫秒),解析 SRT 时取区间中点 |
confidence |
number | null |
置信度;从 SRT 导入时固定为 1 |
pageBreakAfter |
boolean? |
可选,标记该字幕之后强制换页(用于分页字幕) |
安装前置条件
开始之前,项目需要先安装 @remotion/captions。按项目使用的包管理器选择对应命令:
npx remotion add @remotion/captions # If project uses npm
bunx remotion add @remotion/captions # If project uses bun
yarn remotion add @remotion/captions # If project uses yarn
pnpm exec remotion add @remotion/captions # If project uses pnpm
安装时有一个重要的版本前提:remotion 与所有 @remotion/* 包必须保持同一版本号,应去掉版本号前的 ^ 使用精确版本(见 packages/captions/README.md)。当前仓库中该包版本为 4.0.521(见 packages/captions/package.json)。
如果还没有 .srt 文件,而是需要从音视频直接生成字幕,应参考同目录下的 transcribe-captions.md,本文只覆盖"已有 SRT 文件"的导入路径。
读取 .srt 文件:staticFile() + fetch() + parseSrt() 组合范式
Remotion 渲染时运行在受控环境里,外部资源的加载必须显式声明,因此导入 SRT 的标准写法是:把 .srt 文件放进 public/ 目录,用 staticFile() 生成引用,fetch() 拉取文本,再交给 parseSrt() 解析。完整可运行的组件示例如下(继承自 import-srt-captions.md 并补充了关键注释):
import { useState, useEffect, useCallback } from "react";
import { AbsoluteFill, staticFile, useDelayRender } from "remotion";
import { parseSrt } from "@remotion/captions";
import type { Caption } from "@remotion/captions";
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const { delayRender, continueRender, cancelRender } = useDelayRender();
// 只在首次渲染时声明一次"延迟渲染"句柄
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
// staticFile("subtitles.srt") 指向 public/subtitles.srt
const response = await fetch(staticFile("subtitles.srt"));
const text = await response.text();
// parseSrt 同步完成解析,返回 { captions }
const { captions: parsed } = parseSrt({ input: text });
setCaptions(parsed);
// 解析成功后才放行渲染,保证字幕一定就绪
continueRender(handle);
} catch (e) {
// 加载或解析失败时终止渲染并上报错误
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return (
<AbsoluteFill>
{/* Use captions here */}
</AbsoluteFill>
);
};
这个范式里有三个设计要点,值得逐一说明:
useDelayRender()是渲染正确性的关键。 字幕是异步加载的外部数据;如果不延迟渲染,Remotion 可能在一帧字幕尚未就绪时就截取了画面。delayRender()返回的handle让渲染进程挂起,直到fetch+ 解析全部成功后调用continueRender(handle)才放行;任何异常则走cancelRender(e)终止,避免渲染出一段"没有字幕"的成品视频。staticFile()负责资源定位。 它把public/目录下的文件映射为可访问的 URL,这是 Remotion 中引用静态资源的官方方式;fetch拿到的是纯文本,直接作为parseSrt的input参数传入。- 远程 URL 同样受支持。 原文档明确说明:不一定要把 SRT 放在本地
public/目录,也可以直接fetch()远程 URL 拿到的文本,然后走完全相同的parseSrt({ input: text })流程——因为parseSrt的输入就是一个普通字符串,与来源无关。
深入 parseSrt():SRT 解析的源码实现
parseSrt() 的完整实现位于 packages/captions/src/parse-srt.ts,代码量不大但逻辑严谨,值得逐段拆解,它能解释"导入后每个字段是怎么来的"以及"哪些输入会直接报错"。
输入输出签名
export type ParseSrtInput = {
input: string; // 完整的 SRT 文件文本
};
export type ParseSrtOutput = {
captions: Caption[];
};
它是同步纯函数,无副作用,这也是它可以在渲染管线中随时调用、方便测试的原因。
时间戳解析:toSeconds()
SRT 的时间格式是 HH:MM:SS,mmm。内部的 toSeconds() 辅助函数按 : 拆分时/分/秒,再按 , 拆分毫秒:
function toSeconds(time: string) {
const [first, second, third] = time.split(':');
// ... 对每一段缺失都抛出 Invalid timestamp 错误
const [seconds, millis] = third.split(',');
// ...
return (
parseInt(first, 10) * 3600 +
parseInt(second, 10) * 60 +
parseInt(seconds, 10) +
parseInt(millis, 10) / 1000
);
}
注意它的严格性:时间戳缺任何一段(没有时、没有分、没有秒、没有毫秒)都会 throw new Error('Invalid timestamp:...')。从源码结构看,parseSrt 本身没有 try/catch,因此一个格式非法的 SRT 文件会让解析抛错——这也正是上方组件示例中 catch 分支调用 cancelRender(e) 的意义所在:格式错误会显式失败,而不是静默产出空字幕。
逐行状态机:四条分支
主循环按行扫描文本,依据"当前行与下一行"的组合判定所处状态,共四条分支:
- 检测到时间戳行(当前行含数字、下一行含
' --> '):拆出起止时间,push一条新字幕。此时text先置空串,待后续文本行填充:由此可以确认两个字段来源:captions.push({ text: '', startMs: start * 1000, endMs: end * 1000, confidence: 1, timestampMs: ((start + end) / 2) * 1000, });confidence固定为1(SRT 文件本身不含置信度信息);timestampMs取起止时间的中点,表示这条字幕的"锚定时刻"。 - 当前行本身就是时间戳行(含
' --> '):continue跳过,避免重复处理。 - 空行:SRT 中每条字幕块以空行结尾,此处对已累积的
text做一次trim(),相当于确认一条字幕收尾。 - 普通文本行:追加到"最后一条字幕"的
text上并加换行符——这意味着 SRT 中的多行文本会被合并进同一条字幕,换行以\n保留。
循环结束后还有一次收尾映射,对每条字幕执行 text.trimEnd(),去掉尾部残留的换行。
测试用例:解析结果的可验证依据
srt.test.ts 用一个三条字幕的样例文件验证了解析结果,可以直接作为"你的 SRT 导入后长什么样"的参照:
输入(节选):
1
00:00:00,000 --> 00:00:02,500
Welcome to the Example Subtitle File!
2
00:00:03,000 --> 00:00:06,000
This is a demonstration of SRT subtitles.
...
期望输出(第一条):
{
confidence: 1,
endMs: 2500,
startMs: 0,
text: 'Welcome to the Example Subtitle File!',
timestampMs: 1250, // (0 + 2500) / 2,中点
}
测试还覆盖了 serializeSrt 的方向:解析后的 Caption[] 可以被序列化为与原输入逐字节一致的 SRT 文本,即 parseSrt → serializeSrt 构成无损往返。如果你导入字幕后还需要二次编辑、清洗或导出(例如基于 pageBreakAfter 强制切分字幕块),这个往返能力会非常有用——serializeSrt 同样在 packages/captions/src/index.ts 中公开导出。
导入之后:Caption[] 与 @remotion/captions 工具链的衔接
原文档最后一节指出:解析完成后,字幕已经处于标准的 Caption 格式,可以配合 @remotion/captions 的全部工具使用。结合仓库中同目录的技能文档,这条链路是:
- 导入 SRT(本文主题):import-srt-captions.md ——
parseSrt()得到Caption[]; - 生成字幕(替代入口):transcribe-captions.md —— 从音视频转录生成同样格式的
Caption[]; - 展示与动画:display-captions.md —— 把
Caption[]交给createTikTokStyleCaptions()按combineTokensWithinMilliseconds组合成分页(TikTokPage),再用<Sequence>逐页渲染,并利用页面内的tokens做逐词高亮。
也就是说,本文导入得到的 Caption[] 可以无缝进入"分页 → Sequence 时间轴定位 → 逐词高亮"的展示流程;pageBreakAfter 字段则允许在 SRT 导入的基础上手动控制换页位置。整条技能链的入口索引见 SKILL.md,其中明确约定"所有字幕都必须以 JSON 化的 Caption 类型流转"。
小结:关键文件与适用前提
- 导入的核心 API 是
parseSrt({ input }),实现见 parse-srt.ts,行为边界(严格时间戳校验、多行文本合并、中点时间戳、置信度为 1)均有 srt.test.ts 佐证; - 资源加载遵循
staticFile()+fetch()+useDelayRender()范式,保证字幕就绪后才出帧,失败则cancelRender显式报错; - 本地
public/文件与远程 URL 两条来源均受支持,解析函数本身与来源解耦; - 适用前提:项目使用当前仓库的包版本(
4.0.521),且remotion与@remotion/*包版本对齐为精确版本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00