OpenMontage 字幕接入实战:使用 @remotion/captions 将 SRT 字幕导入 Remotion 合成
导读
本文讲解在 OpenMontage 的 Remotion 合成工作流(remotion-composer)中,如何把已有的 .srt 字幕文件通过 @remotion/captions 包导入并解析为标准 Caption 数据结构,从而复用整套字幕渲染工具链。读完本文,你将掌握依赖安装、staticFile() + fetch() + parseSrt() 的完整接入模式、useDelayRender() 的异步阻塞渲染技巧,以及解析结果与分词高亮、TikTok 风格分页等下游渲染能力的衔接方式。
背景:SRT 导入在 OpenMontage 字幕链路中的位置
OpenMontage 的字幕处理有一条清晰的链路:语音转写 → 字幕生成 → 字幕导入 → 字幕渲染。
- 生成侧:
tools/subtitle/subtitle_gen.py(即subtitle_gen工具)负责把转写结果转换为 SRT、VTT 或带词级时间戳的 caption JSON(参见技能文档 skills/core/subtitle-sync.md); - 渲染侧:
remotion-composer是 OpenMontage 的 Remotion 合成渲染器,其 remotion-composer/package.json 中直接声明了"@remotion/captions": "^4.0.484",与@remotion/cli、remotion等核心依赖版本对齐(均为4.0.x); - 导入侧:当手头已经存在
FFmpeg、YouTube 上传、外部剪辑流程产出的.srt文件时,就需要本文讲解的parseSrt()导入路径,将 SRT 文本转换成 Remotion 可用的Caption数组。
SRT 是行业通用的字幕交换格式,但 Remotion 的字幕 API 统一以 JSON 形式的 Caption 对象为内部数据模型。因此导入的本质是一个格式归一化过程:.srt → 文本 → parseSrt() → Caption[]。归一化之后,所有 @remotion/captions 的实用工具都可以直接消费这些数据。
前置条件:安装 @remotion/captions
首先需要安装 @remotion/captions 包。OpenMontage 的 remotion-composer 已将其列为正式依赖,若在全新项目中尚未安装,官方推荐通过 Remotion CLI 的 add 子命令安装,它会自动挑选与当前 remotion 主包兼容的版本:
npx remotion add @remotion/captions # 项目使用 npm
bunx remotion add @remotion/captions # 项目使用 bun
yarn remotion add @remotion/captions # 项目使用 yarn
pnpm exec remotion add @remotion/captions # 项目使用 pnpm
以 OpenMontage 当前仓库为例,
remotion-composer使用的是 npm 生态,直接在package.json的dependencies中声明即可,remotion add命令会自动同步该依赖。安装后可以从包中引入两个关键导出:解析函数parseSrt()与类型Caption。
第一步:将 .srt 文件放入 public 目录
Remotion 通过 staticFile() 引用 public/ 目录下的静态资源(图片、视频、音频、字体,以及字幕文件)。约定如下:
- 在项目的
public/目录中放置字幕文件,例如public/subtitles.srt; - 组件中通过
staticFile("subtitles.srt")得到可在浏览器与渲染进程中解析的静态 URL; - 通过
fetch()拉取该文件并读取文本内容,再交给parseSrt()。
staticFile() 的底层机制是:本地 remotion studio 预览与 remotion render 渲染时会自动把 public/ 目录映射为静态服务器根路径,因此该写法在开发预览与最终渲染两种场景下行为一致,不会出现路径漂移。
第二步:读取并解析 .srt(完整示例)
下面是原规则文档给出的完整接入代码,展示了标准的"延迟渲染(DelayRender)"异步模式:
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 {
const response = await fetch(staticFile("subtitles.srt"));
const text = await response.text();
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()会"暂停"渲染计时器,通知渲染器"还有异步数据未就绪";数据到达后调用continueRender(handle)放行渲染;一旦fetch或解析失败,则调用cancelRender(e)主动终止渲染并抛出错误。这是 Remotion 中加载外部资源的标准且必须的模式,否则字幕可能因竞态条件而缺失。parseSrt({ input }):@remotion/captions提供的 SRT 解析函数,接受{ input: string },返回{ captions: Caption[] }。它会处理 SRT 的序号行、时间轴行(HH:MM:SS,mmm --> HH:MM:SS,mmm)与多行文本块,并合并时间相邻或语义连续的字幕片段。- 加载态渲染
null:在captions尚未就绪时渲染null,避免把空数据传给下游渲染组件。
远程 URL 同样支持
staticFile() 并非唯一数据来源。规则文档明确指出,远程 URL 同样受支持——直接把 staticFile("subtitles.srt") 替换为远程字幕文件的完整 URL 即可,例如从 CDN、对象存储或字幕托管服务拉取:
const response = await fetch("https://example.com/subtitles.srt");
这使字幕数据可以与视频素材分离部署,适合 OpenMontage 这类由流水线动态产出素材的场景(素材由工具生成后上传到对象存储,字幕随之远程引用)。
第三步:解析结果——Caption 数据结构
parseSrt() 的输出统一为标准 Caption 类型。remotion-best-practices 技能的字幕规则(.agents/skills/remotion-best-practices/rules/subtitles.md)给出了完整定义:
type Caption = {
text: string; // 该字幕块的文本内容
startMs: number; // 起始时间(毫秒)
endMs: number; // 结束时间(毫秒)
timestampMs: number | null; // 单字/单 token 级时间戳(词级数据才有值)
confidence: number | null; // 识别置信度(转写来源才有值)
};
注意两点:
- 对 SRT 导入而言,SRT 文件通常只携带块级起止时间,因此
timestampMs与confidence一般为null; - 对 Whisper 转写(
transcribe()+toCaptions(),见 .agents/skills/remotion-best-practices/rules/transcribe-captions.md)而言,这两个字段会被填充——timestampMs支撑词级高亮,confidence支撑质量过滤。
也就是说,导入 SRT 后得到的 Caption[] 与转写生成的 Caption[] 在类型上完全同构,下游渲染代码无需区分数据来源。
第四步:使用导入的字幕
解析完成后,字幕即处于 Caption 格式,可以使用 @remotion/captions 的全部实用工具。典型的下游能力包括:
4.1 按时间过滤
在渲染某一段画面时,用起止时间过滤出"当前应该显示"的字幕子集,实现字幕与画面的精确对齐。
4.2 TikTok 风格分页与词级高亮
createTikTokStyleCaptions() 可将 Caption[] 按时间窗口聚合为"页面"(TikTokPage),每页包含若干 token,供逐词高亮使用(详见 .agents/skills/remotion-best-practices/rules/display-captions.md)。combineTokensWithinMilliseconds 控制单页单词数量——值越大每页词越多,越小越接近逐词跳动。
4.3 仓库内的同构实践:CaptionOverlay
OpenMontage 的 remotion-composer 已实现了一套与 TikTokPage 理念同构的自研字幕叠加组件 remotion-composer/src/components/CaptionOverlay.tsx,可作为"字幕数据如何渲染"的参考实现:
- 其
WordCaption接口(word/startMs/endMs/pageBreakAfter)与@remotion/captions的 token 模型一致,其中pageBreakAfter专门服务于 CJK 字幕按句读分页; buildPages()按wordsPerPage与分页标记把词流切成页,页的起止时间取自首词与末词;- 渲染层对每个词计算
isActive/isPast状态:当前正在说的词用高亮色 + 光晕,已说过的词用主色,未到的词用半透明色,配合spring()入场动画; - 页面通过
<Sequence from durationInFrames>挂载到时间轴,与规则文档中"用 Sequence 承载页面"的做法一致。
该组件在 remotion-composer/src/Root.tsx 中被注册为独立的 CaptionOverlayOnly 合成(composition),并集成进 remotion-composer/src/CinematicRenderer.tsx 与 remotion-composer/src/TalkingHead.tsx 的主渲染流程中——即使数据来自外部 SRT 导入,也可以直接喂给这类 WordCaption[] 接口。
实操注意事项
- 空白敏感:
Caption.text是空白敏感的,SRT 文本中的空格会原样保留。渲染时若逐词拼接,应使用whiteSpace: "pre"(或pre-wrap)保留词间空格,避免单词粘连。CaptionOverlay组件正是通过whiteSpace: "pre-wrap"配合wordSeparator参数处理空格语言与 CJK 两种场景。 - 字幕后置处理:SRT 中可能包含 HTML 标签(如
<i>、<font>)或破行,parseSrt()解析后建议按业务需要做清洗(去除标签、合并断行),再进入渲染。 - SRT 与其他格式的分工:SRT 适合通用分发(FFmpeg、播放器、YouTube 上传);若需要词级高亮等精细能力,优先使用
subtitle_gen产出的带timestampMs的 caption JSON(见 skills/core/subtitle-sync.md)。SRT 导入路径的价值在于复用存量字幕资产,避免重复转写。 - 错误处理:
fetch失败、SRT 语法非法都会触发cancelRender(e),渲染任务会明确失败而非产出缺失字幕的成片——这符合 OpenMontage 对交付质量的确定性要求。
小结
在 OpenMontage 中接入存量 SRT 字幕的完整路径是:安装 @remotion/captions → 字幕文件放入 public/ → fetch(staticFile(...)) 读取 → parseSrt() 归一化为 Caption[] → 交给 @remotion/captions 工具链或 remotion-composer 的 CaptionOverlay 渲染。整个过程与转写生成字幕共用同一数据模型,让"外部字幕资产"与"内部转写资产"在渲染层完全打通。需要生成而非导入字幕时,可继续阅读 .agents/skills/remotion-best-practices/rules/transcribe-captions.md;需要设计逐词高亮等展示细节时,可参考 .agents/skills/remotion-best-practices/rules/display-captions.md 与仓库内的 CaptionOverlay 实现。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00