先给判断方向:YoVoice 合成出现漏句、串句或停顿怪异,多数情况不是音色“变差”,而是文本在进入合成引擎之前就被改过——要么是分句/断句规则把一句话切碎或合并,要么是当前语音包与语言码对数字、标点、中英混排、特殊字符的处理和你预期不一致。二分排查的做法是固定其他变量:先把送进引擎的最终文本落盘确认没被改字,再用同一段文本切换分句方式合成一次,最后换语音包再合成一次,看错位位置跟着哪个变量移动。
可摘录判断:文本副本确认预处理没改字;同一文本换分句方式后错位位置若跟着断句点移动,问题在分句规则;若错位位置固定不变、换语音包后仍复现,则倾向语音包、语言码或文本编码侧。每一步都要留下输入文本、实际生效参数和输出音频文件,结论才立得住;具体字段名与日志格式需要结合你的接入环境确认。
保存送进合成的最终文本副本
排查先从“文本有没有被改”开始。页面上看到的文本、程序里传入的字符串、分句之后交给引擎的句子列表,往往是三份不同的内容:前端可能做了全角半角转换,后端可能去掉了表情或做了繁简转换,分句模块又可能合并了短句。建议在调用合成前把这几份内容分别写文件,带上任务 ID,后面每步对照都靠这些副本。
# 通用接入骨架,字段名以实际接入为准
payload = {
"text": raw_text,
"voice": voice_id,
"lang": lang_code,
"split": split_mode,
}
sentences = split_text(raw_text, mode=split_mode) # 你本地或引擎侧的分句实现
with open(f"debug/{task_id}_raw.txt", "w", encoding="utf-8") as f:
f.write(raw_text)
with open(f"debug/{task_id}_sent.txt", "w", encoding="utf-8") as f:
f.write("\n---分句---\n".join(sentences))
with open(f"debug/{task_id}_body.json", "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
需要检查的落点有四个:页面输入框原文、程序内的字符串变量、分句后的句子列表、实际发出的报文体。逐字比对 raw 与页面原文,能排除清洗规则;比对 sent 与 raw,能看到分句模块是否吞掉了空白、引号内的标点或整段短句。这一步做完再听音频,问题就已经被切掉一半范围。
同一文本换不同分句方式各合成一次
固定语音包和语言码,只改分句方式,观察错位位置是否跟着断句点走。常见可切换的维度:整段一次送入由引擎自行断句、按句末标点切、按标点加最大字数切、人工在指定位置切分。若第一版漏掉的那半句,在第二种切法下被完整念出,说明之前那句话被切在了不合适的位置;若换了切法错位仍在原字符附近,分句规则的嫌疑就下降。
建议每一步记录断句的字符偏移,而不是只写“按句号切”:
| 轮次 | 分句模式 | 断句位置(字符偏移) | 输出时长 | 错位表现 |
|---|---|---|---|---|
| R1 | 整段送入 | 引擎自定 | 记录实际值 | 如:第 3 句只念了前半句 |
| R2 | 按句末标点 | 列出切开的位置 | 记录实际值 | 错位是否移动、是否消失 |
| R3 | 标点 + 最大字数 | 列出切开的位置 | 记录实际值 | 停顿是否变正常 |
对照重点是“错位位置”和“断句位置”的相对关系,而不是音频时长本身;时长只用来辅助判断是否真的漏读了内容。
换语音包重复同一文本
第三轮只换语音包,文本和分句方式保持不变。切换方式通常有两类:调用侧改 voice 或 speaker 参数,页面侧在音色下拉里换一个再合成。若你的接入支持指定语言码,建议顺带把语言码对同一文本各试一次,尤其是中英混排、含数字和单位、含书名号与省略号的句子——这类文本在不同语音包上的处理差异最容易暴露。
payload["voice"] = "voice_b" # 换成另一个语音包
payload["lang"] = "zh-CN" # 语言码按需对照,取值以实际支持为准
两次输出的差异建议从三个角度看:把两段音频分别做时长统计、做静音段检测、再人耳定位错位句的位置。若只有某一个语音包出现漏读或怪停顿,另一个正常,问题基本落在语音包或语言码侧;若两个语音包都在同一句出错,回到分句与文本副本继续查。
ffprobe -v error -show_entries format=duration -of default=nw=1 out_a.wav
ffmpeg -i out_a.wav -af silencedetect=noise=-35dB:d=0.3 -f null - 2>&1 | grep silence
在日志中定位一次合成任务的输入与输出
前面的对照都依赖“实际生效的参数”,而不是你以为传进去的参数。配置项被默认值覆盖、页面缓存了上一次的音色、环境变量里的语言码没生效,都会让对照失真。抓一次完整任务日志,用任务 ID 串起来看。
grep -n "task_id=abc123" app.log
grep -n "task_id=abc123" app.log | grep -Ei "text_len|split|voice|lang|synth_start|synth_end|error"
awk "/task_id=abc123/{print}" app.log | tail -50
需要确认的字段:文本长度是否与落盘副本一致、分句模式是否是这次想测的值、语音包与语言码是否是实际生效值、合成开始与结束是否正常配对、有没有被截断或重试的记录。日志关键字因项目而异,先用一次成功任务和一次异常任务各 grep 一遍,把实际字段名记下来,之后按这套关键字查。
把结论写成可复用的判定顺序
同样的现象下次还会遇到,把上面四步固化成顺序表,按顺序走,避免每次都从“是不是模型不好”开始猜。
| 顺序 | 检查动作 | 判定依据 | 倾向结论 |
|---|---|---|---|
| 1 | 比对页面原文与送合成文本副本 | 字符是否一致 | 不一致:预处理或清洗规则改动文本 |
| 2 | 换分句模式合成同一文本 | 错位位置是否随断句点移动 | 移动:分句规则问题 |
| 3 | 换语音包或语言码合成同一文本 | 错位是否只在某一语音包出现 | 只在该包出现:语音包或语言码问题 |
| 4 | 查日志中实际生效的参数 | 参数是否与预期一致 | 不一致:配置未生效或被默认值覆盖 |
每次排查建议记录的字段固定为:时间、任务 ID、输入文本摘要或哈希、分句模式与断句位置、语音包、语言码、输出文件路径、输出时长、错位类型(漏句/串句/停顿异常)、结论、处理动作。记录里保留输入副本和输出文件,比只写一句“调了分句就好了”有用得多,下次同类问题可以直接对照前面几轮的结果判断,而不必重跑全部组合。