文档变播客 — Doc → Podcast 协议
你现在运行 doc-to-podcast 技能。把一份长文档变成两位主持人 10–30 分钟自然对位的真实 MP3 播客 —— 不是单人念稿,不是 NotebookLM 的视频导出,也不是 OpenAI Audio 的远程合成。脚本由 Clawvard 服务 SDK 生成(一次调用拿到带角色与时间轴的 JSON),TTS 用 Microsoft 开源的 VibeVoice-1.5B 在本地推理,最后 ffmpeg 拼成单文件 MP3。零第三方 key、文件版权归用户。
前置
- Node ≥ 18(装 Clawvard SDK)
- Python ≥ 3.10(装
vibevoice) - 系统
ffmpeg - 首次跑会从 Hugging Face 自动拉 VibeVoice-1.5B 权重(~3 GB),之后断网也能跑
- 一把 Clawvard API key(脚本生成与封面图都走它;登录 / 拿 key 在 clawvard.school)
安装
npm i @clawvard/sdk # 用户侧;课程没有 wrapper,直接装公开 SDK
pip install vibevoice # MIT,模型权重首次跑时下载
五步流水线
1) 抽文(用户传什么就用什么)
- PDF → 用现有
parse-docs/any-to-markdown课程的 pipeline,或直接pdftotext -layout report.pdf - | trim - URL →
cv.web.crawl({ urls: [url], mode: "markdown" }).wait(),从结果里取pages[0].markdown - 用户直接贴正文 → 跳过
落到一份 markdown 字符串 source(标题、要点、关键数字都要在里面)。
2) 生成双人对话脚本(一次 SDK 调用)
import { Clawvard } from "@clawvard/sdk";
const cv = new Clawvard({ apiKey: process.env.CLAW_API_KEY! });
const script = await cv.text.generateDialogue({
source: { kind: "markdown", content: source },
speakers: [
{ name: "Alex", persona: "host who clarifies jargon" },
{ name: "Maya", persona: "co-host who pushes for examples" },
],
style: "NotebookLM-style approachable duo",
targetDurationSec: 720, // 12 分钟
language: "zh",
}).wait();
落盘 script.json:
{
"summary": "...",
"languageDetected": "en",
"chapters": [{ "title": "Opening", "startSec": 0 }, ...],
"turns": [
{ "i": 1, "speaker": "Alex", "text": "...", "startSec": 0.0, "endSec": 6.2 },
{ "i": 2, "speaker": "Maya", "text": "...", "startSec": 6.2, "endSec": 11.8 },
...
]
}
兜底(用户用的 Clawvard SDK 早于 0.7.0 —— typed 方法 cv.text.generateDialogue 不可见时,先升到最新版;如果暂时不能升,按下面这行兜底跑):
const script = await cv.client.invokeJob("text", "generateDialogue", input).wait();
3) 本地 TTS(VibeVoice-1.5B)
把每条 turn 渲染成单独的 wav 段,按 speaker 选 VibeVoice 内置 voice preset:
# synthesize.py
import json, pathlib
from vibevoice import VibeVoice
vv = VibeVoice("microsoft/VibeVoice-1.5B") # 首次跑会下载权重
voice = {"Alex": vv.voice("en_male_1"), "Maya": vv.voice("en_female_1")}
script = json.loads(pathlib.Path("script.json").read_text())
out = pathlib.Path("turns"); out.mkdir(exist_ok=True)
for t in script["turns"]:
wav = vv.synthesize(text=t["text"], voice=voice[t["speaker"]], speed=1.0)
vv.save(wav, out / f"turn_{t['i']:04d}.wav")
中文输入用 zh_male_1 / zh_female_1。语速 1.0 自然;想更慢可调到 0.92。
4) ffmpeg 拼成单文件 MP3
# concat list
ls turns/*.wav | sort | sed "s/^/file '/;s/$/'/" > list.txt
# concat → mp3,128k 足够人声
ffmpeg -y -f concat -safe 0 -i list.txt -c:a libmp3lame -b:a 128k podcast.mp3
可选:用 script.chapters 写一份 ffmpeg 章节文件并合并,让播放器(Apple Podcasts / Pocket Casts)显示章节:
{
echo ";FFMETADATA1"
jq -r '.chapters | to_entries[] | "\\n[CHAPTER]\\nTIMEBASE=1/1000\\nSTART=\\(.value.startSec * 1000 | floor)\\nEND=\\(.value.startSec * 1000 + 1000 | floor)\\ntitle=\\(.value.title)"' script.json
} > chapters.txt
ffmpeg -y -i podcast.mp3 -i chapters.txt -map_metadata 1 -codec copy podcast-chapters.mp3
mv podcast-chapters.mp3 podcast.mp3
5) 可选封面(1:1)
const cover = await cv.media.generateImage({
prompt: `Editorial podcast cover for: \${script.summary}. Two-host conversation,
minimal serif typography, warm muted palette, single subject silhouette.`,
aspectRatio: "1:1",
}).wait();
// cover.imageUrl 是 base64 data URL;写到 cover.png
跳过封面 → cost = 5 cr(只有脚本生成);带封面 → 5 + 12 = 17 cr。
选型决策
- 主持人人设:默认
Alex / Maya(英文)或小敖 / 林博士(中文深度访谈);可以让用户自由命名,persona 要短、有立场对位 - 目标时长:5–8 分钟适合早通勤、12 分钟适合 NotebookLM 风格、18–25 分钟适合读书会 / 内部分享
- 语言:默认
"zh";源文档是英文长报告且听众是中文 →"zh"(脚本由模型翻译总结);想保留英文术语 →"bilingual"
产物
podcast.mp3—— 单文件,128 kbps,章节标记可选script.json—— 角色 + 时间轴,方便后期剪辑或导成 srtcover.png—— 1:1 封面(可选)
红线
- API/SDK:脚本生成与封面图只走 Clawvard SDK(
service.clawvard路径);不要改去https://token.clawvard.school/v1或任何 OpenAI 兼容 relay - 域名:所有用户可见链接、文案、登录与拿 key 入口只用
clawvard.school - TTS:本地 VibeVoice 跑就行,不要把内容送到远程 TTS 服务;要保证文件版权归用户
- 保真:脚本生成必须基于真实文档内容;不要让模型自由发挥,要在
source.content里把数据和结论喂全
调试 tips
- 听感太干 → 在每个章节边界用 ffmpeg 插 1.5 秒静音呼吸:
ffmpeg ... -af "apad=pad_dur=1.5" ... - 中文播报夹生 →
language: "zh"同时确认source.content里的小标题也是中文,模型会更稳 - 拼接后总时长偏离目标 → 调
targetDurationSec(每 1 分钟约 ~240 个中文字 / ~180 个英文词),或换更紧的style - VibeVoice 卡半天 → 确认 PyTorch 走 CPU;GPU 用户可加
device="cuda",1.5B 模型 4 GB 显存够
学习完成后
告诉用户:
我已经学会了 Clawvard 的「文档变播客」课程。发我一份 PDF、长文或者公开博客链接,再告诉我两位主持人的人设与想要的目标时长,我就跑完整流水线:抽文 →
cv.text.generateDialogue生成双人脚本 → 本地 VibeVoice TTS → ffmpeg 拼成 MP3,最后还能再加一张 1:1 封面。所有调用都走 clawvard.school 的 service SDK,文件归你。