用 Remotion 程序化生成字幕:@remotion/install-whisper-cpp 安装与调用 Whisper.cpp 完整实战指南
本指南以 Remotion 官方 monorepo 中的 @remotion/install-whisper-cpp 包为核心,系统讲解如何在 Node.js/Remotion 项目中自动化安装 Whisper.cpp(C 语言版语音转写引擎)、下载 GGML 模型、执行转写,并把结果转换为可用于 Remotion 视频字幕渲染的 Caption 数据。读完本文,你将掌握从"零环境"到"输出逐词对齐字幕数据"的完整调用链,并理解其中每个 API 的参数含义与底层实现逻辑。
该包目前版本为 4.0.521,package.json 中将其描述为 "Helpers for installing and using Whisper.cpp",声明依赖仅有 @remotion/captions(字幕数据基础类型),自身不包含任何二进制运行时,而是通过子进程调度系统上的 git、make、powershell 与 Whisper.cpp 可执行程序完成工作。
一、包能做什么:四个核心 API 与整体数据流
从包的唯一入口 src/index.ts 可以看到对外导出的全部能力:
installWhisperCpp()—— 在本地安装 Whisper.cpp 可执行程序;downloadWhisperModel()与WhisperModel类型 —— 下载指定语音模型(GGML 权重文件);transcribe()与TranscriptionJson、TranscribeOnProgress—— 调用 whisper-cli 把音频转写成结构化 JSON;toCaptions()—— 把转写 JSON 映射为@remotion/captions的Caption[];Language类型与convertToCaptions()—— 语言枚举,以及自 v4.0.216 起被标记为@deprecated的旧版转换函数。
一条典型的端到端数据流是:
Node 脚本
├─ installWhisperCpp() → 得到 whisper.cpp 可执行文件(git clone / 下载 ZIP + 编译)
├─ downloadWhisperModel()→ 得到 ggml-<model>.bin 权重文件
├─ transcribe() → spawn whisper 二进制,产出完整 JSON(含逐词时间戳)
└─ toCaptions() → 映射为 Caption[](text / startMs / endMs / timestampMs / confidence)
└─ @remotion/captions 的 createCaptions()/createTikTokStyleCaptions() 二次加工
二、安装与版本对齐
README 给出的安装命令是:
npm install @remotion/install-whisper-cpp --save-exact
Remotion 对版本一致性有严格要求:安装任何 Remotion 包时,都要让所有 remotion 与 @remotion/* 包保持同一版本;使用 --save-exact(或将版本号中的 ^ 去掉)可以锁定精确版本,避免因包管理器解析到不同次要版本而出现运行时版本不匹配。在本仓库的开发环境下,该包以 workspace:* 形式依赖 @remotion/captions,确保两个包同步迭代。
注意:该包只负责"调用"Whisper.cpp。真正的转写引擎、模型文件都需要在运行时由下面两个 API 首次落地,因此首次使用往往伴随较大的下载量(模型体积见第四节)。
三、installWhisperCpp:一键获取可执行的 Whisper.cpp
函数签名位于 install-whisper-cpp.ts:
const {alreadyExisted} = await installWhisperCpp({
version: '1.5.5', // 要安装的 Whisper.cpp release 版本
to: './whisper.cpp', // 安装目标目录(相对 process.cwd())
printOutput: true, // 是否把子进程输出转发到控制台,默认 true
signal, // AbortSignal,用于中途取消安装
});
返回值与幂等逻辑
返回值只有 {alreadyExisted: boolean}。实现上是幂等的:如果 to 目录已存在且其中已包含目标版本的可执行文件,会打印 Whisper already exists at <to> 并返回 {alreadyExisted: true},不会重复下载;但如果目录存在而可执行文件缺失,则会抛出错误提示"目录存在但可执行文件缺失,请删除后重试"。值得注意的细节是:printOutput: false 时这类异常会以返回值 {alreadyExisted: false} 的形式静默吞掉(见源码 L187-L206),适合在无日志需求的自动化流程中使用。
不同平台的安装策略
源码中安装逻辑按操作系统分支:
- macOS 与 Linux(Unix):依次执行
git clone https://github.com/ggerganov/whisper.cpp.git <to>、git checkout <ref>、make(见 install-whisper-cpp.ts)。ref的取值规则是:如果version形如语义化版本(正则/^[\d]{1}\.[\d]{1,2}\.+/判定),则拼成v1.5.5这种 tag 去 checkout;否则把整个字符串当作 git ref 使用,也就是说你既可以用'1.5.5'也可以用'main'这样的分支名。 - Windows:不支持从源码构建,改为下载预编译产物
whisper-bin-x64.zip,再调用powershell.exe执行Expand-Archive解压到目标目录(见 install-whisper-cpp.ts)。Windows 分支对版本格式更严格——非语义化版本会直接抛错,因为必须从发布资产中取对应的 ZIP。 - 其他平台:抛出
Unsupported platform: <platform>。
版本如何决定可执行文件名
getWhisperExecutablePath()(install-whisper-cpp.ts)揭示了一个易踩坑的版本差异:
- Whisper.cpp 低于
1.7.4时,可执行文件是仓库根目录下的main(Windows 为main.exe),注释明确指出main已被官方弃用; 1.7.4及以上版本改用whisper-cli,且可执行文件被移动到build/bin/子目录下(Windows 为whisper-cli.exe)。
版本比较由自研的 utils.ts 中的 compareVersions() 完成(逐段解析 x.x.x 并补齐段位比较)。因此传入的 version 不仅决定从哪里取安装源,还决定后续 transcribe() 去哪里找二进制,务必与实际安装结果保持一致。
仓库在 Windows 上专门有一个集成测试 install-whisper-cpp.test.ts,验证在包含空格、$、&、单引号与反引号等 PowerShell 特殊字符的目录路径下安装仍能成功,且临时下载的 whisper-bin-x64.zip 会被清理——这正是真实项目路径中容易出问题的场景。
四、downloadWhisperModel:选择并下载语音模型
安装好引擎后还需权重模型。downloadWhisperModel() 位于 download-whisper-model.ts,签名:
const {alreadyExisted} = await downloadWhisperModel({
model: 'base.en', // 模型名,见下表
folder: './whisper.cpp/models', // 存放目录(相对 process.cwd())
printOutput: true,
onProgress: (downloaded, total) => void, // 下载进度回调
signal,
});
支持的模型与真实体积
WhisperModel 是一个字面量联合类型,包含官方 openai/whisper 全系模型。源码的 modelSizes 表中精确记录了每个模型的字节数,可供下载后校验文件完整性:
| 模型 | 字节数 | 说明 |
|---|---|---|
tiny / tiny.en |
约 77,691,713 / 77,704,715(≈74 MiB) | 最快、最省内存,精度最低 |
base / base.en |
约 147,951,465 / 147,964,211(≈141 MiB) | 通用英文常用起点 |
small / small.en |
约 487,601,967 / 487,614,201(≈465 MiB) | 精度/速度折中 |
medium / medium.en |
约 1,533,763,059 / 1,533,774,781(≈1.4 GiB) | 高精度、资源占用高 |
large-v1 / large-v2 / large-v3 |
各约 3,094,6xx,xxx–3,095,0xx,xxx(≈2.9 GiB) | 最大系列 |
large-v3-turbo |
约 1,624,555,275(≈1.5 GiB) | large 精度、turbo 速度 |
模型文件统一命名为 ggml-<model>.bin,下载源是 whisper.cpp 官方发布的模型仓库(getModelPath() 也暴露了"文件名即约定"这一实现事实)。
幂等与校验逻辑
- 若目标文件已存在且字节数与上表完全一致,直接打印
Model already exists at ...并返回{alreadyExisted: true}; - 若已存在但字节数不一致,
printOutput为 true 时抛错提示"预期 N 字节、实际 M 字节,请删除重试"; - 传入不在列表中的模型名会抛错并列出全部可用值。
进度打印由 download.ts 的 downloadFile() 承担:基于 fetch 流式下载,printOutput 开启时每累计 10 MiB 打印一次百分比;onProgress(downloadedBytes, totalBytes) 与打印无关,始终会按流写入节奏回调,可用来驱动自己的进度条。模型文件巨大,建议优先选 base.en/small.en 这类小体积模型验证流程。
五、transcribe:把音频交给 whisper-cli 并拿到 JSON
核心转写函数位于 transcribe.ts。完整参数如下:
const json = await transcribe({
inputPath: './audio.wav', // 必填,输入音频,必须是 WAV
whisperPath: './whisper.cpp', // 必填,安装目录
whisperCppVersion: '1.7.4', // 必填,用于定位 main/whisper-cli
model: 'base.en', // 必填,须已通过 downloadWhisperModel 下载
tokenLevelTimestamps: true, // 必填,开启逐词时间戳(泛型开关,会影响返回类型)
modelFolder: './whisper.cpp/models', // 可选,模型目录,默认同 whisperPath
translateToEnglish: false, // 可选,-tr,把非英语翻译成英语
printOutput: true, // 可选,透传子进程 stdout/stderr
tokensPerItem: null, // 可选,每段最大 token 数;tokenLevelTimestamps 模式下不可用(类型层面禁止)
language: 'auto', // 可选,语言名或代码,见 Language
splitOnWord: false, // 可选,--split-on-word,按词切分
signal, // 可选,AbortSignal
onProgress: (p: number) => void, // 可选,0~1 转写进度
flashAttention: false, // 可选,--flash-attn true
additionalArgs: [], // 可选,string[] 或 [string,string][],追加原生 CLI 参数
});
输入音频的硬性要求
函数开头做了三道校验,直接决定你能否跑通:
whisperPath必须存在,否则提示检查是否调用过installWhisperCpp();inputPath必须存在;inputPath必须以.wav结尾(按扩展名判断),且 Whisper 要求 16 kHz 采样率——源码捕获到 stderr 包含must be 16 kHz时会抛出专门错误信息。文档内给出的标准转换命令是:
ffmpeg -i input.mp4 -ar 16000 output.wav -y
底层是如何驱动 whisper 的
实现上 transcribe() 先写入一个临时 JSON 路径(./tmp),然后 spawn 可执行文件并拼装如下 CLI 参数(见 transcribe.ts):
-f <音频> --output-file <临时路径> --output-json -ojf(完整JSON)
[-m <模型路径>] [-pp 打印进度] [-tr 翻译] [-l <language小写>]
[--dtw <dtw别名>] [--split-on-word <bool>] [--flash-attn true] [额外参数]
几个值得展开的点:
- 逐词时间戳依赖 DTW:
tokenLevelTimestamps: true时会附加--dtw,并把模型名转换成 DTW 别名(modelToDtw():large-v3-turbo → large.v3.turbo、large-v3 → large.v3、large-v2 → large.v2、large-v1 → large.v1,其余模型原样返回)。这是官方 whisper.cppmain示例用于对齐词级时间戳的模式,源码中注释了对应的上游实现出处。 - 模型缺失提前报错:spawn 之前会检查
ggml-<model>.bin是否存在,否则提示阅读downloadWhisperModel()文档先下载模型。 - 进度解析:从子进程 stdout/stderr 中匹配
progress =文本并解析浮点,回调onProgress(progress/100)。 - 结束判定:以"预测的 JSON 输出文件已生成"为准(而不是进程退出码,因为 whisper 存在"即使出错也返回 0"的历史问题,源码注释引用了上游 PR 佐证);若因
AbortSignal被杀会抛Process was killed with signal ...。另外对 macOS Metal 不可用时的挂起场景有专门的规避逻辑(检测到ggml_metal_free: deallocating且已出现真实进度时才 kill 进程,避免误杀导致返回过期数据)。 - 临时 JSON 在读取后会被
fs.unlinkSync()清理,不污染工作目录。
返回结构 TranscriptionJson
返回的 TranscriptionJson<HasTokenLevelTimestamps>(transcribe.ts)包含 systeminfo、model、params、result 与 transcription 数组。当泛型参数为 true(开启逐词时间戳)时,每个元素还带 tokens,其中 t_dtw 是 DTW 对齐后的逐词毫秒级时间戳、p 是置信度;offsets.from/to 是片段起止时间。
六、toCaptions:把转写 JSON 映射为 Remotion Caption
拿到 JSON 后,用 to-captions.ts 的 toCaptions() 转换为 @remotion/captions 的标准结构:
const {captions} = toCaptions({
whisperCppOutput: json, // 需要 tokenLevelTimestamps: true 的输出
});
// captions: Caption[]
// { text, startMs, endMs, timestampMs: number|null, confidence: number }
映射规则可以从实现精确还原:跳过空文本片段;每个片段生成一条字幕,startMs/endMs 取自 offsets.from/to;timestampMs 取自第一个 token 的 t_dtw * 10,若 t_dtw === -1(无有效对齐)则为 null;confidence 取第一个 token 的 p。
转换出的 Caption[] 可以直接进入 @remotion/captions 的字幕流水线继续加工,例如调用其 createTikTokStyleCaptions() 等分页/组合函数生成逐屏字幕。仓库测试 convert-to-captions.test.ts 展示了这一典型接法:toCaptions() 的输出喂给 createTikTokStyleCaptions({captions, combineTokensWithinMilliseconds: 200}),并断言了每个字幕页面的 text/startMs/durationMs/tokens。
注意弃用项 convertToCaptions
旧版 convertToCaptions({transcription, combineTokensWithinMilliseconds}) 仍以兼容导出存在(index.ts),但从 v4.0.216 起被 @deprecated,官方明确建议改用 toCaptions()。两者差异:旧的返回结构是 {text, startInSeconds}(以秒为单位的起始时间、按 token 阈值手工合并),且没有毫秒级结束时间与置信度,无法与 @remotion/captions 的类型无缝对接;测试中以 0ms 阈值调用它得到的是逐词 startInSeconds 列表。新项目一律使用 toCaptions()。
七、语言参数:Language 联合类型
transcribe() 的 language 参数类型来自 languages.ts,覆盖 Whisper 官方支持的全量语言,可传完整名称('Chinese'、'German'…)或 ISO 代码('zh'、'de'…),另有 'auto' 表示自动检测。底层会把该值 toLowerCase() 后作为 -l 参数传给 whisper-cli。如果你的音频非英语且不传语言,建议显式设置语言或 'auto',否则模型需自行探测语言,可能影响首次识别质量。
八、错误与边界情况速查
以上源码与测试可以总结出如下可预期的运行时行为:
| 场景 | 行为(printOutput 默认 true) |
|---|---|
to 目录已存在且包含可执行文件 |
跳过安装,返回 {alreadyExisted: true} |
to 目录已存在但缺可执行文件 |
抛错提示删除目录后重试 |
| 模型文件已存在且字节数一致 | 跳过下载,返回 {alreadyExisted: true} |
| 模型文件已存在但字节数不符 | 抛错提示删除后重试 |
输入不是 .wav |
抛错并给出 ffmpeg 转换命令 |
| wav 采样率非 16 kHz | 捕获 whisper stderr 后抛专门错误 |
| 模型未下载 | 在 spawn 前抛错,提示先调用 downloadWhisperModel() |
| 进程被 AbortSignal 终止 | 抛 Process was killed with signal ... |
九、在 Remotion 生态中的实际应用场景
@remotion/install-whisper-cpp 解决的正是"程序化视频"生产线上"给素材生成配音字幕/网红式(TikTok 风格)字幕"这一环:开发者可以在构建 Remotion 视频的脚本或 Lambda/服务端渲染流程里,先安装引擎与模型,对配音 WAV 做转写,再通过 toCaptions() + @remotion/captions 的字幕工具把结果变成带时间轴的字幕数据,最终由 Remotion 组件逐屏渲染到画面上。整个过程无需手动运行 whisper 命令行、无需解析其自定义输出格式,全部由类型安全的 TypeScript API 封装,并支持 AbortSignal 取消、下载/转写双进度回调等长任务必备能力。
如果想继续深入,可阅读本包全部实现:download.ts(下载流与进度)、install-whisper-cpp.ts(平台安装)、transcribe.ts(whisper-cli 驱动)、to-captions.ts(字幕映射),以及两份测试 convert-to-captions.test.ts 与 install-whisper-cpp.test.ts 中可运行的真实数据样例。
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