MAI-Voice-2-Flash 负责出声、文本清洗留给业务侧

文章导读
合成音频里数字、英文缩写、符号读得不对劲时,先别急着改合成参数。MAI-Voice-2-Flash 这一层做的是「照着你给的字符出声」,模型读到什么就念什么;读法不对,多数情况在文本进模型之前就已经定了。较稳的分工是:模型侧只负责发声,业务侧负责把数字、单位、缩写、符号展开成不会产生歧义的读法,两边各自留一段可回看的记录。这样出问题时能直接落到某一侧,而不是在文本和参数之间来回试。
📋 目录
  1. A 把读错的那句原样存下来
  2. B 在业务侧做数字与单位的读法映射
  3. C 把缩写与符号按发音展开
  4. D 用单变量对照实验确认剩余问题在合成侧
  5. E 定一条定位顺序:先文本后参数
A A

合成音频里数字、英文缩写、符号读得不对劲时,先别急着改合成参数。MAI-Voice-2-Flash 这一层做的是「照着你给的字符出声」,模型读到什么就念什么;读法不对,多数情况在文本进模型之前就已经定了。较稳的分工是:模型侧只负责发声,业务侧负责把数字、单位、缩写、符号展开成不会产生歧义的读法,两边各自留一段可回看的记录。这样出问题时能直接落到某一侧,而不是在文本和参数之间来回试。

判断方向:文本清洗留在业务侧,合成侧只调音色、语速这类发声参数。适用场景是播报、客服提示、内容配音等对数字和缩写读法有固定要求的文本。操作上先存下出错原句的最小样本,再按数字、单位、缩写、符号分规则展开,最后用单变量对照确认剩余问题归属。验证方式是同一句原文与清洗后文本分别重跑、对比音频。风险边界:编号、型号、版本号等不该被展开的串要显式排除,否则会读出更离谱的结果。

把读错的那句原样存下来

排查第一步不是改代码,是让错误可复现。从日志里把触发问题的那一句单独摘出来,记录三个字段:用户看到的原文、真正送进合成的清洗后文本、以及这次合成产出的音频文件路径。三列缺一列,后面就没法判断问题出在哪一侧。

样本ID原文清洗后文本音频路径参数快照
num-001订单20240315已发货,共3件订单二零二四年三月十五日已发货,共三件out/num-001.wavvoice=default, speed=1.0
abbr-002APP 内 VIP 权益说明A P P 内 V I P 权益说明out/abbr-002.wavvoice=default, speed=1.0
sym-003温度 25℃~30℃温度 二十五摄氏度 到 三十摄氏度out/sym-003.wavvoice=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五千克单位符号替换为中文单位

对应的匹配可以先写窄一点,跑通再逐步放宽:

MAI-Voice-2-Flash 负责出声、文本清洗留给业务侧
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'),                  # 版本号
]

排除条件建议写成配置而不是散落在代码里,业务新增一类编号时改配置即可,不需要重新走一遍回归。

把缩写与符号按发音展开

英文缩写交给合成侧去猜,结果往往不稳定,同一个词在不同上下文可能读成字母、也可能读成单词。业务侧维护一份缩写白名单,明确哪些按字母逐个读、哪些按单词读、哪些转成中文,比反复调参数有效。

原文展开写法说明
AIA I逐字母,中间留空格
VIPV I P逐字母
USD美元按语义转中文
kg / km千克 / 公里单位转中文
No.编号去掉缩写形式

符号同理,先做一张替换对照表,让进入合成的文本里尽量不留裸符号:

符号替换为备注
%百分之前置到数值前
/或、斜杠按语境二选一
~约、到数量用「到」,估计用「约」
℃摄氏度后置到数值后
@at地址场景可读作「艾特」
#号门牌、编号场景

替换前后的差异要留一条对照,方便回看规则是否覆盖到位:

原文:     A/B 测试覆盖率 ~80%,机房温度 25℃
替换后:   A 或 B 测试覆盖率 约百分之八十,机房温度 二十五摄氏度

替换后文本仍然要能被人读懂,否则日后回查日志会对不上原文,建议原文与替换后文本成对存储。

MAI-Voice-2-Flash 负责出声、文本清洗留给业务侧

用单变量对照实验确认剩余问题在合成侧

文本侧规则跑过一遍还剩下问题,就进入对照实验。做法是锁死同一段清洗后文本,每轮只动一个合成参数,其余保持不变,逐轮听差异并记录。

轮次文本变动的参数观察点
基线固定文本无记录出错位置与时点
A同一文本只改语速错读是否仍出现在同一位置
B同一文本只改音色同上
C同一文本只改停顿/分段同上,关注跨句连读

判定标准可以先用这两条:文本侧问题的特征是错误位置固定、可被文本改写消除,改参数不改变错读的词;合成侧参数问题的特征是同一段文本在改动某个参数后,错读的时点、连读或重音表现随之变化。两边都列不出稳定规律时,说明样本还不够小,回到第一步继续切句。

定一条定位顺序:先文本后参数

把排查动作固定成顺序,能省掉大量随机试错。推荐按下面五步走,每一步都有明确的判定依据:

  1. 取原文与清洗后文本对照,看清洗后文本本身是否已经把数字、缩写展开成预期读法。依据:人读到这串文字会怎么念。
  2. 绕过业务链路,直接把清洗后文本送进合成,听是否仍错。依据:错读是否存在,判断落到文本写法还是模型读法。
  3. 若仍错,换一种等价写法再试一次,例如中文数字与阿拉伯数字互换、字母间加空格。依据:错读是否被写法改变。
  4. 若写法无影响,进入单变量参数对照。依据:错读是否随参数变化。
  5. 确认归属后,把结论写回规则表或参数配置,并补一条样本进回归集。依据:同一句下次能自动判定。

需要留档的证据项建议固定为:原文、清洗后文本、清洗规则版本标识、合成参数快照、音频文件路径、生成时间、模型名称与版本。这些字段凑齐,同一个问题下次复现基本不需要重新猜。

边界上还要说一句:业务侧清洗不是越彻底越好。把每串数字都展开成中文、每个英文词都转拼音,文本会变得难维护,也容易在更新规则时引入新的误伤。通常的做法是只覆盖业务里高频、读法有明确约定的那几类,其余保留原样并记入样本库,下一轮再决定要不要加规则。