Stagewise + Remotion 视频配音实战:用 ElevenLabs 逐场景生成语音并动态对齐画面时长
【免费下载链接】stagewise
The Open Source Agentic IDE. Create and orchestrate coding agents, show app previews and run git workflows. Use your favorite models across all providers.
本文基于仓库内 create-remotion-video 技能 中内置的配音规则文档 voiceover.md 展开。该技能是 Stagewise(开源 Agentic IDE)为 Agent 提供的一站式视频制作流程,涵盖从选风格指南、建项目、写
spec.yaml、做分镜、收集素材到渲染发布的完整链路,配音是其第 3 步「规格定义」中的关键可选环节(spec.yaml中的voiceover: yes/no字段)与实际视频构建的一部分。读完本文,你将掌握:如何为每个场景调用 ElevenLabs TTS 生成 MP3、如何把音频放入public/供 Remotion 的staticFile()引用、如何用calculateMetadata+getAudioDuration按配音真实时长动态计算合成长度,以及配音的导入、修剪、延时、音量、变速、变调等全套控制手段。
一、整体思路:按场景配音 + 动态时长
在 Remotion 视频中,画面节奏与配音时长脱节是常见的翻车点:口播还没念完,场景已经切换;或者一句旁白结束,画面还僵在那里。本仓库给出的解决方案非常明确:
- 按场景(per scene)生成语音:用 ElevenLabs TTS 为每个场景单独合成一段音频;
- 写入
public/目录:生成 MP3 后通过staticFile()在组件内访问; - 用
calculateMetadata动态测算时长:借助getAudioDuration读出每段音频的秒数,乘以 FPS 换算成帧数,累加后动态设置整个合成(Composition)的durationInFrames,让视频长度精确贴合配音。
这一模式还与该技能在「重要规则」中的要求相互印证:如果存在 voiceover,场景时长必须匹配它的长度("If voiceover exist, match it's length for scenes")。也就是说,配音不只是「加上声音」,而是直接驱动每个场景的持续时间,画面为旁白服务。
二、前置条件:TTS 提供商与 API Key
默认情况下,本指南使用 ElevenLabs 作为 TTS 提供商,通过环境变量 ELEVENLABS_API_KEY 注入密钥。用户可以替换为任何能够产出音频文件的 TTS 服务,架构不变,只换 API 调用层。
几个实操要点:
- 若用户没有指定 TTS 提供商,Agent 应默认推荐 ElevenLabs,并主动向用户索要 API Key;
- 生成脚本运行时必须确保环境变量可用,即进程能读到
process.env.ELEVENLABS_API_KEY; - 运行生成脚本使用如下命令(
--strip-types允许直接运行带类型注解的 TS 脚本,无需预先编译):
node --strip-types generate-voiceover.ts
如果你希望走该技能标准的新项目脚手架流程,可参考 SKILL.md 中的项目初始化部分:
npx create-video@latest --yes --blank --no-tailwind my-video
npx remotion studio
三、生成音频:逐场景调用 ElevenLabs API
创建一个脚本,它需要完成四件事:
- 读取视频配置(如
spec.yaml中定义的场景文案与配音开关); - 为每个场景调用 ElevenLabs 的 TTS API;
- 把返回的 MP3 写入
public/voiceover/<compositionId>/目录; - 写入后 Remotion 即可通过
staticFile()在组件中访问这些资源(这也符合该技能「所有图片/视频/音频资源都要复制进public文件夹再使用」的硬性规则)。
单个场景的核心 API 调用如下(来自 voiceover.md):
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
method: "POST",
headers: {
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
text: "Welcome to the show.",
model_id: "eleven_multilingual_v2",
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0.3,
},
}),
});
const audioBuffer = Buffer.from(await response.arrayBuffer());
writeFileSync(`public/voiceover/${compositionId}/${scene.id}.mp3`, audioBuffer);
参数说明:
| 参数 | 作用 | 备注 |
|---|---|---|
voiceId | 使用的 ElevenLabs 声音 ID | 位于 URL 路径中,可在 ElevenLabs 控制台获取 |
xi-api-key | 认证密钥 | 从 process.env.ELEVENLABS_API_KEY 读取 |
model_id | 语音合成模型 | 示例使用 eleven_multilingual_v2,支持多语言 |
stability | 稳定性 | 示例为 0.5,值越高声音越平稳、重复更一致 |
similarity_boost | 与原声音色的相似度 | 示例为 0.75 |
style | 风格化程度 | 示例为 0.3,控制表达的戏剧性与情绪 |
Accept: audio/mpeg | 请求返回 MP3 格式 | 直接写盘为 .mp3 文件 |
由于输出文件按 <compositionId>/<scene.id>.mp3 组织,后续在 calculateMetadata 中可以用「场景 → 音频文件」的对应关系逐段测长,天然支持多场景视频。
四、动态合成时长:calculateMetadata + getAudioDuration
4.1 原理与核心代码
生成好音频后,需要在渲染前动态确定合成时长。这通过 Remotion 的 calculateMetadata 完成,其作用是在渲染前设置合成(Composition)的时长、尺寸、fps、输出文件名或 props(详见 calculate-metadata.md)。
配套的 getAudioDuration 来自 mediabunny 库,它可以在浏览器、Node.js、Bun 中读取音频时长(详见 get-audio-duration.md):
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getAudioDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, { getRetryDelay: () => null }),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
直接使用示例(返回秒数,如 180.5):
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
console.log(duration); // e.g. 180.5 (seconds)
在 Remotion 中配合 staticFile() 使用(音频文件位于 public/ 内):
import { staticFile } from "remotion";
const duration = await getAudioDuration(staticFile("audio.mp3"));
在 Node.js 与 Bun 环境中应改用 FileSource 而非 UrlSource(File 对象来自 input 或 drag-drop):
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({ formats: ALL_FORMATS, source: new FileSource(file) });
4.2 在 calculateMetadata 中计算总时长
将两者组合,即可在渲染前把合成时长对齐到配音总时长(来自 voiceover.md):
import { CalculateMetadataFunction, staticFile } from "remotion";
import { getAudioDuration } from "./get-audio-duration";
const FPS = 30;
const SCENE_AUDIO_FILES = [
"voiceover/my-comp/scene-01-intro.mp3",
"voiceover/my-comp/scene-02-main.mp3",
"voiceover/my-comp/scene-03-outro.mp3",
];
export const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props }) => {
const durations = await Promise.all(
SCENE_AUDIO_FILES.map((file) => getAudioDuration(staticFile(file))),
);
const sceneDurations = durations.map((durationInSeconds) => {
return durationInSeconds * FPS;
});
return {
durationInFrames: Math.ceil(sceneDurations.reduce((sum, d) => sum + d, 0)),
};
};
要点拆解:
staticFile(file)把public/下的相对路径解析为运行时 URL,getAudioDuration通过mediabunny的Input读取时长(秒);- 秒数 × FPS(此处
30)得到帧数,Math.ceil向上取整,避免出现半帧导致时长不足; - 多个场景时长累加得到整个合成(Composition)的
durationInFrames,实现「配音多长,视频多长」。
4.3 把场景时长传给组件
calculateMetadata 计算出的 sceneDurations(每段音频对应的帧数数组)需要通过 props 传入组件,让组件知道每个场景应该持续多少帧,从而安排文字出现、图片切换、动画结束等元素与该场景的旁白严格对齐:
return {
durationInFrames: ...,
props: { ...props, sceneDurations },
};
这里用到的正是 calculateMetadata 的 props 返回字段——它允许在渲染前「变形」组件 props。根据 calculate-metadata.md,calculateMetadata 可以返回的字段全部可选,且返回值会覆盖 <Composition> 上的同名属性:
| 返回字段 | 作用 |
|---|---|
durationInFrames | 总帧数(frame count) |
width / height | 渲染像素尺寸 |
fps | 每秒帧数 |
props | 变形后的组件 props |
defaultOutName | 默认输出文件名(.mp4 自动追加) |
defaultCodec | 默认渲染编码 |
calculateMetadata 还支持异步获取数据并在渲染前改写 props,配合 abortSignal 可在 Studio 中 props 变化时取消过期的请求,这与配音流程中「读取配置 → 动态计算」的用法一脉相承:
const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props, abortSignal }) => {
const response = await fetch(props.dataUrl, { signal: abortSignal });
const data = await response.json();
return { props: { ...props, fetchedData: data } };
};
4.4 使用 TransitionSeries 时的时长修正
如果合成使用 <TransitionSeries>(转场会与相邻场景同时播放、重叠占用时间线,因此总时长比各序列时长之和更短),则必须在总时长中减去转场重叠部分。规则文档 transitions.md 中给出了总时长计算方法:
import { linearTiming } from "@remotion/transitions";
const scene1Duration = 60;
const scene2Duration = 60;
const scene3Duration = 60;
const timing1 = linearTiming({ durationInFrames: 15 });
const timing2 = linearTiming({ durationInFrames: 20 });
const transition1Duration = timing1.getDurationInFrames({ fps: 30 });
const transition2Duration = timing2.getDurationInFrames({ fps: 30 });
const totalDuration =
scene1Duration + scene2Duration + scene3Duration -
transition1Duration - transition2Duration;
规律总结:无转场时两个 60 帧场景合计 120 帧;插入一个 15 帧的转场后合计为 60 + 60 - 15 = 105 帧;而 <TransitionSeries.Overlay>(覆盖层)不缩短时间线,对总时长无影响。若 springTiming 未显式指定 durationInFrames,其时长取决于 fps(弹簧物理收敛需要时间),务必用 getDurationInFrames({ fps }) 读取真实值再参与计算。将配音的总帧数与转场重叠修正合并,即可得到既贴合旁白、又不会因转场重叠而超长的最终时长。
五、在组件中渲染配音音频
配音音频在组件内通过 <Audio> 渲染,需要安装 @remotion/media 包。基础用法(来自 audio.md):
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("audio.mp3")} />;
};
远程 URL 同样可用:
<Audio src="https://remotion.media/audio.mp3" />
默认行为:立即开始、全音量、播放完整长度;可以在同一合成中堆叠多个 <Audio> 实现多音轨(如配音 + 背景音乐)。若想整体校验某帧画面与配音节奏,可使用该技能推荐的单帧渲染检查命令:
npx remotion still [composition-id] --scale=0.25 --frame=30
在 30 fps 下 --frame=30 即第 1 秒处(--frame 从 0 开始),适合快速抽查配音与画面的对位情况。
六、配音的修剪、延时、音量与变速控制
6.1 修剪(trimBefore / trimAfter)
用 trimBefore + trimAfter 裁剪音频片段,值以帧为单位,需要先从 useVideoConfig() 取 fps 换算。音频仍然从合成开始播放,只是只播放选中片段:
const { fps } = useVideoConfig();
return <Audio src={staticFile("audio.mp3")} trimBefore={2 * fps} trimAfter={10 * fps} />;
6.2 延时开始(Sequence 包裹)
这是 voiceover 指南中「Delaying audio start」的标准做法:把 <Audio> 包进 <Sequence from={...}> 即可让配音稍后开始。例如让旁白在第 1 秒后才进入:
import { Sequence, staticFile } from "remotion";
import { Audio } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Audio src={staticFile("audio.mp3")} />
</Sequence>
);
如果你需要更通用的「延时、修剪与设定组件时长」手段,可进一步参考技能中的 sequencing.md。
6.3 音量(静态 / 动态)
静态音量(范围 0–1):
<Audio src={staticFile("audio.mp3")} volume={0.5} />
动态音量(回调参数 f 从音频开始处计为 0,而非合成帧号)。例如前 1 秒淡入:
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);
静音用 muted(也可动态控制,如在第 2–4 秒间静音):
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return <Audio src={staticFile("audio.mp3")} muted={frame >= 2 * fps && frame <= 4 * fps} />;
6.4 变速与变调
变速用 playbackRate,支持加速/减速(不支持倒放):
<Audio src={staticFile("audio.mp3")} playbackRate={2} />
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} />
循环用 loop,loopVolumeCurveBehavior 控制循环时音量曲线的帧号行为:
"repeat"(默认):每轮循环帧号重置;"extend":帧号持续增长。示例:循环播放且音量在 300 帧内从 1 淡出到 0。
<Audio
src={staticFile("audio.mp3")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])}
/>
变调用 toneFrequency,范围 0.01–2,只改变音高、不改变语速:
<Audio src={staticFile("audio.mp3")} toneFrequency={1.5} />
<Audio src={staticFile("audio.mp3")} toneFrequency={0.8} />
注意:变调仅在服务端渲染(server render)时生效,Studio 预览与 <Player /> 中不支持。
七、与技能主流程的衔接
在 create-remotion-video 技能 的完整视频制作流程中,配音决策发生在项目规格阶段:spec.yaml 中通过 voiceover: yes/no 声明是否需要配音,并可与 music、channels、transparent-bg 等字段并列:
title: Feature launch for new credit card
length: 25-35 seconds
content: real life scenes, text overlay, 3d-style animated card in intro, animated outro
voiceover: yes
music: like normal video style
channels: youtube, youtube-shorts, tiktok, linkedin, instagram
transparent-bg: no
style: reuse from skill `my-company-video-style`
若 spec.yaml 发生变化,必须同步重构视频代码。实际构建时按「场景 → 音频文件」的映射生成配音并写入 public/,通过 calculateMetadata 让每个场景的时长跟随旁白,再配合 <Audio> 的修剪、延时与音量控制微调节奏,最终在渲染设置中按该技能推荐的高质量参数(如 CRF=5、JPEG=95)输出成片。
八、常见问题速查
- API Key 未生效:确认启动生成脚本的终端确实导出了
ELEVENLABS_API_KEY,且脚本运行在同一个进程中;node --strip-types generate-voiceover.ts直接读取process.env。 - 时长对不上:检查是否用了
Math.ceil向上取整、是否把秒数乘了 FPS(默认 30);若使用<TransitionSeries>,务必按 transitions.md 的公式减去转场重叠帧数。 - 音频访问不到:确认 MP3 写入了
public/下的正确路径(voiceover/<compositionId>/<scene.id>.mp3),并通过staticFile()而非相对文件路径引用。 - 想换 TTS 服务:仅替换脚本中的 API 调用层,保持「写出 MP3 到
public/→staticFile()引用 →getAudioDuration测长」的链路不变。 - 变调不生效:
toneFrequency只在服务端渲染时有效,Studio 预览和<Player />中不会体现。
【免费下载链接】stagewise
The Open Source Agentic IDE. Create and orchestrate coding agents, show app previews and run git workflows. Use your favorite models across all providers.
更多推荐
所有评论(0)