中文小说 tts 项目二次优化

img

一、背景

之前写过一篇《中文小说 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 缺少 subtitles filter 时,不直接失败,而是跳过烧录并给 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