- name
- remotion-video
- description
- 使用 Remotion 框架以编程方式创建视频。Remotion 让你用 React 组件定义视频内容,支持动画、字幕、音乐可视化、3D 视频、教程讲解视频等。适用于程序化视频、批量生成、数据驱动视频、音乐可视化、自动字幕等场景。
# Remotion Video
用 React 以编程方式创建 MP4 视频的框架。
## 核心概念
1. **Composition** - 视频的定义(尺寸、帧率、时长)
2. **useCurrentFrame()** - 获取当前帧号,驱动动画
3. **interpolate()** - 将帧号映射到任意值(位置、透明度等)
4. **spring()** - 物理动画效果
5. **<Sequence>** - 时间轴上排列组件
## 快速开始
### 创建新项目
```bash
npx create-video@latest
```
选择模板后:
```bash
cd <project-name>
npm run dev # 启动 Remotion Studio 预览
```
### 项目结构
```
my-video/
├── src/
│ ├── Root.tsx # 注册所有 Composition
│ ├── HelloWorld.tsx # 视频组件
│ └── index.ts # 入口
├── public/ # 静态资源(音频、图片)
├── remotion.config.ts # 配置文件
└── package.json
```
## 基础组件示例
### 最小视频组件
```tsx
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
export const MyVideo = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: "white", justifyContent: "center", alignItems: "center" }}>
<h1 style={{ fontSize: 100 }}>Frame {frame}</h1>
</AbsoluteFill>
);
};
```
### 注册 Composition
```tsx
// Root.tsx
import { Composition } from "remotion";
import { MyVideo } from "./MyVideo";
export const RemotionRoot = () => {
return (
<Composition
id="MyVideo"
component={MyVideo}
durationInFrames={150} // 5秒 @ 30fps
fps={30}
width={1920}
height={1080}
/>
);
};
```
## 动画技巧
### interpolate - 值映射
```tsx
import { interpolate, useCurrentFrame } from "remotion";
const frame = useCurrentFrame();
// 0-30帧:透明度 0→1
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: "clamp", // 超出范围时钳制
});
// 位移动画
const translateY = interpolate(frame, [0, 30], [50, 0]);
```
### spring - 物理动画
```tsx
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
frame,
fps,
config: { damping: 10, stiffness: 100 },
});
```
### Sequence - 时间编排
```tsx
import { Sequence } from "remotion";
<>
<Sequence from={0} durationInFrames={60}>
<Intro />
</Sequence>
<Sequence from={60} durationInFrames={90}>
<MainContent />
</Sequence>
<Sequence from={150}>
<Outro />
</Sequence>
</>
```
## AI 语音解说集成
为视频添加 AI 语音解说,实现音视频同步。支持两种方案:
| 方案 | 优点 | 缺点 | 硬件要求 | 推荐度 |
|------|------|------|----------|--------|
| **MiniMax TTS** | 云端克隆、速度极快(<3秒)、音质优秀 | 按字符计费 | 无 | ⭐⭐⭐ 首选 |
| **Edge TTS** | 零配置、免费 | 固定音色、无法自定义 | 无 | ⭐⭐ |
### 方案选择流程
```
1. 首选 MiniMax TTS
- 检测 API Key 是否配置
- 测试调用是否正常(余额充足)
- 如果成功 → 使用 MiniMax
2. MiniMax 不可用时
→ 退回 Edge TTS(使用预设音色 zh-CN-YunyangNeural)
```
---
## 方案一:MiniMax TTS(推荐)
云端 API 方案,无需本地 GPU,生成速度极快,音色克隆效果优秀。
### 配置
1. 注册 https://www.minimax.io (国际版)或 https://platform.minimaxi.com (国内版)
2. 获取 API Key
3. 在 MiniMax Audio 上传音频克隆音色,获取 voice_id
### API 差异
| 版本 | API 域名 | 说明 |
|------|----------|------|
| 国际版 | `api.minimax.io` | 推荐,稳定 |
| 国内版 | `api.minimaxi.com` | 需国内账号 |
**⚠️ 常见错误**:`api.minimax.chat` 是**错误的域名**,会返回 "invalid api key"。请确认使用上表中的正确域名。
### 生成脚本
使用 `scripts/generate_audio_minimax.py` 生成音频,支持:
- **断点续作**:已存在的音频文件自动跳过
- **实时进度**:显示生成进度,避免茫然等待
- **自动更新配置**:生成完成后自动更新 Remotion 的场景配置
```bash
# 设置环境变量
export MINIMAX_API_KEY="your_api_key"
export MINIMAX_VOICE_ID="your_voice_id"
# 运行脚本
python scripts/generate_audio_minimax.py
```
### 价格参考(2025年)
| 模型 | 价格 |
|------|------|
| speech-02-hd | ¥0.1/千字符 |
| speech-02-turbo | ¥0.05/千字符 |
### ⚠️ MiniMax TTS 踩坑经验
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| `invalid api key` | 使用了错误的 API 域名 | 国际版用 `api.minimax.io`,国内版用 `api.minimaxi.com` |
| config.ts 语法错误 `Syntax error "n"` | Python 脚本在 f-string 中用 `",\\n".join()` 产生了字面量 `\n` 而非真正换行 | 见下方「Python 生成 TypeScript 注意事项」 |
| 长时间无进度显示 | 后台执行命令看不到输出 | 前台执行脚本,或用 `tail -f` 实时查看日志 |
### Python 生成 TypeScript 注意事项
**❌ 错误写法**:在 f-string 中使用 `\n` 会产生字面量字符
```python
# 这会在生成的文件中写入字面的 \n 字符串,而非换行!
content = f'export const SCENES = [{",\\n".join(items)}];'
```
**✅ 正确写法**:分开处理字符串拼接
```python
# 先用真正的换行符拼接
scenes_content = ",\n".join(items) # 在 f-string 外部拼接
# 再放入模板
content = f'''export const SCENES = [
{scenes_content}
];'''
```
---
## 方案二:Edge TTS
无需特殊硬件,完全免费,适合不需要克隆音色的场景。
### 安装
```bash
pip install edge-tts
```
### 推荐语音
| 语音 ID | 名称 | 风格 |
|---------|------|------|
| zh-CN-YunyangNeural | 云扬 | 专业播音腔(推荐) |
| zh-CN-XiaoxiaoNeural | 晓晓 | 温暖自然 |
| zh-CN-YunxiNeural | 云希 | 阳光少年 |
### 生成脚本
使用 `scripts/generate_audio_edge.py` 生成音频:
```bash
python scripts/generate_audio_edge.py
```
### Remotion 音频同步
```tsx
import { Audio, Sequence, staticFile } from "remotion";
// 音频配置(根据生成的时长)
const audioConfig = [
{ id: "01-intro", file: "01-intro.mp3", frames: 450 },
{ id: "02-main", file: "02-main.mp3", frames: 600 },
];
// 计算起始帧
const sceneStarts = audioConfig.reduce((acc, _, i) => {
if (i === 0) return [0];
return [...acc, acc[i - 1] + audioConfig[i - 1].frames];
}, [] as number[]);
// 场景渲染
{audioConfig.map((scene, i) => (
<Sequence key={scene.id} from={sceneStarts[i]} durationInFrames={scene.frames}>
<SceneComponent />
<Audio src={staticFile(scene.file)} />
</Sequence>
))}
```
---
## 教程类视频架构(场景驱动)
教程、讲解类视频的核心架构:**音频驱动场景切换**。
### 架构概览
```
音频脚本 → TTS 生成 → audioConfig.ts → 场景组件 → 视频渲染
```
关键思想:
1. **音频决定时长**:每个场景的持续时间由音频长度决定
2. **场景即章节**:一个概念 = 一个场景 = 一段音频
3. **配置即真理**:`audioConfig.ts` 是音画同步的单一数据源
### audioConfig.ts 模板
参见 `templates/audioConfig.ts`,包含:
- SceneConfig 接口定义
- SCENES 数组
- getSceneStart() 计算函数
- TOTAL_FRAMES 和 FPS 常量
### 场景切换 Hook
```tsx
import { useCurrentFrame } from "remotion";
import { SCENES } from "./audioConfig";
// 根据当前帧号返回场景索引
const useCurrentSceneIndex = () => {
const frame = useCurrentFrame();
let accumulated = 0;
for (let i = 0; i < SCENES.length; i++) {
accumulated += SCENES[i].durationInFrames;
if (frame < accumulated) return i;
}
return SCENES.length - 1;
};
// 使用
const sceneIndex = useCurrentSceneIndex();
const currentScene = SCENES[sceneIndex];
```
### 主场景组件模式
```tsx
import { AbsoluteFill, Audio, Sequence, staticFile, useVideoConfig } from "remotion";
import { ThreeCanvas } from "@remotion/three";
import { SCENES, getSceneStart, TOTAL_FRAMES } from "./audioConfig";
export const TutorialVideo: React.FC = () => {
const { width, height } = useVideoConfig();
const sceneIndex = useCurrentSceneIndex();
const currentScene = SCENES[sceneIndex];
return (
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
{/* 3D 内容 */}
<ThreeCanvas width={width} height={height} camera={{ position: [0, 0, 4], fov: 50 }}>
{/* 根据 sceneIndex 渲染不同场景 */}
{sceneIndex === 0 && <Scene01Intro />}
{sceneIndex === 1 && <Scene02Concept />}
{sceneIndex === 2 && <Scene03Demo />}
</ThreeCanvas>
{/* 音频同步 - 每个场景一个 Sequence */}
{SCENES.map((scene, idx) => (
<Sequence key={scene.id} from={getSceneStart(idx)} durationInFrames={scene.durationInFrames}>
<Audio src={staticFile(`audio/${scene.audioFile}`)} />
</Sequence>
))}
{/* UI 层:标题 + 进度 */}
<div style={{ position: "absolute", top: 40, left: 0, right: 0, textAlign: "center" }}>
<h1 style={{ color: "white", fontSize: 42 }}>教程标题</h1>
</div>
<div style={{ position: "absolute", bottom: 60, left: 60 }}>
<span style={{ color: "white" }}>{currentScene?.title}</span>
</div>
{/* 进度条 */}
<div style={{ position: "absolute", bottom: 30, left: 60, right: 60, height: 4, backgroundColor: "rgba(255,255,255,0.2)" }}>
<div style={{ width: `${((sceneIndex + 1) / SCENES.length) * 100}%`, height: "100%", backgroundColor: "#3498DB" }} />
</div>
</AbsoluteFill>
);
};
```
### Root.tsx 使用动态帧数
```tsx
import { Composition } from "remotion";
import { TutorialVideo } from "./TutorialVideo";
import { TOTAL_FRAMES } from "./audioConfig";
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="Tutorial"
component={TutorialVideo}
fps={30}
durationInFrames={TOTAL_FRAMES} // 从 audioConfig 动态获取
width={1920}
height={1080}
/>
);
};
```
### ⚠️ 教程视频踩坑经验
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 场景切换生硬 | 直接切换无过渡 | 用 spring/interpolate 添加入场动画 |
| 3D 内容与音频不同步 | 硬编码帧数 | 所有时长从 audioConfig 读取 |
| 渲染时 WebGL 崩溃 | 多个 ThreeCanvas 同时存在 | 用 sceneIndex 条件渲染,同时只有一个 3D 场景 |
| 视频太简略 | 只有一个大场景 | **一个概念 = 一个场景组件**,分层讲解 |
View on GitHub