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;pyyaml、httpx
  • 本机 ffmpeg / ffprobe(声音装配)

设计文档

架构、装配算法、字幕对齐策略、缓存键设计见 docs/design.md。

License

MIT

Metadata

Release files for castmd 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for castmd 0.1.0
File Size Uploaded Provenance
castmd-0.1.0.tar.gz 186.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for castmd 0.1.0
File Interpreter ABI Platform
castmd-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 210.7 kB

Release files / castmd-0.1.0.tar.gz

Download URL castmd-0.1.0.tar.gz
Size 186.3 kB
Tags Source
SHA-256 checksum
How to use checksums
41839b0f43262777fa8d65f9da517e18ff6cd419ba4fc998e27b07677de13478
BLAKE2b-256 checksum
How to use checksums
e0b3bbd2f1582e272fdab5b5ef4c1909d146949c527eb52f9dc74c541056159d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.2

Release files / castmd-0.1.0-py3-none-any.whl

Download URL castmd-0.1.0-py3-none-any.whl
Size 24.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0dfb2aad02659d1af53083590ee644d034330d2fcfe6c2616538abb1094ffd8
BLAKE2b-256 checksum
How to use checksums
dee300d030b3eca8d58547c2ad94762c31674a7635e11b46ce98356c8ca8050f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.2

Security for castmd 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

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