首页
/ Remotion 字幕导入实战:用 @remotion/captions 的 parseSrt() 将 .srt 字幕文件接入渲染流程

Remotion 字幕导入实战:用 @remotion/captions 的 parseSrt() 将 .srt 字幕文件接入渲染流程

2026-09-07 16:15:07作者:滑思眉Philip

本篇指南聚焦一个具体场景:当你手上已经有一个现成的 .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>
  );
};

这个范式里有三个设计要点,值得逐一说明:

  1. useDelayRender() 是渲染正确性的关键。 字幕是异步加载的外部数据;如果不延迟渲染,Remotion 可能在一帧字幕尚未就绪时就截取了画面。delayRender() 返回的 handle 让渲染进程挂起,直到 fetch + 解析全部成功后调用 continueRender(handle) 才放行;任何异常则走 cancelRender(e) 终止,避免渲染出一段"没有字幕"的成品视频。
  2. staticFile() 负责资源定位。 它把 public/ 目录下的文件映射为可访问的 URL,这是 Remotion 中引用静态资源的官方方式;fetch 拿到的是纯文本,直接作为 parseSrtinput 参数传入。
  3. 远程 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) 的意义所在:格式错误会显式失败,而不是静默产出空字幕。

逐行状态机:四条分支

主循环按行扫描文本,依据"当前行与下一行"的组合判定所处状态,共四条分支:

  1. 检测到时间戳行(当前行含数字、下一行含 ' --> '):拆出起止时间,push 一条新字幕。此时 text 先置空串,待后续文本行填充:
    captions.push({
      text: '',
      startMs: start * 1000,
      endMs: end * 1000,
      confidence: 1,
      timestampMs: ((start + end) / 2) * 1000,
    });
    
    由此可以确认两个字段来源:confidence 固定为 1(SRT 文件本身不含置信度信息);timestampMs 取起止时间的中点,表示这条字幕的"锚定时刻"。
  2. 当前行本身就是时间戳行(含 ' --> '):continue 跳过,避免重复处理。
  3. 空行:SRT 中每条字幕块以空行结尾,此处对已累积的 text 做一次 trim(),相当于确认一条字幕收尾。
  4. 普通文本行:追加到"最后一条字幕"的 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 文本,即 parseSrtserializeSrt 构成无损往返。如果你导入字幕后还需要二次编辑、清洗或导出(例如基于 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/* 包版本对齐为精确版本。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388