castmd
一份 Markdown,就是一期播客。
castmd 是一个 Python 库:把 Podcast Markdown(一种刻意最小的对话 DSL)渲染成成品播客音频。人声通过可插拔的语音供应商生成(内置 ListenHub),背景音乐与音效由本地 ffmpeg 装配,最终产出 音频文件 + 带时间戳和说话人标注的 transcript.json。
episode.md ──► 解析 ──► 逐段人声渲染 (Provider) ──► 声音装配 (ffmpeg) ──► episode.mp3 + transcript.json
安装
pip install castmd # 需要本机安装 ffmpeg
快速开始
export LISTENHUB_API_KEY=sk-...
castmd build examples/episode.md -o out/
或作为库使用:
import castmd
from castmd.providers.listenhub import ListenHubProvider
doc = castmd.parse_file("episode.md")
result = castmd.render(
doc,
provider=ListenHubProvider(), # 默认读 LISTENHUB_API_KEY 环境变量
output="out/episode.mp3",
)
print(result.audio_path, result.duration_ms)
for seg in result.transcript:
print(seg.start_ms, seg.speaker, seg.text)
Podcast Markdown DSL
整篇文档 = frontmatter + 若干个用 --- 分隔的段 (Section)。每个段独立渲染一次人声,段间顺序拼接。
---
title: 恐龙灭绝之谜 · 第3期
language: zh
speakers: # 台词署名 → 供应商音色 id
阿哲: awesome_voice_1
小雨: awesome_voice_2
sounds: # 本篇用到的声音资源:锚名 → 文件路径 / URL
开场乐: assets/theme.mp3
爆炸声: assets/boom.wav
鸟叫: assets/birds.mp3
换场乐: assets/transition.mp3
---
[4s](#开场乐)
阿哲: 大家好,欢迎回到恐龙台。今天聊一个大问题——恐龙到底是怎么灭绝的?
小雨: 随着[一颗小行星撞进地球](#爆炸声),白垩纪在那一天结束了。
---
[2s](#换场乐)
阿哲: 第二幕,我们把时间拨回撞击后的清晨。
[1.2s](#鸟叫)
小雨: 幸存下来的,是那些体型小、什么都吃的家伙……
规则(全部规则)
- Frontmatter(YAML):
-
title— 节目标题(可选); -
language— 语言代码,如zh/en(可选,传给供应商); -
speakers— 署名 → 音色 id 的映射。音色 id 是不透明字符串,由所选供应商解释(ListenHub 即 speakerId,用castmd speakers查询); -
sounds— 锚名 → 声音资源,四种形态:本地文件路径(相对 md 文件)、URL、特殊值silence(静音)、生成音乐(写一段描述,由音乐供应商生成):sounds: 开场乐: music: 温暖轻快的钢琴播客片头,渐强开场 爆炸声: assets/boom.wav
-
- 分段
---:每个段是一次独立的人声渲染单元,可增量重跑(见下文缓存)。 - 台词行:
署名: 台词内容,署名必须在speakers中声明;中英文冒号均可;紧随台词行的普通文本行视为该行台词的续行。 - 声音插入,复用 Markdown 链接语法,两种形态:
- 伴随式:台词内
[被说出的文字](#锚名)— 这段文字照常被念出来,音效在文字出现的位置对齐叠加(靠字幕时间戳定位); - 独立式:独立一行
[时长](#锚名),如[1.2s](#鸟叫)或[500ms](#静音)— 插入一段纯声音,人声暂停,时长即截取/补齐长度。
- 伴随式:台词内
- 没有别的语法。不做:精细混音参数、SSML、多层轨道、整段铺底。需要时再扩展。
输出契约
render() → RenderedEpisode {
audio_path: 成品音频(格式由输出扩展名决定:.mp3 / .ogg / .wav)
transcript: [{start_ms, end_ms, speaker, text}, ...] # 装配后已修正时间戳
duration_ms: 总时长
transcript_path: transcript.json 路径
}
下游(播放器、对话系统)只依赖这份契约,不感知 DSL、供应商与装配细节。
供应商抽象
人声渲染的唯一接口是 SpeechProvider:
from castmd import SpeechProvider, ScriptLine, SpeechResult
class MyProvider(SpeechProvider):
name = "my-tts"
def synthesize(self, script: list[ScriptLine], *, language=None) -> SpeechResult:
"""给我台词序列(voice + text),还我音频文件 + 带毫秒时间戳的字幕 cues。"""
契约要点:
ScriptLine.voice是不透明字符串,provider 自行解释(音色 id、voice name……);SpeechResult必须包含逐句/逐段的字幕 cues(伴随式音效对齐依赖它);- 除此之外,库不对 provider 做任何假设。SRT 解析、文本对齐、装配、缓存都在 provider 之外完成。
音乐生成是平行的第二个抽象 MusicProvider(generate(prompt) → 音频文件),给 sounds 里的 music: 声源用;生成结果按 prompt 哈希持久缓存,同一段描述只生成一次。
内置实现:
| Provider | 后端 | 说明 |
|---|---|---|
ListenHubProvider |
POST /v1/speech(同步多说话人 TTS) |
精确渲染台词,返回音频 + SRT;单段 ≤ 20,000 字符 |
ListenHubMusicProvider |
POST /v1/music/instrumental(异步任务) |
prompt → 纯音乐;轮询任务直到完成 |
段级缓存(增量重跑)
每段渲染结果按 hash(provider, language, 段台词) 缓存(默认 <输出目录>/.castmd-cache/)。重跑时:
- 台词没动的段 → 直接复用上次人声渲染(不再花供应商积分);
- 只改了声音链接 → 该段只重新装配;
- 动了台词 → 只重渲染该段。
CLI
castmd build episode.md -o out/ # 渲染,产出 <title>.mp3 + transcript.json
castmd build episode.md -o out/ep.ogg # 指定输出文件与格式
castmd build episode.md --no-cache # 忽略缓存,全部重渲染
castmd speakers --language zh # 列出 ListenHub 可用音色
castmd check episode.md # 只解析校验,不渲染
依赖
- Python ≥ 3.10;
pyyaml、httpx - 本机
ffmpeg/ffprobe(声音装配)
设计文档
架构、装配算法、字幕对齐策略、缓存键设计见 docs/design.md。
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file castmd-0.1.0.tar.gz.
File metadata
- Download URL: castmd-0.1.0.tar.gz
- Upload date:
- Size: 186.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41839b0f43262777fa8d65f9da517e18ff6cd419ba4fc998e27b07677de13478
|
|
| MD5 |
7578d9f923dd65c2a34aeb83a10da2d0
|
|
| BLAKE2b-256 |
e0b3bbd2f1582e272fdab5b5ef4c1909d146949c527eb52f9dc74c541056159d
|
File details
Details for the file castmd-0.1.0-py3-none-any.whl.
File metadata
- Download URL: castmd-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0dfb2aad02659d1af53083590ee644d034330d2fcfe6c2616538abb1094ffd8
|
|
| MD5 |
a23004cf62689869da23f6282db47194
|
|
| BLAKE2b-256 |
dee300d030b3eca8d58547c2ad94762c31674a7635e11b46ce98356c8ca8050f
|