360 Secure SkillHub
360 安全技能中心

将 Markdown 技术文档自动转换成带配音旁白的专业视频

将 Markdown 技术文档转为带自然人声旁白的 1920×1080 MP4 视频:用 edge-tts 生成配音、Remotion(React/TS)渲染视觉场景、FFmpeg 拼接与合并音视频,并沉淀帧边界同步、voice 选择与批量渲染等经验及配套脚本模板。

mengbin92
mengbin92
热度 44.55
最近更新 2026-06-12 15:32:30
分类:创作依赖:代码执行 / 读写文件 / 第三方 API / 联网浏览搜索 / 内部调用大模型应用场景:视频编辑 / 自动化脚本 / 工作流 / 文件转换

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add doc-to-video
Metadata
name
doc-to-video
description
将 Markdown 技术文档自动转换成带配音旁白的专业视频。使用 edge-tts 生成自然人声、Remotion 渲染视觉场景、FFmpeg 合并音视频,输出 1920×1080 全高清视频。适用场景:项目文档视频化、教程制作、知识分享。
author
mengbin
version
1.0.9
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 / Hermes 技能类型:文档 → 视频自动化 输出格式:1920×1080 MP4,H.264 视频 + AAC 音频 |> v1.0.6 修订:加 references/batch-rendering.md(subagent 委派 + 并行渲染 + 跳过第二遍 render 的 F[] 优化)。Step 8 加"skip-second-render"快路径(如果 F[] 是从实测 audio 算的,整遍"先 render → ffprobe → 改 F[] → 再 render"可以只跑第一步——实测 5 个视频全部一次命中)。新加"工具设计原则"小节:non-binding helper vs opinionated workflow(用户偏好:helper 高,workflow 低)。 |> v1.0.5 修订:加 references/second-video-pattern.md(cp -R 复用 + 4 个新 visual patterns:CEI 时间线 / 双卡对比 / 4-file 列表 / 4-列对比表)和 references/syncing-to-openclaw.md(Hermes self-update 陷阱 + 可复用 sync 脚本)。SKILL.md Step 1.5 加"从已有项目派生"快路径。voice-swap-and-iterate.md 之外多了迭代 patterns 参考。 |> v1.0.4 修订:加 templates/audio_frames.py(measure + frames 子命令)替代手写 ffprobe 和 F[] 公式。加 templates/voice_test.py 把 6 voice 对比做成一行命令。Step 0 加 voice 试听流程,Step 4 用工具替代。macos-gotchas.md 加 §8.1 频谱分析辅助选 voice + §9 templates 索引表。配合 v1.0.3 的 voice-swap-and-iterate.md,换 voice 后的 F[] 重算一行命令完成。 |> v1.0.3 修订:加 references/voice-swap-and-iterate.md — 换 voice / 改语速 / 改文本后的完整迭代工作流(含 F[] 整体缩放 vs 逐段重算、整体 atempo 误区) |> v1.0.2 修订:补 edge-tts 48kbps MP3 硬上限说明(Q8)—— 之前没说清楚"音听着别扭"的根因不是参数,是源端码率天花板 |> v1.0.1 修订:修 ffmpeg concat 路径 bug、补 macOS Remotion 渲染 gotcha、加"无需 atempo 加速"快路径、增加 references/ 和 templates/ 配套文件

将 Markdown 技术文档一键转换成带自然人声旁白的专业视频。 从内容分析、旁白编写、配音生成、视觉渲染,到音视频合并,全流程自动化。


📌 效果预览

本 Skill 已在 27 个真实项目中验证(详见末尾"Skill 开发过程记录"):

视频类型 数量 场景数 / 个 文件大小 / 个
Docker / Nomad 教程(原 3 个) 3 9-16 3-6MB
Solidity 基础 7 个(L04-L10) 7 8-9 ~5-7MB
Solidity 进阶 5 个(L11-L15) 5 9 ~5-7MB
Solidity 安全 5 个(L16-L20) 5 7-9 ~5-7MB
Solidity 实战 5 个(L21-L25) 5 8-9 ~6-8MB
Solidity 项目 5 个(L26-L30,借贷/DAO/3 个安全案例复盘) 5 7-9 ~6-8MB

27 个 TSP 系列视频实测(L04-L30):总计 ~75 min / ~167MB,平均每个视频 2:46 / 6.2MB。 漂移范围 5-64ms(绝大多数 < 50ms)。 详细分段统计见 references/batch-rendering.md §14;安全案例复盘的视觉模式见 references/case-study-pattern.md。

L04 详细见 references/worked-example-tsp-solidity04.md;同系列第二个起复用 references/second-video-pattern.md;批量 N 个 references/batch-rendering.md;换 voice / 改语速 references/voice-swap-and-iterate.md;Hermes ↔ OpenClaw sync references/syncing-to-openclaw.md;macOS 平台坑 references/macos-gotchas.md。


🔧 核心技术栈

Markdown 文档
     │
     ▼
┌─────────────────┐
│  edge-tts        │  ← 中文自然人声(Tingting/XiaoxiaoNeural)
│  Python 生成配音 │
└────────┬────────┘
         │ .m4a 音频文件
         ▼
┌─────────────────┐
│  FFmpeg atempo   │  ← 加速配音匹配目标时长
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Remotion        │  ← React 场景组件,TypeScript
│  视觉场景渲染     │     帧率 30fps,分辨率 1920×1080
└────────┬────────┘
         │ MP4 视频(无声)
         ▼
┌─────────────────┐
│  FFmpeg 合并     │  ← 去原音 + 嵌入配音
└────────┬────────┘
         │
         ▼
   带配音的 MP4 视频 ✅

📦 安装

方式一:一键安装(推荐)

skillhub install doc-to-video

SkillHub 自动安装 Python 依赖(edge-tts)和 Node 依赖(Remotion)。

方式二:手动安装

# 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 0:选 voice 并试听(v1.0.4 新增)

选错 voice 是事后改起来最麻烦的——换 voice 会让总时长漂移几秒,触发 F[] 重算 + Remotion 重渲染,整个迭代 loop 多 4–5 分钟。花 2 分钟在这里试听,能省后面 5 分钟。

跑 templates/voice_test.py 生成 6 个候选 voice 的 9.5 秒样本:

python3 templates/voice_test.py
open /tmp  # Finder 打开,逐个点 voice_*.m4a 用 QuickTime 试听

📎 想自己改样本/候选 voice 列表?模板在 templates/voice_test.py(v1.0.4 加),改 SAMPLE 变量或 VOICES 列表即可。

选 voice 时考虑:

  • 你的内容领域(技术教程 → 男声 Yunxi/Yunjian 通常更专业;知识科普 → 女声 Xiaoxiao 更亲切)
  • 男女声频谱差别大:女声 centroid 3000–4000Hz 偏亮,男声 2500–3500Hz 偏温润
  • 决定后记下来,下面 generate_audio.py 模板里 VOICE = "..." 就用它

Step 1:创建工作目录

mkdir my-video-project && cd my-video-project
mkdir -p src audio out

Step 1.5:从已有项目派生(v1.0.5 新增 — 第二个及之后视频)

如果是同系列的第二个视频(同 voice、同作者、同代码风格),别从头搭项目。直接 cp -R 旧项目、删产物、改内容,比从 templates 重搭快 5-10 分钟。

cd ~/vscode
cp -R my-first-video-project my-second-video-project
cd my-second-video-project
rm -rf audio/* out/*.mp4 out/*.jpg node_modules package-lock.json build
npm install --no-audit --no-fund   # 3s,缓存命中

详见 references/second-video-pattern.md。

Step 2:编写 generate_audio.py

#!/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:生成配音

python3 generate_audio.py

Step 4:测量各段音频时长(v1.0.4 用 audio_frames.py 替代手写 ffprobe)

第一次跑这个 skill 必看:现在用 templates/audio_frames.py 的 measure 子命令,输出总时长和每段秒数。还有保留功能:同时输出 F[] 数组,可以直接粘到 src/Scene.tsx。

# 测各段时长 + 写到 scenes.json(供后续 frames 子命令用)
python3 templates/audio_frames.py measure --json-out scenes.json

输出形如:

📊 各段音频时长 (10 段)
  00_title     11.40s
  01_why       18.46s
  ...
  TOTAL      174.38s (2.91 min)

💾 写到 scenes.json

💡 快路径提示:总时长 170–185s 范围(目标 3 分钟左右)就别用 atempo 加速,直接拼到 Step 5 的命令。总时长 < 165s 或 > 195s 走 atempo(v1.0.1 加的快路径)。

Step 5:拼接 + 加速音频

⚠️ 路径坑(v1.0.1 修):ffmpeg -f concat 的 demuxer 把 file_list.txt 里的路径解析为相对列表文件所在目录,不是 cwd。所以下面这种写法会报 "Impossible to open 'audio/audio/00_title.m4a'":

# ❌ 错误(路径重复 audio/)
cat > audio/file_list.txt << 'EOF'
file 'audio/00_title.m4a'
file '01_chapter1.m4a'
EOF

正确做法是切到 audio/ 下生成 list,路径用裸文件名:

# ✅ 正确:相对 audio/ 目录
cd audio
printf "file '00_title.m4a'\nfile '01_chapter1.m4a'\nfile '02_chapter2.m4a'\n" > file_list.txt
cd ..

# 必走 -c:a aac 重编码(-c copy 拼 m4a 会因 AAC bitstream 不一致失败)
ffmpeg -y -f concat -safe 0 -i audio/file_list.txt \
  -c:a aac -b:a 128k audio/combined.m4a

# 加速(示例:原始 360s → 目标 210s,加速比 1.714)
# 两级 atempo = sqrt(1.714) ≈ 1.31
ffmpeg -y -i audio/combined.m4a \
  -filter:a "atempo=1.31,atempo=1.31" \
  -c:a aac -b:a 128k audio/combined_final.m4a

💡 快路径(v1.0.1 新增):如果拼接后总时长已经在 180–210s 范围内(目标 3–3.5 分钟),跳过 atempo 加速,零音质损失,命令直接用上面那个 ffmpeg -y -f concat ... -c:a aac audio/combined.m4a 即可。

Step 6:编写 Remotion 场景组件

// 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)} />;
};

💡 第二个视频起复用 visual patterns:CEI 时间线 / 双卡对比 / 4-file 列表 / 4-列对比表,详见 references/second-video-pattern.md §"13 段视频的 4 个新 visual patterns"。

Step 7:入口文件 src/index.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:渲染 + 合并

⚠️ macOS 必看(v1.0.1 新增):Remotion 4.x 内置的 chrome-headless-shell 在 macOS 上有 sandbox/网络绑定问题。所有 npx remotion render 命令都必须加 --browser-executable 指向系统 Chrome:

CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

# 第一次渲染:确认实际帧数
npx remotion render src/index.tsx MyVideo out/temp.mp4 \
  --browser-executable="$CHROME" --concurrency=2

# ffprobe 确认实际帧数
ffprobe -v error -select_streams v:0 \
  -show_entries stream=nb_frames -of csv=p=0 out/temp.mp4
# → 假设输出 6295,用此值更新 F[] 和 durationInFrames

# 重新渲染(用精确帧数)
npx remotion render src/index.tsx MyVideo out/final_video.mp4 \
  --browser-executable="$CHROME" --concurrency=2

# 合并音视频
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"

Linux 上把 --browser-executable="$CHROME" 删掉即可。

🎯 快路径(v1.0.6 新增 — 跳过第二遍渲染):如果你的 F[] 是用 templates/audio_frames.py 从实测音频时长算出来的(不是估算视频长度),那第一遍渲染的帧数 = 最终帧数。整个"先 render → ffprobe → 改 F[] → 再 render"流程可以只跑第一步。

适用条件(两个都满足才能跳过第二步):

  1. F[] 来自 audio_frames.py frames --total N(N = round(audio_total × 30))
  2. Remotion 渲染出的帧数 ≈ N(没有用会改变内容时长的 CSS 动画)

实测:5 个批量视频(tsp-solidity06–10)全部一次渲染命中目标,省掉 5 次第二遍渲染 = 节省 ~25 分钟。

不适用场景:用了 transition: all 1s 这类会延展内容时长的 CSS 动画——Remotion 可能渲染超过 durationInFrames 几帧,这种项目仍需两遍。

详见 references/batch-rendering.md §Step 3。

💡 耗时预估(M 系列 MacBook, concurrency=2):

总帧数 渲染时间
~4500 帧 (150s) ~3 分钟
~7000 帧 (235s) ~5 分钟
~9000 帧 (300s) ~6-7 分钟

多视频批量时优先 2-3 个 render 并行(M 系列 64GB RAM 安全,详见 references/batch-rendering.md §Step 6)。


🔑 核心经验:音视频同步的坑与解法

❌ 错误做法(会导致不同步)

估算时长 → 计算帧边界 → 渲染 → 合并音频
          ↑ 用的是估算帧数,实际渲染帧数可能不同

Remotion 渲染的实际帧数不一定等于 durationInFrames 设置值! 因为 Remotion 按内容自动决定帧数,CSS 动画时长也会影响。

✅ 正确做法(两步确认法)

估算时长 → 渲染一次视频 → ffprobe 确认实际帧数
         ↓ 用实际帧数重新计算帧边界
       更新 F[] + durationInFrames → 重新渲染 → 合并

帧边界计算公式:

某场景开始帧 = round(该场景前累计秒数 / 音频总秒数 × 实际渲染总帧数)

⚠️ 占位 F[] 偏差实测:第二个视频(tsp-solidity05)占位 F[] 算 7220 帧,实际需要 7100 帧(差 0.3%),但每个场景的相对位置偏 5-10%——t=60s 看到的不是 ThreeWays 场景而是 LowLevelScene。所以第一遍渲染是 exploratory 的,不是浪费,必须做。

用 templates/audio_frames.py 算 F[]:

# 拼接后用 ffprobe 拿总音频时长
TOTAL_S=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 audio/combined.m4a)

# 跑 frames 子命令算 F[]
python3 templates/audio_frames.py frames \
  --from-measure scenes.json \
  --total $(python3 -c "print(round($TOTAL_S * 30))")
# 直接复制输出的 F[] 到 src/Scene.tsx

为什么音频用 FFmpeg atempo 而不是 Remotion 内置?

Remotion 内置 <Audio> 组件依赖 React,在多场景场景下不稳定(报错 #130)。 FFmpeg atempo 无损加速,可精确控制时长,音质可控。


🎨 场景组件设计规范

布局原则

  • 背景:深色渐变(#0b1d3a → #1a3a6b)或代码风格(#0d1117)
  • 字体:标题 40–52px,内容 15–17px,等宽 13–14px
  • 间距:水平留白 80–100px,垂直居中

动画原则

// 动画进度 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 场景容器 渐变背景 + 相对定位

第二个视频的 4 个新 visual patterns(v1.0.5)

详见 references/second-video-pattern.md:

  • CEI 时间线(Checks-Effects-Interactions):3 标签条 + 代码块
  • 双卡对比(delegatecall vs staticcall):2 列并排
  • 4-file 列表(Foundry 项目结构):2×2 grid,文件名 + 角色
  • 4-列对比表(策略选型):每行 4 列:名称 / 安全评级 / 优点 / 风险

📂 项目结构示例

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–240 秒 3–4 分钟(实际取决于文章长度)
加速比 1.3–2.0× 过大影响音质
atempo 级联 两级相乘≈目标加速比 每级不超过 2.0
每段旁白 100–300 字 对应场景内容
动画时长 30–40 帧 ~1–1.3秒
音频码率 128k AAC 清晰度与体积平衡
推荐 voice zh-CN-YunxiNeural 男声,有故事感,齿音最轻(v1.0.2 实测)

❓ 常见问题

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 注册。

解决:

import { Composition, registerRoot } from "remotion";
registerRoot(() => (
  <Composition id="UniqueId" component={Scene}
    durationInFrames={6295} fps={30} width={1920} height={1080} />
));

Q3:FFmpeg 合并后音频只有几 KB

原因:原视频有静音音频轨道,-shortest 保留了原轨道。

解决: 必须先用 -an 去掉原音:

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)

Q6:ffmpeg concat 报 "Impossible to open 'audio/audio/00_title.m4a'"(v1.0.1 新增)

原因:file_list.txt 里写 file 'audio/xxx.m4a',但 ffmpeg 的 concat demuxer 是相对列表文件所在目录解析路径的,不是相对当前工作目录。所以正确写法是去掉 audio/ 前缀。详见 Step 5 里的注释。

Q7:macOS 上 Remotion 渲染报 spawn Unknown system error -88 或 Visited "http://localhost:3000/index.html" but got no response(v1.0.1 新增)

原因:Remotion 4.x 自带的 chrome-headless-shell 在 macOS 上有 sandbox/网络绑定问题。

解决:用系统安装的 Google Chrome 替代内置 headless-shell,详见 Step 8。

CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
npx remotion render src/index.tsx MyVideo out/temp.mp4 \
  --browser-executable="$CHROME"

Q8:edge-tts 出来的配音"听着别扭"——女声有金属感/齿音(v1.0.2 新增)

根因:edge-tts 服务端只输出 48kbps MP3 / 24kHz / mono,无论 voice/rate/pitch 怎么设都一样。这是 Microsoft 服务的硬上限,不是配置问题。ffprobe 验证:

ffprobe -v error -show_streams audio/combined.m4a | grep -E "codec_name|sample_rate|bit_rate"
# codec_name=aac
# sample_rate=24000
# bit_rate=102029   # 上限就是 ~96–102k AAC,源是 48k MP3,再怎么重编码也回不去

别浪费时间在以下方向(都不会改善音质):

  • 改 rate/pitch 参数
  • 把 AAC 码率从 128k 提到 192k/256k —— 源码就 48k,徒增文件大小
  • aresample=48000 升采样 —— 0 新增信息,徒增体积

真正能改善音质的路径(按代价从小到大):

  1. 换 voice(零成本,立刻可试)。Xiaoxiao 是默认最自然的,但 48k MP3 暴露齿音最明显。备选:

    • zh-CN-YunxiNeural — 男声,有故事感,最不像"机器女声"
    • zh-CN-XiaoyiNeural — 女声,柔和,齿音最轻
    • zh-CN-YunyangNeural — 男声,新闻播报腔,正式感强
    • zh-CN-YunjianNeural — 男声,活力感 改 generate_audio.py 里的 VOICE 常量,重跑 python3 generate_audio.py。
  2. 用 macOS 系统 say(离线,零网络,音质比 edge-tts 48k MP3 略好但风格明显不同):

    say -v Tingting -r 175 -o chapter01.aiff "旁白内容"
    ffmpeg -i chapter01.aiff -codec:a libmp3lame -qscale:a 2 chapter01.mp3
    

    见 Q5 的"备选"分支。

  3. 换 TTS 引擎(音质跃迁,但配置成本高):

    • Azure TTS(付费,按字符计费)— 同源模型但提供 48k/96k/192k MP3 和 16k/24k/48k PCM,齿音明显改善
    • CosyVoice / GPT-SoVITS(本地开源)— 自带训练或微调能力,可克隆特定声音 这些都需要单独接 API 或装额外服务,不在本文档覆盖范围。

经验法则:先试换 voice(5 分钟),不行再上 macOS say(半小时试出风格差异),最后才考虑换引擎。

换 voice 后的重新同步流程(重新生成音频 → 重渲染 Remotion → 合并),见 references/voice-swap-and-iterate.md。该流程中算 F[] 的 python 代码已抽到 templates/audio_frames.py 的 frames 子命令:

# 第二次渲染后用 ffprobe 拿到实际帧数,例如 4471
ACTUAL=$(ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of csv=p=0 out/final_video.mp4)

# 自动从 scenes.json 算新 F[]
python3 templates/audio_frames.py frames --from-measure scenes.json --total $ACTUAL
# 直接复制输出的 F[] 到 src/Scene.tsx,复制 DURATION_IN_FRAMES 到 src/index.tsx

Q9:Remotion 报 The character ">" is not valid inside a JSX element(v1.0.9 新增)

症状:L23 Section3.tsx:40:66 渲染时 esbuild 报错,定位到某一行。错误信息里的 line:col 是esbuild 看到的源码位置**,比 React 编译器报的位置更准。

根因:subagent 在 JSX 文本节点(夹在两个标签之间的普通文字)里嵌了 JSX 标签,比如:

<div>
  ERC1155 用一张 <code style={{color: "#f0883e"}}>(id → balance)</code> 表管理所有 token 类型。
</div>

JSX 解析器看到 <code> 试图匹配 JSX 元素,但这是文本节点位置,不是合法的 JSX 语法位置。> 被识别为标签闭合,触发 parse error。

修复:用全大写或反引号替代:

<div>
  用一张映射表 (id → balance) 管理所有 token 类型。
</div>

预防(写 subagent prompt 时显式禁):见 references/batch-rendering.md §10。


📂 配套 references 索引

文档 用途
references/worked-example-tsp-solidity04.md 第一个视频(10 段)端到端实例
references/second-video-pattern.md 第二个及之后视频的 cp -R 复用 + 4 个新 visual patterns
references/voice-swap-and-iterate.md 换 voice / 改语速 / 改文本后的 F[] 重算
references/syncing-to-openclaw.md Hermes ↔ OpenClaw 两端 sync(self-update 陷阱)
references/batch-rendering.md N 个视频批量 pipeline(v1.0.6 新增):subagent 委派 + 并行渲染 + 跳过第二遍 render
references/macos-gotchas.md macOS 平台专项坑(Remotion Chrome、ffmpeg 路径等)
references/case-study-pattern.md 历史 / 安全案例复盘类内容的 visual patterns:timeline、year-marker、attack-flow、response phases(v1.0.9 新增)

⚠️ 工具设计原则:non-binding helper,not opinionated workflow(v1.0.6 新增)

这是用户的明确偏好,提炼出来避免下次跑偏。

设计新工具时,默认做"非绑定 helper",不做"opinionated workflow":

类型 例子 用户接受度
✅ non-binding helper audio_frames.py measure/frames —— 一个 Python 工具,根据调用方式自由组合 高,跑了 7 个项目都好用
✅ deterministic wrapper merge.sh —— 单一动作的固定脚本(音频合并),不试图"全流程" 高,方便复制
⚠️ opinionated workflow "批量化项目脚手架脚本" —— 一键从 0 到 mp4,假设了 npm/voice/项目结构 低,用户反馈"加了之后就不通用了"

判断标准:

  • 工具是回答一个问题("F[] 怎么算")还是执行一个完整流程("从 md 到 mp4")?
  • 前者:✅ helper
  • 后者:⚠️ workflow — 需要用户先认同流程的所有假设才有用

反模式:把"npm install 自动化 + 项目结构约定 + 默认 voice + 默认 F[] 算法"打包成一个 new-project.sh——如果用户的下一个项目用 pnpm / 不同的 voice / 不同的场景命名,这脚本就成了阻力。

该做的:保持每个工具的小而专一(一个工具 = 一个职责),让用户用 bash / make / invoke 自由组合。


📤 发布到 SkillHub / ClawHub

发布前准备

  1. 确保 SKILL.md 包含完整的 frontmatter(name, description, author, version, tags 等)
  2. Skill 目录结构清晰,文件命名规范
  3. 准备好封面图(可选,512×512 PNG)

SkillHub(推荐)

访问 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 发布:

# 方式一:直接推送目录
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 嵌入音频 → 完成

5 个原始验证项目(v1.0.0):

  1. docker-registry-guide-final.mp4 — 9场景,153s ✅
  2. deploy-docker-registry-final.mp4 — 16场景,207s ✅
  3. solidtidy-final.mp4 — 11场景,210s,音视频完美同步 ✅

TSP 系列扩展(v1.0.5+): 4. tsp-solidity04-final.mp4 — 10场景,178.9s,Yunxi 男声 ✅ 5. tsp-solidity05-final.mp4 — 13场景,236.7s,Yunxi 男声,13 个新 visual pattern ✅

TSP 完整 27 视频系列(v1.0.9):

  • L04-L25 共 22 个(v1.0.8 截止)
  • L26-L30 新增 5 个:借贷 SimpleLending / DAO SimpleDAO / The DAO Hack 2016 复盘 / Parity Wallet Hack 2017 复盘 / Nomad Bridge Hack 2022 复盘
  • 27 个视频总计 ~75 min / ~167MB,漂移全部 < 70ms
  • 安全案例复盘类内容(28-30)触发了新视觉模式需求,沉淀到 references/case-study-pattern.md
  • L23 JSX 文本节点里嵌 <code> 标签的踩坑(详见 batch-rendering.md §10)— 这条之前没在 SKILL.md 主文档里出现,作为 v1.0.9 的新坑点补充到主文档
  • L26/L27 7-段项目 Scene.tsx 路由计数错误(subagent 写 8 routes for 7 audio)—— 修复脚本模板固化到 batch-rendering.md §6

TSP 完整 22 视频系列(v1.0.8):

  • L04-L25 全 22 个视频,全部使用 YunxiNeural 男声,10 万+ 字文档转 65 分钟视频
  • 总大小 ~138MB,平均每视频 6.3MB
  • 漂移全部 < 50ms(实测最大值 L14 = 64ms)
  • 5 个实测 batch run (L06-10, L11-15, L16-20, L21-25) 总耗时约 1.5 小时
  • 生产环境可用:v1.0.6 之后的项目都是"一次渲染命中",没有需要重渲染的情况

📄 许可

MIT License — 可自由使用、修改、分发。