• 人工智能
  • AI Agent
  • 代码智能体
  • 开发工具

【免费下载链接】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.

项目地址: https://gitcode.com/gh_mirrors/st/stagewise
点击查看 免费下载

本文基于仓库内 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 视频中,画面节奏与配音时长脱节是常见的翻车点:口播还没念完,场景已经切换;或者一句旁白结束,画面还僵在那里。本仓库给出的解决方案非常明确:

  1. 按场景(per scene)生成语音:用 ElevenLabs TTS 为每个场景单独合成一段音频;
  2. 写入 public/ 目录:生成 MP3 后通过 staticFile() 在组件内访问;
  3. 用 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

创建一个脚本,它需要完成四件事:

  1. 读取视频配置(如 spec.yaml 中定义的场景文案与配音开关);
  2. 为每个场景调用 ElevenLabs 的 TTS API;
  3. 把返回的 MP3 写入 public/voiceover/<compositionId>/ 目录;
  4. 写入后 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 /> 中不会体现。
已完成
  • 人工智能
  • AI Agent
  • 代码智能体
  • 开发工具

【免费下载链接】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.

项目地址: https://gitcode.com/gh_mirrors/st/stagewise
点击查看 免费下载
Logo

邀请您加入社区

更多推荐