用 Remotion + AI 语音合成,零基础搭建一个教育视频的完整实践
一、背景与动机
我一直在研究视频自动生成的低成本方案——用代码替代人工剪辑,用 AI 替代配音和字幕,目标是让"写一篇文章就能生成一段视频"成为现实。之前折腾过 FFmpeg 模板渲染、Python 脚本拼接片段,但总觉得控制力不够,直到关注到 Remotion 这个项目:它用 React 组件描述每一帧画面,TypeScript 精确控制动画、音频、字幕,等于把视频变成了一个前端工程。
刚好早上和家里小朋友聊天,说起他们班上同学玩游戏的事——有打职业的,也有沉迷被收手机的。聊着聊着就想:要不拿这个话题做个实验?用 Remotion 走一遍全流程,看看代码生成视频的可行性到底到哪一步。
这篇文章就是这次实验的完整记录:从零搭建一个"初中生玩游戏的利弊与自控力培养"教育视频,涉及 Remotion 场景编排、AI 语音合成选型(TTS)、音频集成、字幕系统等所有环节。项目代码已开源,所有步骤可复现。
二、环境准备
整个项目的技术栈主要有三层:视频渲染引擎(Remotion + React)、语音合成(TTS)和 音频处理(FFmpeg)。项目目录结构如下:
remotionai/
├── scripts/ # TTS 语音生成脚本
│ ├── generate_audio_voxcpm.py # VoxCPM2 生成(当前在用)
│ └── generate_audio_edge.py # Edge-TTS 备选方案
├── public/
│ ├── audio/ # 旁白 + 背景音乐
│ │ ├── bgm.wav
│ │ ├── scene1_intro.mp3
│ │ ├── scene2_pros.mp3
│ │ ├── scene3_cons.mp3
│ │ ├── scene4_selfcontrol.mp3
│ │ └── scene5_ending.mp3
│ ├── images/ # Unsplash 背景图
│ └── subtitles/subtitles.srt # SRT 字幕文件
├── src/
│ ├── Composition.tsx # 主编排组件
│ ├── Root.tsx # 注册入口
│ └── scenes/ # 5 个场景组件
│ ├── themes.ts # 配色 + 时长配置
│ ├── subtitles.ts # 字幕帧数据
│ ├── SubtitleOverlay.tsx # 字幕叠加组件
│ ├── Scene1Intro.tsx
│ ├── Scene2Pros.tsx
│ ├── Scene3Cons.tsx
│ ├── Scene4SelfControl.tsx
│ └── Scene5Ending.tsx
├── package.json
└── remotion.config.ts1. 核心依赖
| 包 | 版本 | 用途 |
|---|---|---|
remotion + @remotion/cli |
4.0.484 | 视频渲染引擎 |
react |
19.2.3 | UI 组件 |
typescript |
5.9.3 | 类型系统 |
voxcpm |
2.0.3 | 中文 TTS 语音合成 |
| FFmpeg | 最新 | 音频格式 / 背景音乐合成 |
2. Remotion 的核心机制
Remotion 的工作方式很特别:它不是"播放"视频,而是逐帧渲染——每一帧都会重新调用 React 组件,渲染出该帧的画面。这意味着所有动画都必须是帧编号(frame)的函数,不能用 CSS transition 或 @keyframes。关键的 API 就两个:
useCurrentFrame()— 获取当前帧编号interpolate()— 将帧编号映射到动画值(透明度、位移、缩放等)
三、实战步骤
整个项目的开发分为四个环节:语音生成 → 场景组件 → 音视频集成 → 字幕系统。下面逐一展开。
1. 语音旁白生成——VoxCPM2
这是我踩坑最多的环节,先后试了 4 种方案才定下来。最终胜出的是清华开源的 VoxCPM2(2B 参数,Apache 2.0 协议),支持 30 种语言,中文发音自然度远超 macOS 的 say 命令,而且能在本地 CPU 上跑(~2.5 it/s)。
安装和生成脚本(Python):
# 安装 VoxCPM2
pip3 install voxcpm
# 修复依赖兼容性(实测需要)
pip3 install "torch==2.11.0"
pip3 install "torchvision==0.26.0"
pip3 install "tokenizers>=0.22.0"生成 5 段旁白的核心脚本(generate_audio_voxcpm.py 简化版):
from voxcpm import VoxCPM2
import soundfile as sf
model = VoxCPM2.from_pretrained("openbmb/VoxCPM2")
# CPU 推理,不需要 GPU
scenes = [
("scene1_intro", "初中生该不该玩游戏?答案不是该或不该,关键在于如何驾驭它。"),
("scene2_pros", "玩游戏可以锻炼反应力与手眼协调,培养策略思维..."),
("scene3_cons", "但过度玩游戏也会带来问题:沉迷成瘾挤占学习时间..."),
("scene4_selfcontrol", "那么如何培养自控力呢?第一,设定时间边界..."),
("scene5_ending", "游戏是生活的调味品,不是主菜。能驾驭游戏的人,才是真正的玩家。"),
]
for name, text in scenes:
audio = model.generate(text, speed=1.0)
sf.write(f"public/audio/{name}.mp3", audio, samplerate=44100)输出的音频参数:48kHz 原始采样,192kbps MP3,经 ZipEnhancer 降噪处理后音质非常干净。
2. 场景组件与动画设计
视频共 5 个场景,总时长约 55 秒(1635 帧 @ 30fps)。每个场景是一个独立的 React 组件,用 interpolate() 驱动动画,通过 <Sequence> 组件编排时间线。
以场景①(开场)为例,展示代码风格:
import { interpolate, useCurrentFrame } from "remotion";
const TRANSITION_FRAMES = 15;
export const Scene1Intro: React.FC<{
audioDuration: number;
}> = ({ audioDuration }) => {
const frame = useCurrentFrame();
const DURATION = Math.ceil(audioDuration * 30) + 10;
// 淡入淡出(通用)
const opacity = interpolate(
frame,
[0, TRANSITION_FRAMES, DURATION - TRANSITION_FRAMES, DURATION],
[0, 1, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
// 标题缩放入场
const titleScale = interpolate(frame, [0, 30], [0.8, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div style={{ opacity, transform: `scale(${titleScale})` }}>
<h1 style={{ color: "#ff6b6b" }}>游戏·掌控</h1>
<p>初中生该不该玩游戏?</p>
</div>
);
};5 个场景各有不同的入场动画风格——开场用缩放,利弊分析用卡片滑入,自控力部分用 spring 弹性动画,结尾用逐行淡入。配色方案也做了差异化设计:
| 场景 | 背景色 | 强调色 | 氛围 |
|---|---|---|---|
| 开场引入 | 深蓝 #0f0f23 |
红 #ff6b6b |
沉稳 |
| 游戏之"利" | 深蓝 #0a1628 |
青 #4ecdc4 |
积极 |
| 游戏之"弊" | 深红 #1a0808 |
红 #ff6b6b |
警示 |
| 自控力之道 | 深绿 #0a1a0a |
金 #ffd93d |
希望 |
| 结尾升华 | 深紫 #0f0a1a |
紫 #c084fc |
升华 |
3. 音频集成——背景音乐 + 旁白
音频架构分两层:全局背景音乐贯穿全程,每个场景的旁白音频在对应时间段播放。
背景音乐用 FFmpeg 合成了一小段柔和的钢琴和弦(带衰减 + 混响),然后用 Remotion 的 <Audio> 组件实现渐入渐出:
// Composition.tsx
<Audio
src={staticFile("audio/bgm.wav")}
volume={interpolate(
frame,
[0, 30, totalFrames - 60, totalFrames],
[0, 0.15, 0.15, 0], // BGM 音量控制在 15%,不抢旁白
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
)}
/>每个场景的旁白使用 endAt 属性,在过渡前 15 帧停止,避免与下一场景的旁白重叠:
<Sequence from={0} durationInFrames={SCENE1_DURATION}>
<Audio src={staticFile("audio/scene1_intro.mp3")}
endAt={SCENE1_DURATION - TRANSITION_FRAMES} />
<Scene1Intro audioDuration={7.2} />
</Sequence>4. 字幕系统
字幕有三个层次:TypeScript 数据层 → React 渲染层 → SRT 文件导出层。
数据层定义每段字幕的起止帧和文本内容:
// subtitles.ts
export interface SubtitleEntry {
startFrame: number;
endFrame: number;
text: string;
}
export const SUBTITLES: SubtitleEntry[] = [
{ startFrame: 0, endFrame: 115, text: "初中生该不该玩游戏?答案不是该或不该," },
{ startFrame: 115, endFrame: 215, text: "关键在于如何驾驭它。" },
// ... 共 19 条字幕
];渲染层用 SubtitleOverlay.tsx 在视频底部叠加当前帧对应的字幕文本。SRT 文件用于导入外部剪辑软件做进一步编辑。
四、效果展示 / 问题踩坑
1. TTS 方案试错全记录
语音合成是折腾最久的部分,前后试了 5 个方案,走了一条曲折的选型路径:
macOS say (Tingting) ──→ 声音机械感强,弃用
↓
Edge-TTS (微软Xiaoxiao) ──→ 有背景噪音,需联网,备选
↓
CosyVoice2 (阿里) ──→ grpcio/pyworld C++ 扩展编译失败 ❌
↓
ChatTTS ──→ torchvision::nms macOS 兼容问题 ❌
↓
VoxCPM2 (清华) ✅ ← 最终选择CosyVoice2 和 ChatTTS 的音质评测都很高,但在 macOS 上的 C++ 扩展编译都遇到了兼容性问题——grpcio 编译失败、torchvision::nms 符号找不到。VoxCPM2 是唯一一个 pip install 就能用的高质量中文 TTS,而且 Apache 2.0 开源协议对商用友好。
2. 代码审查发现的两个 Bug
在写完初稿后做代码审查时,发现两个需要修复的问题:
| 问题 | 原因 | 修复 |
|---|---|---|
| 场景过渡期间音频重叠 | 旁白没有在过渡前停止 | 添加 endAt={DURATION - TRANSITION_FRAMES} |
| 总时长硬编码 | 数组索引 [4] 硬编码 |
改用 getTotalDuration() 函数动态计算 |
3. 音频质量优化
- 背景噪音:最初 BGM 用纯正弦波合成,听感像嗡嗡声。改为带衰减 + 混响的柔化和弦,音量降低到 15%,效果自然很多
- 旁白音质:从最初的 48kbps/24kHz 升级到 192kbps/44.1kHz,清晰度明显提升
4. 视频渲染
渲染命令很简单:
npx remotion render MyComp out/video.mp4最终输出 55 秒的 MP4 文件,5 个场景流畅衔接,旁白干净同步,字幕在视频底部逐句显示。
五、总结
这次用 Remotion + AI 语音合成搭建教育视频,让我有几点体会:
第一,代码生成视频这条路可行。 对于结构化的内容型视频,用 Remotion 把每一个画面写成 React 组件,精确度和可维护性远超拖拽剪辑——改文案就是改字符串,改动画就是改一个插值参数,版本控制也天然支持。
第二,TTS 选型关键是"开箱即用"。 研究了几个音质最好的方案,最后选了安装最简的那个。VoxCPM2 在中文语音合成上的表现让我很满意,而且不需要 GPU 就能跑,对个人开发者非常友好。
第三,最花时间的往往不是核心逻辑。 这个项目里场景动画写起来很快,反而是音频降噪、字幕对齐、过渡帧微调这些"边角料"占了大半时间。做视频和做软件一样——细节决定体验。
项目完整代码已开源,感兴趣的同学可以 clone 下来自己跑一遍。如果你也想用代码做视频,Remotion 值得一试,它的 Studio 工具可以实时预览每一帧,调试体验比想象中好很多。
