dots.tts 合成人声偏机械,先核对文本标点与韵律停顿

文章导读
dots.tts 合成出来语气平、断句怪,通常先核对输入文本的标点与韵律停顿,而不是先换模型或换音色。机械感常见的来源是:整段没有逗号句号,模型只能按固定长度切分;数字和英文缩写被逐字念或替换成不合适的读法;标点前后没有留出可被模型识别的停顿。可以先固定采样率、音色、语速等参数,只改文本做对照合成,看差异是否跟着标点变化。
📋 目录
  1. Ⅰ 把同一段文本改成三种标点版本再合成
  2. Ⅱ 检查数字、英文和缩写的读法入口
  3. Ⅲ 在合成结果里标记停顿异常点
  4. Ⅳ 整理一份可复用的文本预处理清单
A A

dots.tts 合成出来语气平、断句怪,通常先核对输入文本的标点与韵律停顿,而不是先换模型或换音色。机械感常见的来源是:整段没有逗号句号,模型只能按固定长度切分;数字和英文缩写被逐字念或替换成不合适的读法;标点前后没有留出可被模型识别的停顿。可以先固定采样率、音色、语速等参数,只改文本做对照合成,看差异是否跟着标点变化。

判断方向:只改标点后语气和断句若明显变化,机械感主要来自文本预处理,而不是声学模型;若三版都平,再查文本规范化开关和韵律参数。操作上先做同参数三版本对照,再检查数字、英文、缩写读法,最后把听感异常点回落到字符位置。边界:不同版本对数字和标点的处理策略可能不同,最终以本机输出音频和日志为准,不要只凭描述判断。

把同一段文本改成三种标点版本再合成

确认机械感是否来自标点与断句,最直接的做法是拿同一段文字做三个版本:原句、补逗号句号、拆成短句。对比时只改文本,其他参数不变,否则分不清是标点起作用还是语速、采样步数在起作用。

  • 原句:今天天气不错我们下午三点开会记得带上电脑和充电器
  • 补逗号句号:今天天气不错,我们下午三点开会,记得带上电脑和充电器。
  • 拆成短句:今天天气不错。我们下午三点开会。记得带上电脑和充电器。

把这三行分别送入同一合成配置,输出三个音频文件,命名可以带版本号。下面是一个通用接入骨架,tts.synthesize 需要按你本机 dots.tts 的实际入口替换;重点是除了 text,其余字段保持一致。

import soundfile as sf
# 假设本地已有 dots.tts 的推理入口,按项目实际 API 替换
texts = {
    'v0_raw': '今天天气不错我们下午三点开会记得带上电脑和充电器',
    'v1_punct': '今天天气不错,我们下午三点开会,记得带上电脑和充电器。',
    'v2_split': '今天天气不错。我们下午三点开会。记得带上电脑和充电器。',
}
for name, text in texts.items():
    # 固定 speaker / speed / seed / sample_rate 等参数
    wav = tts.synthesize(text=text, speaker='default', speed=1.0, seed=42)
    sf.write(f'{name}.wav', wav, 24000)

验证方式是听三个文件的停顿位置:v1 和 v2 如果在“今天天气不错”之后以及“开会”之前出现自然停顿,说明机械感主要来自标点缺失;如果三个版本听感都很平,再去看文本规范化开关和韵律相关参数。注意不要把标点和语速一起改,否则无法归因。

检查数字、英文和缩写的读法入口

数字、年份、英文缩写被念成串音或怪读,通常出在文本规范化这一层:模型看到的不是“2024 年”,而是被切开的字符或被错误替换的读法。先在本机配置或推理入口里找 normalize、text_normalization、number_reading 这类字段,确认它们是否生效;找不到入口时,就在送入模型前用脚本替换。

  • 2024年:可能读成“两千零二十四”或逐位读,若要按年份读,可改成“二零二四年”。
  • 3.5:可能读成“三逗五”,可改成“三点五”。
  • 10kg:可能读成“十 k g”,可改成“十千克”。
  • CPU:要读字母时写成“C P U”并保留空格,要中文语义时改成“处理器”。
  • API:可改成“接口”或“A P I”。
  • 15%:可改成“百分之十五”。
  • v1.2.3:可改成“v 一点二点三”或“版本一点二点三”。

从输出音频定位问题位置,可以用播放器听怪读出现在第几秒,再把原文按标点或字符区间切开,单独合成出问题的那一小段。例如一段话第 12 个字符开始是“2024”,听到怪读也大致落在该段开头,就回到原文替换那一处,再合成同一片段做前后对比。

dots.tts 合成人声偏机械,先核对文本标点与韵律停顿
t = '2024年3.5版本CPU占用降到15%'
t = t.replace('2024年', '二零二四年')
t = t.replace('3.5', '三点五')
t = t.replace('CPU', '处理器')
t = t.replace('15%', '百分之十五')
print(t)
# 二零二四年三点五版本处理器占用降到百分之十五

这类替换是保守做法,是否适合要看模型对中文数字的发音是否稳定;如果替换后读法反而变怪,就换另一种写法,不要一次改多个词。

在合成结果里标记停顿异常点

听感问题要转成可复现的文本位置,才方便二次合成。播放合成音频时,遇到连读、停顿过长、停顿过短、句尾语调异常,就暂停并记下播放器时间戳,例如 00:03.4。然后按标点把原文切成片段,建立序号和字符区间,判断时间戳落在哪个片段。

  1. 用播放器或音频编辑器播放合成 wav,暂停在异常处,记录秒数。
  2. 把原文按逗号、句号、问号、叹号、分号切成片段,编号。
  3. 把时间戳对齐到片段序号,例如第 2 句“我们下午三点开会”内部没有停顿。
  4. 只重新合成该片段,补标点或拆成两句,再听是否改善。
  5. 如果问题在片段中间,就把该片段从中间二分,继续缩小到具体文字位置。
import re
text = '今天天气不错,我们下午三点开会,记得带上电脑和充电器。'
parts = [p for p in re.split(r'[,。!?;]', text) if p]
for i, p in enumerate(parts, 1):
    print(i, p, '字符数', len(p))

记录时建议写成小表:时间戳、对应片段、听感描述、修改后的文本、二次合成文件名。只改这个片段对应的文本,其余参数保持和第一次合成一致。不要因为一次停顿异常就调全局语速或换音色,先确认文本位置。

整理一份可复用的文本预处理清单

批量合成前先按固定顺序过滤文本,可以减少反复试听。检查顺序建议从标点开始,再到数字、英文、特殊符号、空格,最后处理表情符号和标记。每发现一个读法问题,就往清单里追加一条,而不是每次靠记忆。

  1. 标点:句末是否有句号、问号或叹号;长句是否缺逗号;中英文标点是否混用。修改原则是统一用全角中文标点,长句按语义切成便于停顿的片段。
  2. 数字:年份、小数、百分比、单位、电话号码、编号。修改原则是模型读法不确定时先改成汉字读法;电话和编号按逐位读,用空格或汉字分隔。
  3. 英文和缩写:CPU、API、URL、HTTP 等。修改原则是先确定要读字母还是读单词;读字母时用空格隔开,要中文语义时改成中文词。
  4. 特殊符号:&、/、-、_、@、#、*。修改原则是先看模型是否支持;不支持就改写为中文,例如“/”改成“或”,“-”改成“到”或“杠”。
  5. 空格:中英文之间、数字和单位之间的多余空格、连续空格、Tab、换行。修改原则是合成前合并连续空白,避免把空白当成额外停顿。
  6. 其他:表情符号、颜文字、Markdown 标记、网址。修改原则是合成前删除或转写成可读中文。
def preprocess(text: str) -> str:
    text = text.replace('\\u3000', '').replace('\\t', ' ')
    text = text.replace(',', ',').replace('?', '?').replace('!', '!')
    text = re.sub(r'\\s+', ' ', text).strip()
    # 数字和英文缩写按项目读法替换
    return text

这份清单不是一次写死。可以先拿最近十条合成文本跑一遍,把发现的数字、缩写、符号读法补进去,再用于后续批量合成。需要结合环境确认的是:dots.tts 本机版本是否自带文本规范化,以及它和外部替换脚本谁先执行。