Skip to main content

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](#鸟叫)

小雨: 幸存下来的,是那些体型小、什么都吃的家伙……

规则(全部规则)

  1. Frontmatter(YAML):
    • title — 节目标题(可选);

    • language — 语言代码,如 zh / en(可选,传给供应商);

    • speakers — 署名 → 音色 id 的映射。音色 id 是不透明字符串,由所选供应商解释(ListenHub 即 speakerId,用 castmd speakers 查询);

    • sounds — 锚名 → 声音资源,四种形态:本地文件路径(相对 md 文件)、URL、特殊值 silence(静音)、生成音乐(写一段描述,由音乐供应商生成):

      sounds:
        开场乐:
          music: 温暖轻快的钢琴播客片头,渐强开场
        爆炸声: assets/boom.wav
      
  2. 分段 ---:每个段是一次独立的人声渲染单元,可增量重跑(见下文缓存)。
  3. 台词行:署名: 台词内容,署名必须在 speakers 中声明;中英文冒号均可;紧随台词行的普通文本行视为该行台词的续行。
  4. 声音插入,复用 Markdown 链接语法,两种形态:
    • 伴随式:台词内 [被说出的文字](#锚名) — 这段文字照常被念出来,音效在文字出现的位置对齐叠加(靠字幕时间戳定位);
    • 独立式:独立一行 [时长](#锚名),如 [1.2s](#鸟叫)[500ms](#静音) — 插入一段纯声音,人声暂停,时长即截取/补齐长度。
  5. 没有别的语法。不做:精细混音参数、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;pyyamlhttpx
  • 本机 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

castmd-0.1.0.tar.gz (186.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

castmd-0.1.0-py3-none-any.whl (24.5 kB view details)

Uploaded Python 3

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

Hashes for castmd-0.1.0.tar.gz
Algorithm Hash digest
SHA256 41839b0f43262777fa8d65f9da517e18ff6cd419ba4fc998e27b07677de13478
MD5 7578d9f923dd65c2a34aeb83a10da2d0
BLAKE2b-256 e0b3bbd2f1582e272fdab5b5ef4c1909d146949c527eb52f9dc74c541056159d

See more details on using hashes here.

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

Hashes for castmd-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d0dfb2aad02659d1af53083590ee644d034330d2fcfe6c2616538abb1094ffd8
MD5 a23004cf62689869da23f6282db47194
BLAKE2b-256 dee300d030b3eca8d58547c2ad94762c31674a7635e11b46ce98356c8ca8050f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page