文本转语音(TTS)技术如今已经相当成熟,但要让机器说出来的话听起来像真人,仅仅调用一个API是远远不够的。同一个引擎,不同的输入处理方式,最终效果可能天差地别。本文将从文本预处理、发音与韵律控制、合成参数调优到音频后处理,完整梳理TTS落地的最佳实践。

文本预处理:决定语音质量的第一道关口
TTS引擎接收的是纯文本,但真实业务中的文本往往混杂着数字、日期、货币、缩写、标点混乱等各种元素。如果不做归一化处理,引擎很容易读错。比如"2024年3月5日"可能被读得磕磕绊绊,"128.5元"可能被读得含糊不清。
文本归一化(Text Normalization)是预处理的核心。它包含几个层面:数字转换,把阿拉伯数字根据上下文转成合适的读法,"第1名"应读"第一名","10086"应按数字串逐位读;单位与符号处理,"%"读作"百分之","℃"读作"摄氏度";缩写展开,"etc."读作"等等","Dr."根据语境读"博士"或"医生"。归一化没有通用方案,必须结合业务语料统计常见模式,建立专属词典和规则库。
另一个容易被忽视的点是标点清洗。网络文本中常见连续感叹号、省略号滥用、表情符号混杂,这些会干扰引擎的韵律判断。建议在送入引擎前统一标点规范:将连续的问号叹号压缩为一个,删除emoji和特殊符号,把中文全角数字和英文半角符号统一。一个实用的做法是维护一个正则清洗管道:
import re
def normalize_text(text):
# 去除emoji与控制字符
text = re.sub(r'[\U0001F300-\U0001FAFF]', '', text)
# 压缩连续标点
text = re.sub(r'([!?。,])\1+', r'\1', text)
# 全角数字转半角
table = str.maketrans('0123456789', '0123456789')
return text.translate(table)分句与长度控制:长文本合成的稳定性关键
神经TTS模型对输入长度很敏感。文本过长时,注意力机制容易失稳,出现跳字、重复、语速异常等问题。实践中建议单次合成的文本控制在100到200字以内,超过就切分后分段合成,再拼接音频。
切分位置的选择直接影响听感。最佳切分点是句号、问号、叹号处,其次是分号和逗号。要避免在数字中间、专有名词内部、引语中间强行切断。切分后每段的首尾要做短暂静音处理(一般100到200毫秒),拼接处才不会出现气息突兀的感觉。对于有声读物这类超长文本,还应按段落级别做批处理,并记录每段的时长元数据,方便后续生成章节时间轴。
此外要注意语种的混合问题。中英混排文本中,如果引擎对英文支持弱,可以考虑将常见英文单词映射为音译中文,或者选用支持多语种的模型。混排文本的分句还应考虑英文句点的歧义,比如"3.5版本"里的点不能当作句子边界。
发音标注与韵律控制:用SSML精雕细琢
多音字是中文TTS的老大难问题。"银行"、"重量"、"重庆"这类词,引擎偶尔会读错音。最可靠的方案是维护一份业务专有词发音词典,并通过SSML强制指定读音:
<speak> 请前往<sub alias="chong qing">重庆</sub>办理业务。 <break time="300ms"/> 本次操作已<emphasis level="moderate">全部完成</emphasis>。 <say-as interpret-as="telephone">13800138000</say-as> </speak>
除了发音,韵律标记同样重要。<break>标签可以控制停顿时长,让长句有呼吸感;<prosody>标签能调整语速、音高和音量,适合区分叙述与强调部分;<say-as>标签可以指定数字、日期、电话号码的读法,从根源上解决归一化遗漏问题。
不过SSML不宜滥用。过度堆砌标签会让语音听起来一顿一顿,反而失真。建议的策略是:先用裸文本合成,人工或自动检测发音错误,再有针对性地加标注。对固定话术(如客服开场白)可以精修SSML,对动态生成的文本则依赖词典加规则自动标注。
参数调优与音频后处理
合成参数的选择要匹配场景。客服场景语速稍快(1.1倍左右)、音调平稳,显得干练;有声读物语速放慢(0.9倍),句间停顿加长,营造讲述感;播报通知类则适合默认语速加更强的句尾降调。采样率方面,电话通道用8kHz或16kHz即可,App和网页端建议至少24kHz。
合成完成后,后处理能进一步提升体验。常见的处理包括:音量归一化,让多段音频响度一致,目标响度可对齐-16 LUFS;静音修剪,去除首尾过长的空白;淡入淡出,在拼接点加10毫秒的交叉淡化消除爆音。如果用Python处理,pydub就能覆盖大部分需求:
from pydub import AudioSegment
from pydub.effects import normalize
def post_process(segments):
result = AudioSegment.empty()
for seg in segments:
audio = AudioSegment.from_file(seg)
audio = normalize(audio)
audio = audio[200:] # 去掉开头200ms静音
result += audio + AudioSegment.silent(duration=150)
result.export("output.wav", format="wav")最后,建立一套评估闭环不可或缺。用MOS评分做主观听感测试,用字错率(通过ASR回转文本对比原文)做客观指标,持续收集线上badcase反哺词典和规则。TTS优化是一个迭代过程,词典越攒越厚,规则越调越准,语音的自然度也会随之稳步提升。