Skill: mengbin92/doc-to-video (version 1.0.0)

--- 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 — 可自由使用、修改、分发。


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)