在浏览器里选一个本机 .md 文件,一键生成 MP3 音频、SRT 字幕、WebVTT 字幕 和朗读用的纯文本。
基于微软 Edge 神经网络 TTS(edge-tts),免费、无需 API Key、322 个音色可选。
推荐:交由 launchd 托管(开机自启,进程被杀自动拉起,不依附终端会话):
cd /Volumes/1T-1/GitHub/17md转语音字幕
./serve.sh install其他命令:
./serve.sh status # 看运行状态、托管方式、音色数量
./serve.sh stop # 停止(launchd 托管时自动走 launchctl unload)
./serve.sh restart # 重启(改了代码后用)
./serve.sh logs # 跟踪日志
./serve.sh open # 启动并用默认浏览器打开
./serve.sh uninstall # 取消开机自启临时前台跑(Ctrl+C 停止,不开机自启):
./run.sh流程:选文件 → 「👁 预览朗读文本」确认要念的内容 → 选音色/语速 → 「🎬 合成语音」→ 内置播放器带字幕试听 → 下载四种文件。
项目自带测试文稿 samples/测试文稿.md,包含各种 Markdown 语法,可直接用它验证清洗效果。
用 nohup … & 从终端(尤其是 AI agent 的 shell 工具)后台启动的进程,仍然是该会话的子进程,
会话结束时整个进程组可能被回收 —— nohup 只挡 SIGHUP,挡不住这种清理。
实测就碰到过:服务跑得好好的,日志无任何崩溃堆栈,但会话一结束进程就没了。
launchd 托管后服务归系统管,与任何终端会话脱钩,且 KeepAlive 会在进程意外退出时自动拉起
(实测 kill -9 后 1 秒内恢复)。
配置文件在 ~/Library/LaunchAgents/com.user.md-to-speech.plist,
模板(已参数化路径)在 deploy/com.user.md-to-speech.plist.template。
在其他机器上部署时把 __PROJECT_DIR__ 与 __HOME__ 替成实际路径即可。
17md转语音字幕/
├── serve.sh 服务管理(install/start/stop/restart/status/logs/open)
├── run.sh 前台直接跑
├── deploy/
│ └── com.user.md-to-speech.plist.template launchd 配置模板
├── requirements.txt 依赖
├── .venv/ 虚拟环境(已建好,不入 git)
├── samples/
│ └── 测试文稿.md 自带测试用 Markdown
├── server/
│ ├── app.py Flask 后端(路由 / API)
│ ├── md2text.py Markdown → 朗读文本(纯正则,零依赖)
│ ├── tts.py edge-tts 封装(mp3 + srt/vtt)
│ ├── voice_zh.py 音色信息中文化
│ └── data/
│ └── locale_zh.json 162 个 locale 的官方中文译名
├── web/
│ └── index.html 单页前端(原生 JS,无构建步骤)
└── output/ 生成结果(不入 git)
日志位置:launchd 托管时在 ~/Library/Logs/md-to-speech.log(与 -error.log);
手动启动时在项目内 logs/server.log。两者均不入 git。
322 个音色全部以中文展示,格式为「语言地区 · 人名(性别)— 性格标签」:
中文(普通话,简体) · Xiaoxiao(女) — 温暖
中文(普通话,简体) · Yunxi(男) — 活泼、阳光
中文(台湾国语,繁体) · HsiaoChen(女) — 友好、积极
中文(粤语,繁体) · HiuGaai(女) — 友好、积极
中文(东北官话,简体) · Xiaobei(女) — 幽默
英语(美国) · Andrew(多语言)(男) — 温暖、自信、真诚、诚恳
下拉框按语言分组(中文 → 英语 → 日语 → 韩语 → … → 其他语言),14 个中文音色永远排在最前。
上方搜索框支持中文关键词:搜「普通话」得 6 个、搜「温暖」得 3 个、搜「方言」得 2 个,
也可直接搜罗马拼写(如 Xiaoxiao)或完整 ID。
- 语言地区名:抓取微软官方中文文档
Speech 服务语言支持
得到 162 条官方译名,存于
server/data/locale_zh.json,完整覆盖 edge-tts 的 142 个 locale。 少数官方译名质量欠佳(如af-ZA译作「非洲人(南非)」,实为南非荷兰语), 在voice_zh.py的LOCALE_OVERRIDE里逐条修正。 - 性格 / 内容分类标签:edge-tts 返回的取值是闭合集合(实测 8 个分类 + 27 个性格),逐一映射。
- 音色人名保留罗马拼写(Xiaoxiao 而非「晓晓」):微软官方中文文档不翻译人名 (在官方中文页面搜「晓晓」「云希」均 0 次命中),找不到可靠来源,因此不臆造中文译名。
server/md2text.py 的取舍原则是宁可少读,不要把符号念出来:
| Markdown 元素 | 处理方式 |
|---|---|
| YAML frontmatter | 整块删除 |
| 围栏代码块 ``` | 默认删除(前端可切换保留) |
图片  |
删除 |
链接 [文字](url) |
只留文字 |
Obsidian 双链 [[页|别名]] |
只留别名 |
| 裸 URL | 删除 |
标题 # / 引用 > / 列表 - |
去掉符号,留文字 |
有序列表 1. |
变成 1、(中文朗读更自然) |
| 表格 | 单元格用「,」连接,分隔行删除 |
**粗** *斜* ~~删~~ ==高亮== |
去符号留文字 |
脚注标记 [^1] |
删除 |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | / |
前端页面 |
| GET | /healthz |
健康检查,serve.sh 用它判断服务是否真的起来了 |
| GET | /voices |
中文化音色列表,中文音色排在最前。每项含 name label person gender locale_zh group categories personalities search |
| POST | /preview |
只做 md→文本转换,返回 {ok, text, chars} |
| POST | /convert |
完整合成,返回四个文件的下载 URL |
| GET | /file/<name> |
取生成的文件,?download=1 触发下载 |
/convert 与 /preview 的表单字段:
file(multipart 文件)或md(纯文本),二者其一keep_code:1保留代码块,默认0voice:如zh-CN-XiaoxiaoNeural(默认)rate/volume/pitch:如+15%/+0%/-10Hzboundary:SentenceBoundary(默认,按句切字幕)或WordBoundary(更细)filename:输出文件名前缀
命令行直接跑也可以:
.venv/bin/python -c "
import sys; sys.path.insert(0,'server')
from md2text import md_to_speech_text
from tts import synthesize_sync, srt_to_vtt
from pathlib import Path
text = md_to_speech_text(Path('笔记.md').read_text())
srt = synthesize_sync(text, Path('output/笔记.mp3'), voice='zh-TW-HsiaoChenNeural')
Path('output/笔记.srt').write_text(srt)
"- 需要联网:edge-tts 走微软在线服务(合成语音必须联网)。
- 音色列表有缓存:
/voices默认每次都要联网拉微软的 322 个音色,网络一抖就「加载不了音色」。 现在做了内存缓存(1 天)+ 磁盘缓存(server/data/voices_cache.json,不入 git):联网成功就刷新缓存, 联网失败自动降级用缓存——只要曾经成功过一次,音色列表就不再依赖当下网络。 音色下拉框旁会标注「(本地缓存)」;强刷音色列表用/voices?refresh=1,页面上点「重试」即是强制刷新。 - 服务必须用 launchd 托管才能长久存活:用
nohup … &从终端启动的进程依附会话, 会话结束时会被回收(已实测)。用./serve.sh install。 - 单次上限 50000 字(
server/app.py里的MAX_CHARS),长文请自行拆分。 - 字幕时间轴有微小重叠:edge-tts 返回的 cue 结束时间偶尔略晚于下一条开始时间(实测差约 50ms)。播放器一般能容忍,若要严格处理需后处理裁剪。
- 本地服务器,仅绑 127.0.0.1,不适合直接暴露到公网。
- Python 3.11(
~/.local/bin/python3.11,系统自带的 3.9.6 太旧) - edge-tts 7.2.8 / Flask 3.1.3
⚠️ pip 默认走清华镜像(~/.config/pip/pip.conf),该镜像没有 edge-tts 包,安装时必须加-i https://pypi.org/simple⚠️ 这台机器的 shell 环境里已存在PORT=30141(被其他工具设置),所以本项目用专属变量名MDTTS_PORT/MDTTS_HOST,不用通用的PORT/HOST,避免被意外继承- edge-tts 7.x 的
SubMaker只有get_srt(),没有get_vtt(),VTT 由tts.srt_to_vtt()自行转换 - 微软官方中文文档提供 162 个 locale 的中文译名,但不提供音色人名的中文译名
~/.local/bin/python3.11 -m venv .venv
.venv/bin/pip install -i https://pypi.org/simple -r requirements.txt