中文小说 tts 项目二次优化

一、背景
之前写过一篇《中文小说 tts 生成音视频》,当时项目目标很简单:把本地 txt 小说切章节,喂给 TTS,最后把音频转成方便分发的视频。
这段时间在原项目上又做了迭代, 算是一个更全面的本地工具:
- 支持 txt / epub / mobi,
- 默认输出 mp3,也可以生成 SRT 字幕和带封面的 MP4;
- CLI 可以交互使用,也可以被脚本、CI、agent 或 MCP 调用。
GitHub - Wizna/txt2audio: transform txt book to audio book
二、主要优化
-
改动分成几个方向:
- TTS 模型
- 文本输入和章节解析
- 音频 / 字幕 / 视频输出链路
- 配置化和断点恢复
- agent-friendly 的结构化接口
2.1 从脚本变成命令行工具
- 第一版还是直接运行
python3 src/transform_to_audio.py xxx.txt,然后靠 prompt 输入章节范围 - 现在项目已经配置了 CLI entrypoint,常用方式变成:
uv run txt2audio book.txt --range all
uv run txt2audio book.txt --range 0~8
uv run txt2audio book.txt --video --range all
uv run txt2audio book.txt --range all --srt
-
主要是“交互”和“自动化”分开了,人和 agent 都能用:
- 人手动跑时,不传
--range, 仍然可以先看目录再选择章节 - agent / 脚本跑时,显式传
--range,不会卡在 stdin - 加
--json后,stdout 只输出机器可读的最终结果 - 加
--quiet后,可以尽量减少终端 print
- 人手动跑时,不传
-
例如:
uv run txt2audio book.txt --range all --json
uv run txt2audio --dump-config --json
2.2 输入格式从 txt 扩展到 epub / mobi
- 第一版只处理
.txt,而现实里很多书是 epub / mobi -
现在支持:
.txt.epub.mobi
- epub / mobi 会通过 Calibre 的
ebook-convert先转换成同目录下的*.txt2audio.txt - 这个中间 txt 会被复用。第一次转换完以后,后续如果只是调整语速、输出格式、字幕参数,不需要反复解析电子书源文件
2.3 章节解析更稳了一些
- 旧版主要识别“卷”“章”,并特殊处理“序言”“楔子”“后记”
-
现在这块思路没有大改,但补了不少实际使用时会遇到的小问题:
- “序”“序章”“前言”“引言”“终章”等特殊章节会被单独识别
- 正文里纯装饰分隔线,例如
======、------、******,会在进入 TTS 前过滤掉 - 第一个卷 / 章之前的内容会归到“引言”,不会跑到书目录外面
- 输出目录会额外做路径清理,避免 Windows 下的非法字符、保留文件名、尾部空格和点号 (虽然我没有实际测试啊)
- 章节名太长时会截断并附带 hash,尽量既可读又不撞名
2.4 TTS 从 XTTS 换到 CosyVoice3
- 第一版使用的是 coqui tts 的
xtts_v2 - 现在换成了
Fun-CosyVoice3-0.5B,通过 submodule 的方式,使用参考音频做零样本声音克隆:
tts:
model_dir: pretrained_models/Fun-CosyVoice3-0.5B
speaker_wav: resources/my_speaker.wav
prompt_text: "You are a helpful assistant.<|endofprompt|>他站在城墙上,望着远处的千军万马,脸上没有一丝慌乱。"
speed: 1.05
inter_sentence_silence_ms: 150
- 这里有一个硬性约束:
<|endofprompt|>后面的中文必须和speaker_wav里实际说的内容一致 - 不一致时不一定会直接报错,但声音稳定性、语气、发音都可能变差
- 还有一个踩坑是依赖版本。当前
torch/torchaudio仍然限制在<2.4,因为更高版本下 CosyVoice 相关链路的中文输出会出现异常 - 另外,代码里对少量高频多音字做了拼音 token 标注,例如“校”在“精校”“校对”里的读法,可以按需添加;这里没有大范围自动标注,因为标太多会影响声音自然流畅
2.5 输出默认从 wav 变成 mp3
- 第一版结果保存成本地 wav,质量好,但是文件很大
- 现在纯音频模式默认输出 mp3:
audio:
max_chars_per_clip: 6300
output_format: mp3
mp3_bitrate: 128k
- 如果想保留无压缩 wav,也可以临时覆盖:
uv run txt2audio book.txt --range all --output-format wav
- 对有声书来说,
128k的 mp3 已经比较够用;视频里的 aac 默认也降到了更适合语音的码率 - 这类优化不改变 TTS 质量,但会明显减少磁盘占用和上传成本
2.6 原子写入和断点恢复
- 以前长时间 TTS 中途被打断,最容易留下半个 wav 或半个 mp4。下一次运行时如果只判断文件存在,就可能把坏文件当成完成结果
-
现在音频、字幕、视频、章节 manifest 都遵循同一个原则:
- 先写到
.tmp文件 - 成功后再 rename / replace 到最终路径
- 下次运行前清理残留
.tmp - 遇到 0 字节输出也会删掉重跑
- 先写到
- 断点恢复也沿用这个逻辑:如果 mp4 / mp3 / wav 已经存在并且非空,就跳过;如果需要字幕但字幕缺失,就重新生成对应片段
2.7 字幕从“附属功能”变成完整链路
- 新版可以单独导出 SRT:
uv run txt2audio book.txt --range all --srt
- 也可以在视频模式下直接烧录字幕:
uv run txt2audio book.txt --video --range all
uv run txt2audio book.txt --video --keep-srt --range all
- 字幕时间不是简单按文本长度平均分,而是基于实际合成出的音频做句级对齐
-
这里用到了
stable-ts/ Whisper 对齐,流程大概是:- 按句子组织 TTS 输入
- 得到当前 batch 的真实音频
- 用对齐模型把文本贴回音频时间轴
- 如果 batch 对齐失败,就自动拆成更小的组重试
- 最后再把句级时间合并成当前 clip 的 SRT
-
字幕显示上也补了不少边角问题:
- 长字幕按分句标点拆分
- 过长文本自动换行
- 去掉字幕帧首尾一些非必要的,只保留问号、叹号之类的
- ffmpeg 缺少
subtitlesfilter 时,不直接失败,而是跳过烧录并给 warning
MarginV也有个坑。ffmpeg 的 ASS 字幕坐标不是直接按最终视频像素计算,而是默认 PlayResY=288。比如 1280 高的竖屏里,MarginV=90实际大约是 400px
2.8 视频封面和编码参数
- 封面仍然保留第一版的思路:根据书名 hash 出一个固定底色,同一本书的封面保持一致
-
但现在做了几个改进:
- 支持竖屏和横屏
- 标题太长会自动折行
- 字体加载失败时回退到默认字体
- 静态封面视频使用更合适的 CRF / framerate / bitrate
- MP4 加上
+faststart,方便在线播放
- 命令上也从 shell 字符串变成 list-based
subprocess.run(...),路径里有空格、中文、特殊字符时更稳
2.9 配置化和临时覆盖
- 很多参数不应该写死在代码里,所以现在集中放到
config.yaml -
常改的包括:
- TTS 模型路径
- 参考音频路径
- prompt text
- 语速
- 输出格式
- mp3 / aac 码率
- 视频尺寸和方向
- 字幕样式
- 输出目录
- 临时实验可以不用改配置文件,直接用命令行覆盖:
uv run txt2audio book.txt --range all --speed 0.95
uv run txt2audio book.txt --range all --set audio.mp3_bitrate=192k
uv run txt2audio book.txt --range all --set video.subtitles=false
2.10 给 agent 用的接口
- 过去这个项目只能“人看终端,人做判断”。现在提供了一组不会加载 TTS 模型的轻量检查接口:
uv run txt2audio book.txt --validate-paths --json
uv run txt2audio book.txt --plan-json --range all
uv run txt2audio book.txt --range all --chapter-manifest
uv run txt2audio book.txt --range all --json --events-jsonl events.jsonl
--validate-paths只解析章节和输出路径,适合提前发现非法路径或奇怪目录--plan-json会给出计划生成哪些章节、目标格式、哪些已有文件会跳过--events-jsonl会持续写出run_started、chapter_started、artifact_created、chapter_completed、run_completed等事件chapter_manifest.json则记录一次运行后每章的输出、状态、失败信息- 项目还内置了一个 MCP server:
{
"mcpServers": {
"txt2audio": {
"command": "uv",
"args": ["run", "txt2audio-mcp"]
}
}
}
三、一些踩坑
- TTS 质量不只是模型本身决定的,prompt text、参考音频、语速、标点清理都会影响最终听感
- 多音字标注要克制。只修高频、确定、反复读错的字,音标多了非常不自然
- ffmpeg 的字幕 filter、路径转义、ASS 坐标系都有坑,尤其是路径里有空格、中文、Windows 盘符时
四、总结
- 有时候我怀疑,很多身体器官不是用进废退,反而是用就报废;
- 眼睛看多了眼睛干疼,听多了耳朵疼,坐久了腿疼
Written on
