首页
/ Remotion @remotion/openai-whisper 详解:将 OpenAI Whisper 转写结果转换为 Captions 字幕数据

Remotion @remotion/openai-whisper 详解:将 OpenAI Whisper 转写结果转换为 Captions 字幕数据

2026-09-07 16:10:35作者:宣利权Counsellor

@remotion/openai-whisper 是 Remotion 生态中处理语音转写字幕的轻量工具包,其核心职责是:接收 OpenAI Whisper API 返回的 verbose JSON 转写结果(带逐词时间戳),将其转换为 Remotion 标准 Caption[] 字幕数组,供后续字幕时间轴处理与视频渲染使用。读完本篇,你将掌握该包的完整安装方式、输入/输出数据结构、openAiWhisperApiToCaptions() 的转换算法原理(含标点归属、撇号变体、连字符拆词等边界处理),以及一套可直接复制的 OpenAI SDK 调用示例与测试用例验证方式。

包定位与安装

该包的定位可以用一句话概括(来自 README):"Work with the output of the OpenAI Whisper API",即它不直接调用 Whisper API,而是处理 API 的输出结果。包的元信息定义在 package.json:包名为 @remotion/openai-whisper,当前仓库版本为 4.0.521,MIT 协议,运行时仅依赖 @remotion/captions(用于 Caption 类型),测试则基于 bun test src 运行。

安装命令

npm install @remotion/openai-whisper --save-exact

README 中特别强调了版本对齐原则,安装 Remotion 包时必须让项目中所有 remotion@remotion/* 包保持同一版本:去掉版本号前的 ^ 字符,锁定为精确版本(这也是上面使用 --save-exact 参数的原因)。

包入口 src/index.ts 对外导出 3 个 API 与 1 个类型:

export {
  OpenAiToCaptionsInput,
  OpenAiToCaptionsOutput,
  openAiWhisperApiToCaptions,
} from './openai-whisper-api-to-captions';

export {OpenAiVerboseTranscription} from './openai-format';

输入格式:OpenAiVerboseTranscription

转换函数的输入类型在 openai-format.ts 中定义,它对应 OpenAI Whisper API 在 response_format: 'verbose_json'timestamp_granularities: ['word'] 时的返回结构:

字段 类型 必填性 说明
duration number | string 必填 音频时长(API 可能返回字符串)
language string 必填 识别出的语言,如 english
text string 必填 完整转写文本,转换算法以此为匹配基准
task 'transcribe' 可选 任务类型,非 transcribe 时函数会抛错
words TranscriptionWord[] 可选(本包实际必需 逐词时间戳数组
segments TranscriptionSegment[] 可选 分段信息,本包转换未直接使用

其中逐词结构 TranscriptionWord 只有三个字段,时间单位为(浮点数):

export interface TranscriptionWord {
  end: number;
  start: number;
  word: string;
}

TranscriptionSegment 则包含 Whisper verbose 格式的完整分段元数据(idavg_logprobcompression_rationo_speech_probseektemperaturetokens 等),从源码结构看它只是为类型完整性而声明,转换逻辑并不消费分段数据。

核心 API:openAiWhisperApiToCaptions

函数签名定义在 openai-whisper-api-to-captions.ts

export type OpenAiToCaptionsInput = {
  transcription: OpenAiVerboseTranscription;
};

export type OpenAiToCaptionsOutput = {
  captions: Caption[];
};

export const openAiWhisperApiToCaptions = ({
  transcription,
}: OpenAiToCaptionsInput): OpenAiToCaptionsOutput => {
  // ...
};

端到端使用示例

测试文件 get-and-convert.test.ts 中给出了真实的 API 调用路径(该用例在 CI 环境外会实际调用 OpenAI API,使用仓库内 dialogue.wav 作为音频素材),整理后完整流程如下:

import fs from 'fs';
import OpenAI from 'openai';
import {openAiWhisperApiToCaptions} from '@remotion/openai-whisper';

const openai = new OpenAI();

// 1. 调用 Whisper API,必须要求 word 级时间戳
const transcription = await openai.audio.transcriptions.create({
  file: fs.createReadStream('./dialogue.wav'),
  model: 'whisper-1',
  response_format: 'verbose_json',
  prompt: 'Hello, welcome to my lecture.',
  timestamp_granularities: ['word'], // 关键:缺少此参数本包无法工作
});

// 2. 转换为 Remotion Caption[]
const {captions} = openAiWhisperApiToCaptions({transcription});

timestamp_granularities: ['word'] 是硬性前提:如果不带 words 字段,转换函数会直接抛出 The transcription does need to be been generated with 'timestamp_granularities: ["word"]' 错误(见 源码 L34-L44)。

转换结果示例

基于测试夹具 output.ts 中一段 170 秒的播客真实转写,转换结果的前几条为:

{
  captions: [
    {confidence: null, endMs: 7039.999961853027, startMs: 6519.999980926514, text: "What's", timestampMs: 6779.9999713897705},
    {confidence: null, endMs: 7559.999942779541, startMs: 7039.999961853027, text: ' up,', timestampMs: 7299.999952316284},
    {confidence: null, endMs: 7880.000114440918, startMs: 7619.999885559082, text: ' everybody?', timestampMs: 7750},
    {confidence: null, endMs: 8300.000190734863, startMs: 8239.999771118164, text: ' This', timestampMs: 8269.999980926514},
    // ...
  ],
}

注意一个易被忽略的细节:Whisper 的 words 只有秒级起止时间,而 text 字段(完整转写文本)承载了标点信息。转换算法的工作就是把两者对齐,让每条 Captiontext 带上它"拥有"的标点(如 "What's" 后的逗号归到 up,)。

输出结构:Caption 类型

输出元素的类型 Caption 来自依赖包 @remotion/captions,定义在 caption.ts

export type Caption = {
  text: string;
  startMs: number;
  endMs: number;
  timestampMs: number | null;
  confidence: number | null;
  pageBreakAfter?: boolean;
};

本包的填充规则(见 源码 L70-L76):

  • startMs / endMsword.startword.end 乘以 1000,从秒转为毫秒;
  • timestampMs:取 (start + end) / 2 的中点时刻(毫秒),用于在渲染层定位字幕显示时机;
  • confidence:固定为 null——Whisper 的逐词数据本身不携带逐词置信度(分段级的 avg_logprob 未被采用),因此该字段留空;
  • text:不是直接取 word.word,而是匹配算法从完整文本中切出的片段(含标点,见下节)。

转换算法原理:剩余文本扫描与标点归属

理解这个包的关键在于它不是简单地"一个 word 生成一条 Caption"。完整算法在 openai-whisper-api-to-captions.ts 中,可以拆解为五步:

1. 前置校验

if (!transcription.words) {
  if (transcription.task && transcription.task !== 'transcribe') {
    throw new Error(`The transcription does need to be a "transcribe" task. ...`);
  }
  throw new Error('The transcription does need to be been generated with `timestamp_granularities: ["word"]`');
}

先区分"任务类型错误"与"缺少词级时间戳"两种错误,给出可诊断的报错信息。

2. 首词修剪(issue #5031)

if (firstWord) {
  word.word = word.word.trimStart();
}

某些(第三方/兼容型)Whisper API 会在第一个词前面多带一个空格(如 " Hello")。测试文件 foreign-api.test.ts 就是针对这一类输入:每个词都带前导空格的转写结果,最终首条 Caption 的 text 被规整为 'Hello' 而非 ' Hello'

3. 逐词构建正则,在"剩余文本"中匹配

算法维护一个 remainingText(初始为 transcription.text),对每个词构造如下正则(见 源码 L56-L68):

const punctuation = `\\?,\\.\\%\\–\\!\\;\\:\\'\\\"\\-\\_\\(\\)\\[\\]\\{\\}\\@\\#\\$\\^\\&\\*\\+\\=\\/\\|\\<\\>\\~\`\\u2018\\u2019\\u02bc\\uff07`;
const wordToMatch = word.word.replace(new RegExp(`^[${punctuation}]+`), '');
const match = new RegExp(
  `^([\\s?${punctuation}]{0,4})${escapeWordForRegex(wordToMatch)}([${punctuation}]{0,3})?`,
).exec(remainingText);

正则的三个组成部分各有用意:

  1. 前导组 ([\s?<标点>]{0,4}):允许词前最多 4 个空白/标点字符(空格、逗号、连字符、货币符号等),这些字符归属于当前词——例如 " up," 的前导空格、"It's" 后面的词 ' up,' 携带的逗号;
  2. 词本体escapeWordForRegex 对词做正则转义,但有一个特殊处理——撇号的 5 种 Unicode 变体(U+0027 直引号、U+2018U+2019 弯引号、U+02BC 修饰符撇号、U+FF07 全角)在词与文本之间互相等价匹配,见 源码 L16-L27。这解决了 issue #7298:Whisper 有时在 words 里用弯引号 Let's,在 text 里用直引号 Let'sregressions.test.ts 的 "Issue 7298 - apostrophe variants" 用例专门验证了这一点;
  3. 后缀组 ([标点]{0,3})?:词后最多 3 个标点,归入该词的 text——如句末的 .?!、百分号 %(回归用例 "it is 99% better" 中 99 的 Caption 文本是 ' 99%')。

匹配成功后,remainingText 从匹配片段末尾继续,保证每个词只消费一次文本,且整体是顺序推进的。

4. 匹配失败即抛错

如果某个词在剩余文本中找不到,函数会抛出携带上下文的错误(源码 L61-L65),提示词是什么、剩余文本前 100 字符是什么,并引导用户提交 issue。这是"快速失败"设计:宁可直接报错,也不产出时间戳错位的字幕。

5. 生成 Caption 并推进

匹配片段 match[0] 直接作为 text 写入 Caption,时间字段按前文规则换算为毫秒。

边界情况与测试证据

包内 7 个测试文件(src/test/)恰好覆盖了转换算法的所有难点分支,是验证各边界行为最直接的证据:

测试文件 覆盖场景 关键断言
issue-50069.test.ts 连字符拆词 "Like-minded."、千分位数字 "50,000." Like-minded. 各自成条;50,000. 各自成条——前导 -, 通过前导标点组归入后续词
partial-word.test.ts 词间时间间隙、句末标点 massive 的 Caption 为 ' massive.',句号由后缀组捕获
regressions.test.ts 99%real-time 连字符、撇号变体 99%% 归后缀;real 后跟 -- 归入后续 time 词;Let's(U+2019)能匹配文本中的 Let's(U+0027)
special-chars.test.ts 货币符号 $500 $ 经前导标点组归入当前词,最终文本为 ' $500'
foreign-api.test.ts 第三方 API 词带前导空格 首词 trimStart 后输出 'Hello',其余词的空格被前导组吸收
get-and-convert.test.ts 完整真实转写(170 秒播客)+ 真实 API 调用 前 10 条 Caption 精确断言;非 CI 环境实际请求 whisper-1 并断言 captions.length > 60

这些用例说明该函数设计目标非常明确:容错地吸收 Whisper(含兼容 API)输出中的空格、标点、Unicode 变体差异,但拒绝在无法确定归属时静默产出错误数据

与 Remotion 字幕体系的衔接

Caption[] 是 Remotion 字幕体系的通用中间格式:本包通过 @remotion/captionspackage.json 中声明的 workspace 依赖)导入该类型,保证输出与 @remotion/captions 的时间轴工具(如字幕分句、分页、时间轴计算)类型兼容。一条典型的数据流是:

音频文件 → OpenAI Whisper API (verbose_json + word 时间戳)
        → openAiWhisperApiToCaptions()
        → Caption[] (毫秒级, 带标点)
        → @remotion/captions 时间轴工具 / 你的 React 组件渲染

使用前提与限制

结合源码与测试,使用该包时需要满足以下前提:

  1. 必须带词级时间戳:API 调用需设置 response_format: 'verbose_json'timestamp_granularities: ['word'],且任务为 transcribe,否则函数抛错;
  2. textwords 必须来自同一次转写:算法依赖两者字符级对齐,文本对不上会触发匹配失败异常;
  3. 置信度不可得:输出的 confidence 恒为 null,若需要置信度应另行使用分段级数据;
  4. 版本锁定:与项目中其他 remotion / @remotion/* 包保持同一精确版本(如当前仓库版本 4.0.521)。

关键文件索引

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390