合成音频里数字、英文缩写、符号读得不对劲时,先别急着改合成参数。MAI-Voice-2-Flash 这一层做的是「照着你给的字符出声」,模型读到什么就念什么;读法不对,多数情况在文本进模型之前就已经定了。较稳的分工是:模型侧只负责发声,业务侧负责把数字、单位、缩写、符号展开成不会产生歧义的读法,两边各自留一段可回看的记录。这样出问题时能直接落到某一侧,而不是在文本和参数之间来回试。
判断方向:文本清洗留在业务侧,合成侧只调音色、语速这类发声参数。适用场景是播报、客服提示、内容配音等对数字和缩写读法有固定要求的文本。操作上先存下出错原句的最小样本,再按数字、单位、缩写、符号分规则展开,最后用单变量对照确认剩余问题归属。验证方式是同一句原文与清洗后文本分别重跑、对比音频。风险边界:编号、型号、版本号等不该被展开的串要显式排除,否则会读出更离谱的结果。
把读错的那句原样存下来
排查第一步不是改代码,是让错误可复现。从日志里把触发问题的那一句单独摘出来,记录三个字段:用户看到的原文、真正送进合成的清洗后文本、以及这次合成产出的音频文件路径。三列缺一列,后面就没法判断问题出在哪一侧。
| 样本ID | 原文 | 清洗后文本 | 音频路径 | 参数快照 |
|---|---|---|---|---|
| num-001 | 订单20240315已发货,共3件 | 订单二零二四年三月十五日已发货,共三件 | out/num-001.wav | voice=default, speed=1.0 |
| abbr-002 | APP 内 VIP 权益说明 | A P P 内 V I P 权益说明 | out/abbr-002.wav | voice=default, speed=1.0 |
| sym-003 | 温度 25℃~30℃ | 温度 二十五摄氏度 到 三十摄氏度 | out/sym-003.wav | voice=default, speed=1.0 |
重跑同一句时,建议把合成参数固定成一份快照一起存档,否则第二次跑出来的差异说不清是文本改了还是参数变了。调用侧可以用一个最小脚本,只接收清洗后文本和参数文件:
# 最小重跑骨架,具体方法名以实际接入的客户端为准
python synth_once.py \
`--text-file` samples/num-001.txt \
`--params` config/params-voice-default.json \
`--out` out/num-001-rerun.wav
每次重跑都用新的文件名,不要覆盖原音频;对比时听的是两份文件,不是凭记忆。
在业务侧做数字与单位的读法映射
数字是最容易出问题的一类,因为同一串数字在不同语境下读法不同:年份通常逐位读,金额按数值读,编号则根本不该展开。先把业务里出现过的数字形态列成一张规则表,再写替换逻辑,比直接上一条大正则更可控。
| 类型 | 触发形态 | 目标读法 | 处理方式 |
|---|---|---|---|
| 年份 | 1998、2024 | 一九九八、二零二四 | 逐位展开为中文数字 |
| 小数 | 3.5 | 三点五 | 小数点替换为「点」 |
| 百分比 | 12% | 百分之十二 | 前置「百分之」并去符号 |
| 金额 | ¥1200 | 一千二百元 | 数值转中文并补单位 |
| 区间 | 3-5 天 | 三到五天 | 连字符替换为「到」 |
| 计量 | 5kg | 五千克 | 单位符号替换为中文单位 |
对应的匹配可以先写窄一点,跑通再逐步放宽:
import re
PCT = re.compile(r'(\d+(?:\.\d+)?)\s*%')
DEC = re.compile(r'(?<!\d)(\d+)\.(\d+)(?!\d)')
YEAR = re.compile(r'(?<![\dA-Za-z-])(19|20)(\d{2})(?![\dA-Za-z-])')
易误伤的场景要提前列出来并显式排除:订单号、SKU、设备型号、批次号、版本号、电话号码、时间戳。这些串里的数字一旦被逐位或按数值展开,读出来比原来更糟。常见的做法是先用占位符把它们保护起来,替换完再还原:
PROTECT = [
re.compile(r'[A-Z]{1,6}[-_]?\d{2,}[A-Za-z0-9-]*'), # 型号/编号
re.compile(r'\b\d{11}\b'), # 手机号
re.compile(r'\b\d+\.\d+\.\d+\b'), # 版本号
]
排除条件建议写成配置而不是散落在代码里,业务新增一类编号时改配置即可,不需要重新走一遍回归。
把缩写与符号按发音展开
英文缩写交给合成侧去猜,结果往往不稳定,同一个词在不同上下文可能读成字母、也可能读成单词。业务侧维护一份缩写白名单,明确哪些按字母逐个读、哪些按单词读、哪些转成中文,比反复调参数有效。
| 原文 | 展开写法 | 说明 |
|---|---|---|
| AI | A I | 逐字母,中间留空格 |
| VIP | V I P | 逐字母 |
| USD | 美元 | 按语义转中文 |
| kg / km | 千克 / 公里 | 单位转中文 |
| No. | 编号 | 去掉缩写形式 |
符号同理,先做一张替换对照表,让进入合成的文本里尽量不留裸符号:
| 符号 | 替换为 | 备注 |
|---|---|---|
| % | 百分之 | 前置到数值前 |
| / | 或、斜杠 | 按语境二选一 |
| ~ | 约、到 | 数量用「到」,估计用「约」 |
| ℃ | 摄氏度 | 后置到数值后 |
| @ | at | 地址场景可读作「艾特」 |
| # | 号 | 门牌、编号场景 |
替换前后的差异要留一条对照,方便回看规则是否覆盖到位:
原文: A/B 测试覆盖率 ~80%,机房温度 25℃
替换后: A 或 B 测试覆盖率 约百分之八十,机房温度 二十五摄氏度
替换后文本仍然要能被人读懂,否则日后回查日志会对不上原文,建议原文与替换后文本成对存储。
用单变量对照实验确认剩余问题在合成侧
文本侧规则跑过一遍还剩下问题,就进入对照实验。做法是锁死同一段清洗后文本,每轮只动一个合成参数,其余保持不变,逐轮听差异并记录。
| 轮次 | 文本 | 变动的参数 | 观察点 |
|---|---|---|---|
| 基线 | 固定文本 | 无 | 记录出错位置与时点 |
| A | 同一文本 | 只改语速 | 错读是否仍出现在同一位置 |
| B | 同一文本 | 只改音色 | 同上 |
| C | 同一文本 | 只改停顿/分段 | 同上,关注跨句连读 |
判定标准可以先用这两条:文本侧问题的特征是错误位置固定、可被文本改写消除,改参数不改变错读的词;合成侧参数问题的特征是同一段文本在改动某个参数后,错读的时点、连读或重音表现随之变化。两边都列不出稳定规律时,说明样本还不够小,回到第一步继续切句。
定一条定位顺序:先文本后参数
把排查动作固定成顺序,能省掉大量随机试错。推荐按下面五步走,每一步都有明确的判定依据:
- 取原文与清洗后文本对照,看清洗后文本本身是否已经把数字、缩写展开成预期读法。依据:人读到这串文字会怎么念。
- 绕过业务链路,直接把清洗后文本送进合成,听是否仍错。依据:错读是否存在,判断落到文本写法还是模型读法。
- 若仍错,换一种等价写法再试一次,例如中文数字与阿拉伯数字互换、字母间加空格。依据:错读是否被写法改变。
- 若写法无影响,进入单变量参数对照。依据:错读是否随参数变化。
- 确认归属后,把结论写回规则表或参数配置,并补一条样本进回归集。依据:同一句下次能自动判定。
需要留档的证据项建议固定为:原文、清洗后文本、清洗规则版本标识、合成参数快照、音频文件路径、生成时间、模型名称与版本。这些字段凑齐,同一个问题下次复现基本不需要重新猜。
边界上还要说一句:业务侧清洗不是越彻底越好。把每串数字都展开成中文、每个英文词都转拼音,文本会变得难维护,也容易在更新规则时引入新的误伤。通常的做法是只覆盖业务里高频、读法有明确约定的那几类,其余保留原样并记入样本库,下一轮再决定要不要加规则。