把一份技术文档直接丢给 TTS,最容易翻车的往往不是长句,而是数字、单位符号和英文缩写:一句「重试间隔 0.25 秒,最多重试 3 次」里同时出现了小数读法、单位连读和量词搭配。处理顺序建议反过来——先从真实文档里抽句子做试听,把「听着别扭」拆成具体类型,再判断这句话该交给文本改写,还是交给语速、停顿这类合成参数。文本改写成本低、可复用;参数只调整节奏,改不了「3.5」被读成「三点五」还是「三点十五」这种文本层歧义。
数字与缩略语的读法问题,大多能在文本层解决:把 0.25 写成「零点二五」,把 % 写成「百分之」,把容易被整词吞掉的缩略语按字母分隔或展开,再送合成。只有停顿过长、语速偏快、句末语调这类节奏问题才值得动合成参数。判断依据是同一句只改一个变量重复试听;边界是发音词典与接口字段并不通用,不同版本参数名不一致,必须以你实际接入版本的返回和播放效果为准。
从线上真实文档里抽出 20 条测试文本
测试集不要凭空造句,直接从你手上会真正送进 TTS 的文档里摘:配置说明、API 参考、运维手册、发布公告、脚本注释。摘录时做脱敏(域名、内网地址、人名换成占位符),但保留原始写法——千分位、全角括号、单位大小写、函数名里的下划线,这些细节恰恰是读错的来源。下面这 20 条覆盖了常见写法,可以直接作为你第一轮试听的底稿。
| 序号 | 测试文本(脱敏) | 取自文档的哪类位置 | 关注点 |
|---|---|---|---|
| 1 | 单节点最多承载 20000 个长连接 | 容量说明段 | 五位整数读成「两万」还是逐位念 |
| 2 | 超时阈值设为 3.5 秒 | 参数表 | 一位小数与量词的连读 |
| 3 | 重试间隔 0.25 秒,最多重试 3 次 | 故障处理步骤 | 两位小数是否读成「零点二十五」 |
| 4 | 日志默认保留 180 天 | 存储策略段 | 三位整数的自然读法 |
| 5 | 请升级到 v2.10.3 及以上 | 升级说明 | 点分版本号被当小数读 |
| 6 | 单文件上限 512 MB,批量上传上限 20 GB | 限制说明 | 单位字母逐字母念还是整词 |
| 7 | 响应时间 P99 约 120 ms | 指标监控段 | P99 这种字母+数字混排 |
| 8 | 内存峰值 1.2 GiB | 资源评估段 | GiB 与 GB 的区分 |
| 9 | CPU 使用率超过 75% 触发限流 | 告警规则 | 百分号紧贴数字 |
| 10 | 机房温度不超过 35°C | 运维规范 | 度符号是否被吞掉 |
| 11 | 带宽 10 Gbps,抖动小于 5 ms | 网络指标 | 多字母单位的连读 |
| 12 | 通过 HTTPS 上传,证书使用 TLS 1.3 | 安全说明 | 缩写逐字母念的顺序 |
| 13 | 接口默认开启 QPS 限流 | 接口约束 | QPS 被当单词拼读 |
| 14 | 该能力依赖 SDK 中的 RAM 角色授权 | 权限配置 | SDK、RAM 两个缩写连用 |
| 15 | 使用 CRUD 接口批量写入 | 开发指南 | 可发音缩写的读法 |
| 16 | 日志出现 OOM 时先看 RSS 曲线 | 排障手册 | OOM 与 RSS 的区分度 |
| 17 | 默认值 30(单位:秒,范围 1-3600) | 参数说明 | 全角括号与连接号的停顿 |
| 18 | 需开通白名单(联系运维同学,工单类型选网络策略) | 前置条件 | 括号内长句的断句 |
| 19 | 调用 open_stream() 时传入 chunk_size 参数 | 代码示例说明 | 下划线、括号、驼峰 |
| 20 | 返回 AsyncIterator 对象,需在 finally 中释放 | API 参考 | 中英混排与关键字 finally |
摘的时候顺手记一下每句来自哪一段。后续修规则时,你能判断某条规则是为「参数表」服务的还是为「排障步骤」服务的,不至于一刀切改写所有文本。
逐条试听并按错误类型归类记录
试听时不要只写「不好听」,要落到可改动的维度上:数字读法、缩略语处理方式、多音字、标点处停顿。下面给出分类维度与常见现象的对应关系,结果栏请边听边填,以你实际听到的为准——不同音色、不同参数下表现会有差异。
| 类型 | 判断标准 | 常见现象 | 优先处理方向 |
|---|---|---|---|
| 数字读法 | 小数、版本号、长整数 | 0.25 读成「零点二十五」;v2.10.3 读成「二点十点三」 | 文本改写 |
| 缩略语 | 逐字母、整词、还是展开 | QPS 被拼成一个生造词;TLS 逐字母吞掉末尾 | 文本改写或发音词典 |
| 多音字 | 重、调、载、行、率、数 | 重试读成 zhòng;调用读成 tiáo | 换词或加注音词典 |
| 标点停顿 | 括号、顿号、连接号、斜杠 | 括号内长句一口气念完;1-3600 停顿突兀 | 标点替换,必要时调停顿参数 |
记录格式建议固定成四列,便于横向比对:
序号 | 原文 | 实际听到的关键片段 | 归类
3 | 0.25 秒 | 听到『零点二十五秒』 | 数字读法
13 | QPS 限流 | 听到一个整词,像『qups』 | 缩略语
2 | 重试间隔 | 听到『zhòng 试』 | 多音字
17 | 1-3600 | 连接号处无停顿,数字连成一串 | 标点停顿
归类完成后再回头看:落在「数字读法」和「缩略语」上的问题,基本都靠改文本;落在「标点停顿」且改写后仍然别扭的,才需要动参数。「多音字」介于两者之间——优先换词(把「重试」写成「再次尝试」),换不掉再考虑发音词典。
一次只改一个变量做对照试听
同一句文本合成三遍:A 原文 + 默认参数,B 改写文本 + 默认参数,C 原文 + 调整参数。三遍用同一个音色、同一段文本、同一套输出格式,只动一个变量,差异才有意义。如果三遍同时改,你无法判断好转来自哪一步。
# 通用请求骨架,字段名请按你接入的版本核对
{
'model': '<tts_model_name>',
'input': '重试间隔 0.25 秒,最多重试 3 次',
'voice': '<voice_id>',
'format': 'wav',
'speed': 1.0
}
# B 组只改 input
'input': '重试间隔零点二五秒,最多重试三次。'
# C 组只改参数,文本回到原文
'speed': 0.9 # 或按版本支持的停顿/韵律字段调整
对照结论按下面这种方式记,不要写成「效果不错」:
- A 组:小数读法错误,句子节奏正常 → 问题在文本层。
- B 组:小数读法正确,但数字与量词之间听起来黏连 → 需要补停顿或加量词。
- C 组:文本未改,只放慢语速,小数读法依然错误 → 说明参数不解决读法问题。
一个常见误区是把语速调慢当成万能药。语速慢下来只会让错误读法听得更清楚,不会把「三点十五」变成「三点一五」。凡是 A 组就听出错读的句子,先改写再考虑参数。
把有效改法固化成预处理规则清单
试听结论要落成规则,否则每接一份新文档都要重听一遍。规则写在送合成之前,输入文本、输出文本一对一可 diff,误伤才查得出来。下面这份清单按保守方向写,按你实际文档的写法增减。
| 规则 | 处理方式 | 对照例句 |
|---|---|---|
| 万级以上整数 | 改写成「两万」「十二万」这类数量词,或补量词 | 20000 个 → 两万个 |
| 小数 | 阿拉伯数字保留,但两位以上小数写成中文数字,消除歧义 | 0.25 秒 → 零点二五秒 |
| 版本号 | 用「点」显式分隔,或写成「版本二,一零,三」逐段念 | v2.10.3 → 版本 2 点 10 点 3 |
| 百分号、度符号 | 替换成中文词,数字与词之间留空格 | 75% → 百分之 75;35°C → 35 摄氏度 |
| 常用单位 | 保留字母,确认数字与单位之间有空格;易混的写全称 | 120ms → 120 毫秒;1.2 GiB → 1.2 吉字节 |
| 范围连接号 | 替换成「到」,两侧留空格 | 1-3600 → 1 到 3600 |
| 斜杠 | 按语义换成「每」或「或」,不要留着让模型猜 | 次/秒 → 次每秒 |
缩略语要不要展开,给一个可操作的判断条件:如果试听时它被读成一个生造词,或逐字母念吞掉了末尾字母,并且你的接入方式没有可用的自定义发音词典,就展开或按字母加分隔;如果缩写本身是可发音的常见词(如 CRUD),且试听时整词读法正确,就保留不展开。展开会拉长朗读时间,长文档里不必全量展开,只处理高频出现的那几个。
需要删除或替换的符号,建议单独维护一张表:` 和 ** 这类 Markdown 标记直接去掉;竖线 | 换成逗号;下划线在函数名中改成空格或按语义拆词,例如 open_stream 写成 open stream;全角括号保留但括号内的长句单独断句;emoji、制表符、连续空格统一清理。占位符 <xxx> 在朗读版里替换成可读名称,不要原样念出尖括号。
import re
REPLACE = [
(r'~', '约'),
(r'(\d)\s*[–—-]\s*(\d)', r'\1 到 \2'),
(r'(\d)\s*/\s*([^\s,,。]+)', r'\1 每 \2'),
(r'(\d)\s*%', r'百分之 \1'),
(r'(\d+(?:\.\d+)?)\s*°C', r'\1 摄氏度'),
(r'(\d+(?:\.\d+)?)\s*ms\b', r'\1 毫秒'),
(r'[`*|]', ''),
(r'_(?!\w)', ' '),
]
def preprocess(text: str) -> str:
for pat, rep in REPLACE:
text = re.sub(pat, rep, text)
return re.sub(r'[ \t]{2,}', ' ', text).strip()
# 上线前先跑一遍差异检查,确认没有改掉原意
if __name__ == '__main__':
samples = ['单文件上限 512 MB,批量上传上限 20 GB', '范围 1-3600']
for s in samples:
print(preprocess(s))
规则上线后仍要留一段人工核对:随机抽若干句比较改写前后,重点看两件事——数字有没有被改错量级,术语有没有因为拆词而变味。这套规则的价值不在一次到位,而在于让后续接入方按同一份清单处理文本,试听工作量从每份文档重来,变成只在新增写法时补一条规则。