将 Markdown 技术文档自动转换成带配音旁白的专业视频。使用 edge-tts 生成自然人声、Remotion 渲染视觉场景、FFmpeg 合并音视频,输出 1920×1080 全高清视频。适用场景:项目文档视频化、教程制作、知识分享。
---
name: doc-to-video
description: 将 Markdown 技术文档自动转换成带配音旁白的专业视频。使用 edge-tts 生成自然人声、Remotion 渲染视觉场景、FFmpeg 合并音视频,输出 1920×1080 全高清视频。适用场景:项目文档视频化、教程制作、知识分享。
author: mengbin
version: "1.0.0"
homepage: https://clawhub.ai/skills/doc-to-video
keywords: [markdown, video, edge-tts, remotion, ffmpeg, tutorial, 文档视频化, 配音]
tags: [video, tutorial, automation, edge-tts, remotion, ffmpeg]
requirements:
- python3
- ffmpeg
- node ≥ 18
- pip (for edge-tts)
---
# 🎬 Doc to Video:Markdown 文档转专业视频
> **Skill 名称**:doc-to-video
> **适用版本**:OpenClaw / QClaw
> **技能类型**:文档 → 视频自动化
> **输出格式**:1920×1080 MP4,H.264 视频 + AAC 音频
将 Markdown 技术文档一键转换成带自然人声旁白的专业视频。
从内容分析、旁白编写、配音生成、视觉渲染,到音视频合并,全流程自动化。
---
## 📌 效果预览
本 Skill 已在三个真实项目中验证:
| 视频 | 时长 | 场景数 | 文件大小 |
|------|------|--------|---------|
| Docker Registry 使用指南 | ~153s | 9个 | ~3.2MB |
| Docker Registry 搭建记录 | ~207s | 16个 | ~5.1MB |
| Solidity Nomad 多签教程 | ~210s | 11个 | ~6.2MB |
---
## 🔧 核心技术栈
```
Markdown 文档
│
▼
┌─────────────────┐
│ edge-tts │ ← 中文自然人声(Tingting/XiaoxiaoNeural)
│ Python 生成配音 │
└────────┬────────┘
│ .m4a 音频文件
▼
┌─────────────────┐
│ FFmpeg atempo │ ← 加速配音匹配目标时长
└────────┬────────┘
│
▼
┌─────────────────┐
│ Remotion │ ← React 场景组件,TypeScript
│ 视觉场景渲染 │ 帧率 30fps,分辨率 1920×1080
└────────┬────────┘
│ MP4 视频(无声)
▼
┌─────────────────┐
│ FFmpeg 合并 │ ← 去原音 + 嵌入配音
└────────┬────────┘
│
▼
带配音的 MP4 视频 ✅
```
---
## 📦 安装
### 方式一:一键安装(推荐)
```bash
skillhub install doc-to-video
```
> SkillHub 自动安装 Python 依赖(edge-tts)和 Node 依赖(Remotion)。
### 方式二:手动安装
```bash
# 1. 安装 Python 依赖
pip3 install edge-tts
# 2. 安装 FFmpeg
brew install ffmpeg # macOS
apt install ffmpeg # Ubuntu/Debian
# 3. 确认 Remotion 已安装在工作区
ls /Users/mac/.qclaw-oversea/workspace/node_modules/.bin/remotion
```
---
## 🚀 快速开始
### Step 1:创建工作目录
```bash
mkdir my-video-project && cd my-video-project
mkdir -p src audio out
```
### Step 2:编写 `generate_audio.py`
```python
#!/usr/bin/env python3
"""生成各场景配音(edge-tts XiaoxiaoNeural)"""
import asyncio, edge_tts, os
SCENES = [
("00_title", "欢迎观看本教程。本节介绍主要内容..."),
("01_chapter1", "第一章,首先介绍背景知识..."),
("02_chapter2", "第二章,讲解核心概念..."),
# 更多场景...
]
VOICE = "zh-CN-XiaoxiaoNeural"
os.makedirs("audio", exist_ok=True)
async def gen(scene_id: str, text: str):
m4a = f"audio/{scene_id}.m4a"
if os.path.exists(m4a):
print(f" [skip] {scene_id}")
return
print(f" → {scene_id}...")
await edge_tts.Communicate(text, VOICE).save(m4a)
print(f" done")
async def main():
await asyncio.gather(*[gen(sid, txt) for sid, txt in SCENES])
print("\nAll done!")
asyncio.run(main())
```
### Step 3:生成配音
```bash
python3 generate_audio.py
```
### Step 4:测量各段音频时长
```bash
for f in audio/*.m4a; do
dur=$(ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 "$f")
echo "$f: ${dur}s"
done
```
### Step 5:拼接 + 加速音频
```bash
# 生成文件列表
cat > audio/file_list.txt << 'EOF'
file 'audio/00_title.m4a'
file 'audio/01_chapter1.m4a'
file 'audio/02_chapter2.m4a'
# ...所有文件
EOF
# 拼接
ffmpeg -y -f concat -safe 0 -i audio/file_list.txt \
-codec:a libmp3lame -qscale:a 2 audio/combined_raw.mp3
# 加速(示例:原始 360s → 目标 210s,加速比 1.714)
# 两级 atempo = sqrt(1.714) ≈ 1.31
ffmpeg -y -i audio/combined_raw.mp3 \
-filter:a "atempo=1.31,atempo=1.31" \
-codec:a aac -b:a 128k audio/combined_final.m4a
```
### Step 6:编写 Remotion 场景组件
```tsx
// src/Scene.tsx
import React from "react";
import { useCurrentFrame } from "remotion";
function prog(t: number, s: number, d: number): number {
return Math.min(1, Math.max(0, (t - s) / d));
}
// 精确帧边界(先渲染一次确认实际帧数后填入)
const F = [0, 266, 1096, 1780, 2730, 3545, 4093, 4610, 5215, 5715, 6130];
export const Scene: React.FC = () => {
const f = useCurrentFrame();
if (f < F[1]) return <CoverScene p={prog(f, 0, 40)} />;
if (f < F[2]) return <Chapter1Scene p={prog(f, F[1], 40)} />;
// ... 更多场景
return <EndScene p={prog(f, F[F.length-1], 40)} />;
};
```
### Step 7:入口文件 `src/index.tsx`
```tsx
import React from "react";
import { Composition, registerRoot } from "remotion";
import { Scene } from "./Scene";
registerRoot(() => (
<Composition
id="MyVideo"
component={Scene}
durationInFrames={6295} // 先填估算值,后续更正
fps={30}
width={1920}
height={1080}
/>
));
```
### Step 8:渲染 + 合并
```bash
cd /path/to/workspace
# 第一次渲染:确认实际帧数
./node_modules/.bin/remotion render \
my-project/src/index.tsx MyVideo \
out/temp.mp4
# ffprobe 确认实际帧数
ffprobe -v error -select_streams v:0 \
-show_entries stream=nb_frames -of csv=p=0 out/temp.mp4
# → 假设输出 6295,用此值更新 F[] 和 durationInFrames
# 重新渲染(用精确帧数)
./node_modules/.bin/remotion render \
my-project/src/index.tsx MyVideo \
out/final_video.mp4
# 合并音视频
ffmpeg -y -i out/final_video.mp4 -an -c:v copy /tmp/noaudio.mp4
ffmpeg -y -i /tmp/noaudio.mp4 -i audio/combined_final.m4a \
-c:v copy -c:a aac -b:a 128k -shortest \
out/final_with_audio.mp4
# 验证
ffprobe -v error -show_streams out/final_with_audio.mp4 \
| grep -E "codec_type|duration"
```
---
## 🔑 核心经验:音视频同步的坑与解法
### ❌ 错误做法(会导致不同步)
```
估算时长 → 计算帧边界 → 渲染 → 合并音频
↑ 用的是估算帧数,实际渲染帧数可能不同
```
Remotion 渲染的实际帧数不一定等于 `durationInFrames` 设置值!
因为 Remotion 按内容自动决定帧数,CSS 动画时长也会影响。
### ✅ 正确做法(两步确认法)
```
估算时长 → 渲染一次视频 → ffprobe 确认实际帧数
↓ 用实际帧数重新计算帧边界
更新 F[] + durationInFrames → 重新渲染 → 合并
```
**帧边界计算公式:**
```
某场景开始帧 = round(该场景前累计秒数 / 音频总秒数 × 实际渲染总帧数)
```
### 为什么音频用 FFmpeg atempo 而不是 Remotion 内置?
Remotion 内置 `<Audio>` 组件依赖 React,在多场景场景下不稳定(报错 #130)。
FFmpeg atempo 无损加速,可精确控制时长,音质可控。
---
## 🎨 场景组件设计规范
### 布局原则
- 背景:深色渐变(`#0b1d3a → #1a3a6b`)或代码风格(`#0d1117`)
- 字体:标题 40–52px,内容 15–17px,等宽 13–14px
- 间距:水平留白 80–100px,垂直居中
### 动画原则
```tsx
// 动画进度 0→1(约 1–1.5 秒)
function prog(t: number, s: number, d: number): number {
return Math.min(1, Math.max(0, (t - s) / d));
}
function ease(t: number) { return t * t; }
// 示例:渐入 + 上浮
<div style={{
opacity: ease(p), // 0→1
transform: `translateY(${(1-ease(p))*30}px)`, // 下→上 30px
}}>
```
### 推荐视觉组件
| 组件 | 场景 | 特点 |
|------|------|------|
| `CodeBlock` | 代码展示 | 黑色背景,蓝色文字,等宽字体 |
| `StepItem` | 步骤流程 | 彩色编号圆圈 + 文字说明 |
| `ProblemCard` | 问题排查 | 红色标题,原因+解决布局 |
| `BulletItem` | 要点列表 | 图标 + 内容 |
| `Tag` | 章节标签 | 圆角胶囊 + 光晕效果 |
| `VideoScene` | 场景容器 | 渐变背景 + 相对定位 |
---
## 📂 项目结构示例
```
my-video-project/
├── generate_audio.py # 配音生成脚本
├── src/
│ ├── index.tsx # Remotion 入口
│ └── Scene.tsx # 场景组件
├── audio/
│ ├── 00_title.m4a # 各场景配音
│ ├── 01_chapter1.m4a
│ ├── ...
│ ├── combined_final.m4a # 拼接加速后完整音频
│ └── file_list.txt # 拼接文件列表
└── out/
├── temp.mp4 # 首次渲染(确认帧数用)
└── final_with_audio.mp4 # 最终输出
```
---
## ⚙️ 参数参考
| 参数 | 推荐值 | 说明 |
|------|--------|------|
| 帧率 | 30 fps | 标准视频帧率 |
| 分辨率 | 1920×1080 | 16:9 全高清 |
| 目标时长 | 180–210 秒 | 3–3.5分钟 |
| 加速比 | 1.3–2.0× | 过大影响音质 |
| atempo 级联 | 两级相乘≈目标加速比 | 每级不超过 2.0 |
| 每段旁白 | 100–300 字 | 对应场景内容 |
| 动画时长 | 30–40 帧 | ~1–1.3秒 |
| 音频码率 | 128k AAC | 清晰度与体积平衡 |
| 推荐 voice | zh-CN-XiaoxiaoNeural | 女声,自然流畅 |
---
## ❓ 常见问题
### Q1:音频比视频快(或慢)——最常见问题
**原因**:帧边界基于估算帧数,而非实际渲染帧数。
**解决:**
1. `ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of csv=p=0 out/video.mp4`
2. 用实际帧数重新计算所有帧边界
3. 更新 `index.tsx` 的 `durationInFrames` 和场景组件的 `F[]`
4. 重新渲染并合并
### Q2:Remotion 渲染报错 "useCurrentFrame() can only be called..."
**原因**:入口文件没有用 `Composition` API 注册。
**解决:**
```tsx
import { Composition, registerRoot } from "remotion";
registerRoot(() => (
<Composition id="UniqueId" component={Scene}
durationInFrames={6295} fps={30} width={1920} height={1080} />
));
```
### Q3:FFmpeg 合并后音频只有几 KB
**原因**:原视频有静音音频轨道,`-shortest` 保留了原轨道。
**解决:** 必须先用 `-an` 去掉原音:
```bash
ffmpeg -i video.mp4 -an -c:v copy noaudio.mp4
ffmpeg -i noaudio.mp4 -i audio.m4a -shortest output.mp4
```
### Q4:atempo 加速后人声变调
**原因**:单级 atempo 超过 2.0。
**解决:** 两级级联,例如 3.5x:`atempo=1.87,atempo=1.87`
### Q5:edge-tts 无网络
**备选:** macOS 系统语音 `say -v Tingting -r 175 "旁白内容"`
转 MP3:`ffmpeg -i audio.aiff -codec:a libmp3lame -qscale:a 2 audio.mp3`
(注意:音质远不如 edge-tts)
---
## 📤 发布到 SkillHub / ClawHub
### 发布前准备
1. 确保 `SKILL.md` 包含完整的 frontmatter(name, description, author, version, tags 等)
2. Skill 目录结构清晰,文件命名规范
3. 准备好封面图(可选,512×512 PNG)
### SkillHub(推荐)
访问 [https://clawhub.ai](https://clawhub.ai):
1. **注册/登录** ClawHub 账号
2. 点击 **「Publish Skill」** 或 **「Submit」**
3. 填写信息:
- **Skill Name**: `doc-to-video`
- **Description**: `将 Markdown 技术文档自动转换成带配音旁白的专业视频`
- **Category**: `Video & Media` 或 `Automation`
- **Tags**: `markdown, video, edge-tts, remotion, ffmpeg, tutorial`
- **Author**: 你的昵称或机构名
4. 上传文件:
```
skill-doc-to-video/
├── SKILL.md ← 必须
├── generate_audio.py ← 推荐一起打包
└── preview.png ← 可选封面图
```
5. 点击提交,等待审核通过
### ClawHub(原生)
如果 ClawHub 支持 CLI 发布:
```bash
# 方式一:直接推送目录
npx clawhub publish ./skill-doc-to-video
# 方式二:登录后推送
clawhub login
clawhub publish --name doc-to-video --dir ./skill-doc-to-video
```
---
## 🧠 Skill 开发过程记录
本 Skill 并非一步到位,而是通过三次迭代逐步完善:
### 第一版:基础流程
**思路**:Remotion 渲染视频 → 用 FFmpeg 合并配音
**问题**:音频与视频不同步,因为帧边界计算有误
### 第二版:加入精确帧计算
**思路**:渲染一次视频,用 ffprobe 确认实际帧数,再反推边界
**问题**:确认帧数是对的,但渲染出来的帧数与预期仍有偏差
### 第三版(最终):两步确认法 + FFmpeg 嵌入音频
**核心发现**:
- Remotion `durationInFrames` 是参考值,实际帧数由内容决定
- 必须先渲染 → ffprobe 确认 → 再算边界 → 更新代码 → 重渲染
- `<Audio>` 组件在 Remotion 中不稳定,改用 FFmpeg 直接嵌入音频
**最终流程(固化在本 Skill 中):**
```
Markdown → 旁白 → edge-tts → 拼接加速
→ 渲染确认帧数 → ffprobe 实测 → 精确帧边界
→ 重渲染 → FFmpeg 嵌入音频 → 完成
```
**三个验证项目:**
1. `docker-registry-guide-final.mp4` — 9场景,153s ✅
2. `deploy-docker-registry-final.mp4` — 16场景,207s ✅
3. `solidtidy-final.mp4` — 11场景,210s,音视频完美同步 ✅
---
## 📄 许可
MIT License — 可自由使用、修改、分发。
don't have the plugin yet? install it then click "run inline in claude" again.