news 2026/9/12 23:54:06

Remotion 如何为视频添加 AI 自动生成的字幕?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Remotion 如何为视频添加 AI 自动生成的字幕?

Remotion 如何为视频添加 AI 自动生成的字幕?

【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion

如果你的 Remotion 项目中有一段带人声的视频,想通过 AI 自动识别语音、生成带时间轴的字幕并渲染到画面上,Remotion 官方提供了完整的操作路径:先用内置的转录方案把音频转成标准的Caption数据(JSON),再在 React 组件里按帧渲染字幕。本文以本地免费的 Whisper.cpp 方案为主路径,给出从安装、转录、格式转换到上屏渲染的完整步骤,并附云 API 的可选分支。

选择转录方案

Remotion 内置了五种把音频转成字幕的方式,官方文档给出了对比:

@remotion/install-whisper-cpp@remotion/whisper-webgpu@remotion/whisper-web@remotion/openai-whisper@remotion/elevenlabs
EnvironmentServer (Node.js)Client (Browser)Client (Browser)Cloud (API)Cloud (API)
SpeedFast (depends on hardware)Fast (depends on GPU)Slow (WASM overhead)FastFast
CostFreeFreeFreePaid (OpenAI API pricing)Paid (ElevenLabs API pricing)
Offline support
Convert functiontoCaptions()toCaptions()toCaptions()openaiWhisperApiToCaptions()elevenLabsTranscriptToCaptions()

五种方案的输出都可以统一转换为Caption类型,从而共用@remotion/captions里的分页、序列化等 API。本文主路径选用@remotion/install-whisper-cpp(服务端本地运行、免费、离线可用);如果不想在服务器上装任何东西,可选用@remotion/openai-whisper调用 OpenAI Whisper API,但需要付费的 OpenAI API,且不支持离线。

安装依赖

在 Remotion 项目根目录执行:

npx remotion add @remotion/install-whisper-cpp @remotion/captions

npx remotion add会安装与你当前remotion版本匹配的包版本(例如remotion@4.0.100的项目会装上对应版本的@remotion/install-whisper-cpp)。@remotion/captions提供createTikTokStyleCaptions()等字幕处理 API。

版本要求方面,@remotion/install-whisper-cpp从 v4.0.115 可用,transcribe()从 v4.0.131 可用,toCaptions()@remotion/captions从 v4.0.216 可用;如果要用large-v3-turbo模型,还需要 Remotion v4.0.229 及以上。

准备 16KHz 音频

transcribe()对输入文件有硬性要求:必须是16-bit、16KHz 的 WAVE 文件。如果手头是 mp4 或其他格式,先用 ffmpeg 转码(官方文档给出的转换示例):

ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y

命令中的/path/to/audio.mp4/path/to/audio.wav替换为你自己的输入和输出路径。-ar 16000把采样率设为 16KHz,-y表示覆盖已存在的输出文件。关于在服务端如何重采样音频,文档指向了 Resample audio to 16kHz 一节。

安装 Whisper.cpp 与模型

@remotion/install-whisper-cpp提供了跨平台的函数,可以直接把 Whisper.cpp 可执行文件和模型下载到本地文件夹,无需手动编译。官方示例采用 Whisper.cpp1.5.5(文档说明这是目前验证可用且支持 token 级时间戳的最新版本)和medium.en模型:

import path from 'path'; import {downloadWhisperModel, installWhisperCpp, transcribe, toCaptions} from '@remotion/install-whisper-cpp'; import fs from 'fs'; const to = path.join(process.cwd(), 'whisper.cpp'); await installWhisperCpp({ to, version: '1.5.5', }); await downloadWhisperModel({ model: 'medium.en', folder: to, }); // Convert the audio to a 16KHz wav file first if needed: // import {execSync} from 'child_process'; // execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y'); const whisperCppOutput = await transcribe({ model: 'medium.en', whisperPath: to, whisperCppVersion: '1.5.5', inputPath: '/path/to/audio.wav', tokenLevelTimestamps: true, }); // Optional: Apply our recommended postprocessing const {captions} = toCaptions({ whisperCppOutput, }); fs.writeFileSync('captions.json', JSON.stringify(captions, null, 2));

执行这个脚本(例如bun run install-whisper.cppnode运行 ESM 脚本)会做三件事:

  1. installWhisperCpp()把 Whisper.cpp1.5.5安装到whisper.cpp/文件夹;
  2. downloadWhisperModel()medium.en模型下载到同一文件夹,文件名为ggml-medium.en.bin。如果文件已存在,函数不做任何事并返回alreadyExisted: true
  3. transcribe()转录音频,toCaptions()做官方推荐的后期处理,最后把结果写入captions.json

理解 transcribe() 的关键参数

transcribe()的完整选项(参见 transcribe() 文档):

  • inputPath:待转录的 16-bit、16KHz WAVE 文件路径。
  • whisperPathwhisper.cpp文件夹路径,即installWhisperCpp()to参数。
  • model:默认为base.en。可选tinytiny.enbasebase.ensmallsmall.enmediummedium.enlarge-v1large-v2large-v3large-v3-turbo。用哪个模型前,先确认它已存在于whisper.cpp/models文件夹(downloadWhisperModel()可以确保模型在本地可用)。
  • tokenLevelTimestamps:传true会给 Whisper.cpp 加--dtw标志,生成更准确的时间戳(返回在t_dtw字段)。文档推荐开启以获得真正精确的时序,但这要求 Whisper.cpp 为 1.5.5 或更新版本;旧版本请设为false
  • language:通过-l标志指定音频语种,取值包括EnglishChineseauto等约 100 种语言或代码。
  • translateToEnglish:设为true可把外语翻译成英文字幕。注意此时不要用*.en后缀的模型,它们无法做翻译;文档建议至少用medium模型以获得可接受的翻译效果。
  • onProgress:进度回调,参数是01之间的数字,可以打印Transcription progress: ${progress * 100}%这类日志。
  • signal:传入AbortController的信号可取消转录。

结果验证:确认 captions.json

toCaptions()会把transcribe()的原始输出转换成Caption[]数组。文档中给出的示例输出(文档示例,实际文本和时间取决于你的音频):

[ { "text": "William", "startMs": 40, "endMs": 420, "timestampMs": 240, "confidence": 0.813602 }, { "text": " just", "startMs": 420, "endMs": 650, "timestampMs": 480, "confidence": 0.990905 }, { "text": " hit", "startMs": 650, "endMs": 810, "timestampMs": 700, "confidence": 0.981798 } ]

Caption类型的字段:

  • text:字幕文本(字符串)。
  • startMs/endMs:起止时间(毫秒)。
  • timestampMs:单个时间戳(毫秒)或null;使用@remotion/install-whisper-cpp时它对应t_dtw值。
  • confidence:0 到 1 之间的置信度,无法提供时为null
  • pageBreakAfter(可选,v4.0.517 起):为true时强制该条字幕后换页。

验证要点:打开生成的captions.json,检查textstartMs/endMs是否覆盖了你音频中实际有语音的时间段。注意text字段对空白敏感,空格(理想情况下每个词前的空格)必须保留——createTikTokStyleCaptions()依赖空格作为分词分隔符,缺失会导致整段文本合并成一行。

在 Remotion 组件中渲染字幕

captions.json放进项目的public/目录后,用useDelayRender()挂起渲染直到字幕加载完成(参见 Displaying captions):

import {useState, useEffect, useCallback} from 'react'; import {AbsoluteFill, staticFile, useDelayRender} from 'remotion'; 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('captions.json')); const data = await response.json(); setCaptions(data); continueRender(handle); } catch (e) { cancelRender(e); } }, [continueRender, cancelRender, handle]); useEffect(() => { fetchCaptions(); }, [fetchCaptions]); if (!captions) { return null; } return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>; };

把字幕分成“页”

createTikTokStyleCaptions()把字幕按时间分组为页(TikTok 风格逐词显示的基础)。combineTokensWithinMilliseconds控制一次显示多少词:值越大一页词越多,越小越接近逐词动画:

import {useMemo} from 'react'; import {createTikTokStyleCaptions} from '@remotion/captions'; import type {Caption} from '@remotion/captions'; // How often captions should switch (in milliseconds) // Higher values = more words per page // Lower values = fewer words (more word-by-word) const SWITCH_CAPTIONS_EVERY_MS = 1200; const captions: Caption[] = []; const {pages} = useMemo(() => { return createTikTokStyleCaptions({ captions, combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS, }); }, [captions]);

返回的每个TikTokPage包含textstartMsdurationMstokens(每个 token 有textfromMstoMs,可用于逐词高亮)。如果想让停顿处自动换页,可以传breakOnSilenceAfterMilliseconds(v4.0.514 起):两条字幕之间间隔达到该毫秒数就提前换页,它只会让页变短、不会超过combineTokensWithinMilliseconds的上限。

用 Sequence 按时间轴渲染

对每一页计算起始帧和时长,放进<Sequence>中:

import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion'; import type {TikTokPage} from '@remotion/captions'; const pages: TikTokPage[] = []; const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => <div>{page.text}</div>; const CaptionedContent: React.FC = () => { const {fps} = useVideoConfig(); return ( <AbsoluteFill> {pages.map((page, index) => { const nextPage = pages[index + 1] ?? null; const startFrame = (page.startMs / 1000) * fps; const endFrame = Math.min(nextPage ? (nextPage.startMs / 1000) * fps : Infinity, startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps); const durationInFrames = endFrame - startFrame; if (durationInFrames <= 0) { return null; } return ( <Sequence key={index} from={startFrame} durationInFrames={durationInFrames}> <CaptionPage page={page} /> </Sequence> ); })} </AbsoluteFill> ); };

逐词高亮的页组件

每个page.tokens里的 token 带fromMs/toMs,可以判断当前词是否正在被念出并改变颜色。字幕容器要加whiteSpace: 'pre'以保留text中的空格:

import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion'; import type {TikTokPage} from '@remotion/captions'; const HIGHLIGHT_COLOR = '#39E508'; const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => { const frame = useCurrentFrame(); const {fps} = useVideoConfig(); // Current time relative to the start of the sequence const currentTimeMs = (frame / fps) * 1000; // Convert to absolute time by adding the page start const absoluteTimeMs = page.startMs + currentTimeMs; return ( <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', }} > <div style={{ fontSize: 80, fontWeight: 'bold', textAlign: 'center', // Preserve whitespace in captions whiteSpace: 'pre', }} > {page.tokens.map((token, tokenIndex) => { const isActive = token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs; return ( <span key={`${token.fromMs}-${tokenIndex}`} style={{ color: isActive ? HIGHLIGHT_COLOR : 'white', }} > {token.text} </span> ); })} </div> </AbsoluteFill> ); };

在 Remotion Studio 里预览或在命令行渲染成片时,字幕会随时间轴出现;文档给出的完整示例(含加载、分页和渲染三部分的完整组件)见 Displaying captions。

可选分支:用 OpenAI Whisper API 转录

如果不想在本机/服务器上跑 Whisper,@remotion/openai-whisper(v4.0.217 起)提供把 OpenAI Whisper API 的返回转换成语料Caption[]的函数openaiWhisperApiToCaptions(),装法同样是:

npx remotion add @remotion/openai-whisper

该方案的转换结果与本地方案共用同一套@remotion/captionsAPI,所以上面的渲染代码不用改。代价是需要按 OpenAI API 计费且必须联网;@remotion/elevenlabs是另一个云端选项,转换函数为elevenLabsTranscriptToCaptions()

限制与下一步

  • 本地方案的输入必须是 16-bit、16KHz WAVE 文件,其他格式先用 ffmpeg 转码。
  • tokenLevelTimestamps: true需要 Whisper.cpp 1.5.5+;旧版本请设为false
  • large-v3-turbo模型要求 2024 年 11 月之后构建的 Whisper.cpp 版本和 Remotion v4.0.229+。
  • 字幕text字段的空格不能丢,渲染时容器需要white-space: pre
  • 美化方面,文档建议用@remotion/layout-utilsfitText()自动缩放文字宽度、给文字加描边(WebkitTextStroke+paintOrder: 'stroke')提高可读性,以及为字幕进出场加动画。
  • 除了自己渲染,也可以把Caption[]serializeSrt()导出为.srt文件,或用parseSrt()解析现有 SRT。
  • 参考文档:Transcribing audio、@remotion/captions API、@remotion/install-whisper-cpp、createTikTokStyleCaptions()。

【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 23:52:05

Arm-2D:面向Cortex-M的确定性2D图形加速框架

1. 为什么在Cortex-M上做2D图形加速&#xff0c;Arm-2D不是“锦上添花”&#xff0c;而是“生死线” 你有没有遇到过这样的场景&#xff1a;在一款带320240 OLED屏的工业HMI设备上&#xff0c;客户突然要求增加一个动态波形图——不是静态图标&#xff0c;是每50ms刷新一次、带…

作者头像 李华
网站建设 2026/9/12 23:50:55

极空间NAS自建Typecho博客:从云服务器迁移到Docker部署实践

1. 为什么我把博客从云服务器搬回了家里的极空间 NAS前阵子我的云服务器又到期了&#xff0c;续费价格直接翻了一倍。这台服务器上只跑着一个 Typecho 博客&#xff0c;平时流量不大&#xff0c;但我每年要为一台几乎闲置的机器付几百块续费。想想觉得挺不值的&#xff0c;干脆…

作者头像 李华
网站建设 2026/9/12 23:44:47

无限画布百万节点性能压测:从卡顿到流畅的选型指南

前一阵帮一个做工业流程可视化的团队做技术选型&#xff0c;他们原本在某款开源无限画布上搭原型&#xff0c;节点数刚过八千就开始明显掉帧&#xff0c;拖拽时连线像橡皮筋一样拉丝&#xff0c;客户来验收那天直接在框选操作时卡了十几秒。后来换了策略&#xff0c;我先帮他们…

作者头像 李华
网站建设 2026/9/12 23:44:45

U-Net眼底血管分割实战:数据、训练与推理全流程解析

简介&#xff1a;一套基于U-Net的眼底血管分割项目包&#xff0c;面向医学图像处理初学者与算法开发人员&#xff0c;解决眼底血管二分割任务从数据准备到训练推理的完整流程。压缩包共216个文件&#xff0c;以182张切片PNG图像为主&#xff0c;另含8个Python脚本、5个XML配置、…

作者头像 李华
网站建设 2026/9/12 23:43:49

Windows 的 A 卡/I 卡用户如何为 RVC 安装并启用 DirectML 依赖?

Windows 的 A 卡/I 卡用户如何为 RVC 安装并启用 DirectML 依赖&#xff1f; 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI Easily train a good VC model with voice data < 10 mins! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrieval-based-Voi…

作者头像 李华