基于 Story-Creator 项目中 TTS 朗读功能的实际开发经验总结。本文聚焦 Qwen3-TTS 在中文长文本语音合成场景下的问题与优化方案。

本文档基于 Story-Creator 项目实际生产环境经验编写,测试模型:Qwen3-TTS-12Hz-1.7B-CustomVoice-bf16

项目地址: https://github.com/renfufei/Story-Creator



1. 模型简介

Qwen3-TTS 是通义千问团队推出的文本转语音模型,支持中英文语音合成。

本项目使用的是社区量化版本 Qwen3-TTS-12Hz-1.7B-CustomVoice-bf16,通过本地部署提供 OpenAI 兼容的 API 接口。

部署方式

  • 本地部署,暴露 /v1/audio/speech 端点(OpenAI TTS API 兼容格式)
  • 请求参数:model、input(文本)、voice(音色)、response_format(mp3)、speed
  • 支持自定义音色(Custom Voice),可通过参考音频克隆声音

调用示例

POST /v1/audio/speech
{
  "model": "Qwen3-TTS-12Hz-1.7B-CustomVoice-bf16",
  "input": "你好,欢迎收听本章内容。",
  "voice": "serena",
  "response_format": "mp3",
  "speed": 1.0
}

2. 优点

优势说明
本地部署数据不出服务器,无调用次数限制,适合批量生成
中文支持好对标准中文文本朗读效果自然,语调合理
自定义音色支持通过参考音频克隆特定声音,适合角色配音
低延迟本地推理,单块(≤200字)生成通常在几秒内完成
OpenAI 兼容可直接复用 OpenAI TTS 客户端代码,无需额外适配
成本为零一次部署,无后续 API 费用

3. 缺点与踩坑

这是本文的重点。以下问题均在实际批量生成数十万字网络小说音频时发现。

3.1 拟声词/语气词导致音频卡死循环

严重程度:致命

这是 Qwen3-TTS 最严重的问题。当输入文本包含特定拟声词或语气词时,模型会陷入无限重复循环——生成一段极短的音节后不断重复,直到达到最大时长限制。产出的音频听起来像"卡碟"。

实际测试的问题字符清单(共 80+ 个):

啊 哎 唉 哦 噢 喔 哟 嗯 唔 呃 嗝 咦 嗨 哈 嘻 噗 咳 啐 唾
呼 吁 咝 嘶 咄 嗬 嚄 唏 咿 呜 哇 埴 咕 噜 咯 咚 咣 当 叮
咛 铃 嗖 嗡 嗒 嘭 嘣 砰 啪 噼 嘎 嘠 嘞 汪 咩 哞 嗷 啾 咴
嗳 嗽 嚏 啜 咂 吧 咀 咽 呷 啖

这些字符涵盖:

  • 语气词:啊、哎、唉、哦、嗯、唔、呃
  • 象声词/拟声词:咚、咣、砰、啪、噼、嘭
  • 动物叫声:汪、咩、哞、嗷、啾
  • 人体声音:咳、嗽、嚏、啜、咂

特征:这些字符有一个共同点——它们本身就是"声音的文字描述",模型在尝试合成这类"纯声音"时容易陷入循环。

3.2 特殊符号处理问题

模型无法正确处理多种特殊符号:

  • 装饰符号(★☆◆◇■□▲△等):生成异常音频或静音
  • 箭头符号(→←↑↓):无法朗读
  • Markdown 格式标记(**加粗**、# 标题):会尝试朗读星号和井号
  • 不可见/控制字符:导致不可预期的行为

3.3 长文本分块要求

Qwen3-TTS 对单次输入长度敏感。经实测,超过 200 个字符的输入:

  • 生成质量显著下降
  • 容易出现截断或重复
  • 推理速度非线性增长

必须将文本切分为 ≤200 字符的块分别合成,再拼接音频。

3.4 标点符号对语音的影响

标点对 TTS 输出影响巨大:

标点类型问题
引号(“”‘’「」)导致不自然的停顿或语调变化
破折号(——)生成异常长停顿或卡顿
省略号(……)产生重复音节
书名号(《》)试图朗读尖括号音
中文冒号(:)产生不必要的停顿
连续多个逗号产生连续停顿,节奏紊乱

3.5 数字与百分比无法直接朗读

模型对阿拉伯数字的处理不稳定:

  • "100" 可能读成 “一零零” 而非 “一百”
  • "3.14" 可能逐字读 “三点一四” 也可能出错
  • "50%" 大概率读成 “五零百分号” 而非 “百分之五十”
  • 运算符号(+、-、×)直接无法朗读

3.6 繁体字发音问题

当 AI 生成的小说文本中混入繁体字时(常见于古风题材),TTS 可能:

  • 读出错误发音
  • 产生卡顿
  • 典型案例:“氣” 和 “裏” 在某些上下文中发音异常

4. 优化手段

以下是项目中实际采用的完整优化方案。

4.1 文本替换规则系统

设计了一套模板化的文本预处理管道,在送入 TTS 之前对文本进行清洗。共 7 个模板,按顺序执行:

#模板 ID名称作用
1tts-artifactsTTS卡音字符替换移除所有已知会导致卡音的拟声词/语气词
2control-chars不可见/控制字符清理将控制字符替换为逗号(保留换行)
3markdownMarkdown格式清理移除标题、加粗、斜体等格式标记
4punctuation引号/括号/书名号处理移除引号和书名号,破折号/省略号转逗号
5symbols特殊符号清理移除装饰符号,英文逗号转中文逗号
6whitespace空白规范化空格转逗号,合并连续逗号/换行
7text-normalize文本规范化修正连续标点、繁简混杂、拟声词顿号

设计要点:

  • 模板可通过管理界面自定义,支持绑定到特定 TTS 配置
  • 每个模板包含多条规则,支持正则和字面量匹配
  • 规则按序执行,后续规则处理前序规则的输出
  • 未绑定自定义模板时,默认使用全部内置模板

4.2 数字→中文转换

在文本替换规则之前先执行数字转换:

百分比优先: "50%" → "百分之五十"
整数:      "1234" → "一千二百三十四"
小数:      "3.14" → "三点一四"
万/亿:     "100000000" → "一亿"

关键顺序:必须先转百分比再转普通数字,否则 % 符号处理后数字已被转换。

转换规则:

  • 整数按"亿-万-千-百-十-个"分组,正确插入"零"
  • 小数部分逐位读出(“三点一四一五九”)
  • 零的处理:连续零只读一个(“一千零五"而非"一千零零五”)

4.3 智能分块策略

文本分块是整个 TTS 管道中最关键的环节之一。策略如下:

最大块长度: 200 字符
最小块长度: 10 字符(太短的块合并到前一块)

分块优先级(从粗到细):

  1. 按段落分割:以换行符为界
  2. 按句子分割:以句号、问号、感叹号为界
  3. 超长句处理:在靠近边界区域寻找次级标点(逗号、分号等)切分
  4. 末尾标点清理:移除块末尾的次级标点(逗号、顿号等),避免合成时产生悬挂感

块间处理:

  • 正常块之间插入 0.1 秒静音,模拟自然停顿
  • 跳过的块(生成失败)留 0.3 秒静音,保持节奏
  • 章节标题作为独立的第一个块生成

4.4 音频质量检测

生成后不能直接使用,必须检测质量。实现了多层检测机制:

MP3 帧级检测
  • 静音检测:连续帧零字节比例 > 90% 且持续 ≥ 2.0 秒 → 判定静音
  • 重复帧检测:通过 CRC32 对比相邻帧 payload,连续相同 ≥ 1.2 秒 → 判定卡顿
WAV 波形级检测(更精确)
  • 静音检测:100ms 窗口 RMS < 50,持续 ≥ 2.0 秒
  • 重复音检测(stride-1):相邻窗口 Pearson 相关系数 > 0.90,持续 ≥ 1.2 秒
  • 重复音检测(stride-2):隔一个窗口的相关系数 > 0.90,持续 ≥ 1.2 秒(捕捉 ABAB 模式)
  • 滑动窗口均值:连续 12 个窗口平均相关系数 > 0.92 → 判定卡顿
  • 卡死音检测:RMS 变异系数(标准差/均值)< 0.05 持续 ≥ 1.2 秒 → 捕捉固定频率嗡鸣

4.5 重试与降级策略

当质量检测不通过时,不是简单重试,而是采用分级降级策略:

第1步: 直接重试(MAX_QUALITY_RETRIES = 1)
  ↓ 仍失败
第2步: 标点分割重试
  - 将问题块按标点切分为更短的子句
  - 太短的子句(< 10字符)合并到相邻子句
  - 逐个子句生成并检测
  ↓ 子句仍失败
第3步: 时间戳裁剪
  - 根据质量检测返回的 issueStartSeconds
  - 裁剪到问题点前(留 0.1 秒安全边距)
  - 保留已正常合成的部分
  ↓ 完全失败
第4步: 丢弃该块
  - 返回 null,在最终音频中留 0.3 秒静音占位
  - 保证整体流程不中断

标点分割重试的细节:

  • 使用正则 (?<=[。!?,、;:.!?,;:]) 在标点后断开
  • 合并过短的片段(< 10 字符)以避免上下文缺失
  • 每个子句独立生成、独立检测、独立裁剪

4.6 块间静音填充

  • 正常生成的块之间:0.1 秒静音(模拟自然呼吸停顿)
  • 失败/跳过的块:0.3 秒静音(避免内容跳跃感太强)
  • 通过 Mp3ProcessingService 生成静音 MP3 帧并拼接

5. 问题字符完整列表

以下是经过逐字测试确认会导致 Qwen3-TTS 产生卡顿/重复音频的字符完整列表:

语气词(最高风险)

啊 呀 哎 唉 哦 噢 喔 哟 呦 嗯 唔 呃 嗝 咦 嘿 嗨 哈 嘻 哼 嗤

发声/喷气词

噗 咳 呸 啐 唾 呼 吁 咝 嘶 嘘 咄 嚯 嗬 嚄 噫 嚱 唏 咿 呜 哇 哗

象声词/拟声词

嘟 咕 噜 咯 咔 咚 咣 当 叮 咛 铃 嗖 嗡 嗒 嘀 嘭 嘣 砰 啪 噼 嘎 嘠 嘞

动物叫声

喵 汪 咩 哞 嗷 啾 唧 喳 嘎 呱 嗥 嚎 咴

生理动作声

喘 嗳 嗽 喷 嚏 啜 嘬 咂 吧 叭 嗍 咬 嚼 咀 咽 呷 啖 哧

测试方法

项目中包含自动化测试工具 TtsRepeatDetectionTest,可对任意字符列表进行逐字测试:

mvn test -Dtest=TtsRepeatDetectionTest \
  -Dtts.baseUrl=http://localhost:8000 \
  -Dtts.apiKey=111111 \
  -Dtts.voice=serena \
  -Dtts.model=Qwen3-TTS-12Hz-1.7B-CustomVoice-bf16

测试会输出每个字符的通过/失败状态及具体问题描述,便于在更换模型版本后重新验证。


6. 最佳实践总结

接入前

  1. 不要直接拿原文送 TTS — 必须经过完整的文本预处理管道
  2. 测试你的字符集 — 使用自动化测试工具确认哪些字符在你的模型版本上有问题
  3. 设定合理的块大小 — 200 字符是经验值上限,宁短勿长

文本预处理

  1. 数字转换在最前面 — 先百分比后整数/小数
  2. 替换规则有序执行 — 控制字符→格式→标点→符号→空白→规范化
  3. 宁可过度清理也不要漏过 — 一个漏网的拟声词就能毁掉整段音频
  4. 维护问题字符黑名单 — 发现新的问题字符立即加入替换规则

生成策略

  1. 永远做质量检测 — 不要信任模型输出,每块都检测
  2. 多级降级而非暴力重试 — 重试同样的输入大概率还是失败,要改变输入
  3. 标点分割是最有效的降级手段 — 把长句切短几乎总能解决卡顿
  4. 允许部分失败 — 用静音占位比无限重试更实际
  5. 时间戳裁剪保留可用部分 — 不要因为后半段坏了就丢掉整块

音频后处理

  1. 块间加静音 — 没有间隔的拼接听起来像机关枪
  2. 统一音频格式 — 所有块用相同的采样率、比特率
  3. 监控生成耗时 — 异常长的生成时间通常意味着卡顿

运维建议

  1. 设置合理的超时 — 项目中用全局配置 ai_timeout_seconds(默认 300s)
  2. 记录失败日志 — 哪些文本触发了问题,便于持续优化规则
  3. 模板可配置 — 不同模型版本的问题字符可能不同,保持灵活性
  4. 定期重新测试 — 模型更新后原有的问题字符可能修复,也可能出现新的

附录:处理流程图

原始文本
  │
  ├─ 1. 百分比转中文 ("50%" → "百分之五十")
  ├─ 2. 数字转中文 ("123" → "一百二十三")
  ├─ 3. TTS卡音字符移除 (拟声词/语气词 → "")
  ├─ 4. 控制字符清理 (不可见字符 → ",")
  ├─ 5. Markdown格式移除
  ├─ 6. 标点处理 (引号移除, 破折号→逗号)
  ├─ 7. 符号清理 (装饰符号移除, 运算符→汉字)
  ├─ 8. 空白规范化 (空格→逗号, 合并连续逗号)
  ├─ 9. 文本规范化 (繁简修正, 连续标点修正)
  │
  ▼
清洗后文本
  │
  ├─ 按段落分割
  ├─ 按句子分割
  ├─ 超长句按次级标点切分
  ├─ 短块合并 (< 10字符)
  │
  ▼
文本块列表 (每块 ≤ 200字符)
  │
  ├─ 逐块调用 TTS API
  ├─ 质量检测 (静音/重复/卡死音)
  ├─ 失败? → 重试 → 标点分割 → 裁剪 → 丢弃
  ├─ 块间插入静音 (0.1s / 0.3s)
  │
  ▼
最终音频文件 (MP3)

更多推荐