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 | |
|---|---|---|---|---|---|
| Environment | Server (Node.js) | Client (Browser) | Client (Browser) | Cloud (API) | Cloud (API) |
| Speed | Fast (depends on hardware) | Fast (depends on GPU) | Slow (WASM overhead) | Fast | Fast |
| Cost | Free | Free | Free | Paid (OpenAI API pricing) | Paid (ElevenLabs API pricing) |
| Offline support | ✅ | ✅ | ✅ | ❌ | ❌ |
| Convert function | toCaptions() | 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/captionsnpx 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.cpp或node运行 ESM 脚本)会做三件事:
installWhisperCpp()把 Whisper.cpp1.5.5安装到whisper.cpp/文件夹;downloadWhisperModel()把medium.en模型下载到同一文件夹,文件名为ggml-medium.en.bin。如果文件已存在,函数不做任何事并返回alreadyExisted: true;transcribe()转录音频,toCaptions()做官方推荐的后期处理,最后把结果写入captions.json。
理解 transcribe() 的关键参数
transcribe()的完整选项(参见 transcribe() 文档):
inputPath:待转录的 16-bit、16KHz WAVE 文件路径。whisperPath:whisper.cpp文件夹路径,即installWhisperCpp()的to参数。model:默认为base.en。可选tiny、tiny.en、base、base.en、small、small.en、medium、medium.en、large-v1、large-v2、large-v3、large-v3-turbo。用哪个模型前,先确认它已存在于whisper.cpp/models文件夹(downloadWhisperModel()可以确保模型在本地可用)。tokenLevelTimestamps:传true会给 Whisper.cpp 加--dtw标志,生成更准确的时间戳(返回在t_dtw字段)。文档推荐开启以获得真正精确的时序,但这要求 Whisper.cpp 为 1.5.5 或更新版本;旧版本请设为false。language:通过-l标志指定音频语种,取值包括English、Chinese、auto等约 100 种语言或代码。translateToEnglish:设为true可把外语翻译成英文字幕。注意此时不要用*.en后缀的模型,它们无法做翻译;文档建议至少用medium模型以获得可接受的翻译效果。onProgress:进度回调,参数是0到1之间的数字,可以打印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,检查text与startMs/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包含text、startMs、durationMs和tokens(每个 token 有text、fromMs、toMs,可用于逐词高亮)。如果想让停顿处自动换页,可以传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-utils的fitText()自动缩放文字宽度、给文字加描边(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),仅供参考