多余的一声「嗯」:Fun-CosyVoice3 zero_shot 开头伪影排查实录

一次「听起来只是多了一个语气词」的报障,最后落成一段带三道安全阀的输出侧裁切逻辑。

环境:Fun-CosyVoice3-0.5B · RTX 4060 Ti 16G · worker 分支 5.0.0 · 2026-10-29

TL;DR

项目 结论
现象 克隆音色「温柔姐9s」合成的语音,正文前多出一个约 0.2s 的「嗯」哼声,中间隔一小段静音
排除项 参考文本与参考音频不一致(CosyVoice 的经典坑)------ 转写逐字比对完全一致
根因 该音色的确定性模型行为:跨文本、跨随机种子、跨参考音频预处理都稳定存在
修复 输出侧按特征签名裁切(_trim_leading_blip),并升 TTS 缓存版本让旧成品失效
效果 伪影裁掉 0.33s,正文从 0.05~0.1s 直接开始;ASR 转写内容无缺失

真正有价值的部分不是那段裁剪代码(不到 40 行),而是怎么把一个听感问题变成可证伪的实验 ,以及为什么最终没有改输入端。


1. 现象

用户在测试页合成一句话,反馈「开头多了一个无关的音节」。

单听很难描述成「嗯」,更像一声含糊的起音。这一单对应的合成记录是 125735(17:44),音频长度正常、正文完整------只是开头多了一点东西。

先做的第一件事不是看代码,而是确认现场:把那条记录下载下来,切开开头 1 秒单独分析。


2. 先排除最像的那个原因:音文不一致

CosyVoice 的 zero_shot 模式有一个已知坑,也是本项目 踩过的:当 prompt_text 与实际参考音频不一致时,模型会把参考文本吐在成片开头。症状同样是「开头多了一段不是正文的话」。

所以第一步是最小成本的证伪:

  1. 从库里取出这个音色的 ref_audio 与 ref_text;
  2. 下载 ref_audio,用 FunASR 转写;
  3. 与库里的 ref_text 逐字比对。

结果:9.775 秒,内容逐字一致。

这条线直接断掉。注意这一步的价值------它花了不到两分钟,却排除了一个「改后端数据就能修好」的方向,避免后续所有分析都建立在错误假设上。

顺带确认了合成模式:标准中文 + 有参考文本 → 走 zero_shot(不是 instruct2),单 chunk。


3. 把「听感」变成「可测量的量」

「开头多了一个音节」不足以定位。需要三级观测,逐级提高语义分辨率:

第一级:ASR 转写整条音频 ------ 结果是干净的。

这是关键的一次「观测工具失效」:ASR 主动吞掉了那个短音节(语音段检测从 0.15s 才开始)。不能因为转写干净就认为没问题,用户听到的是波形,不是转写文本。

第二级:能量包络 ------ 真相出现。

对开头 1 秒做逐帧 RMS:

复制代码
0.00 ~ 0.20s   有明确人声能量(rms 峰值 8860)
0.20 ~ 0.35s   静音
0.35s 之后     正文「左边克隆...」开始

也就是说,音频开头确实多了一段有能量的人声 ,而不是静音、不是噪声、不是淡入残留。这也解释了为什么项目里现成的 _normalize_pacing(只裁首尾静音)完全没拦住它------哼声是有声的,不在静音判据的射程内。

第三级:切片识别 ------ 确认它是什么。

把 0~0.2s 切出来、复制拼接若干次后单独送 ASR,识别结果是一个 「嗯」。

既不是参考文本,也不是正文内容------是模型自己生成的一个语气起音。


4. 三组对照实验:证明「输入端没有可修的东西」

到这里已经可以猜「这是模型伪影」,但「猜」不能作为改架构的依据。需要证明:参考侧的所有可控变量都不是原因。

# 变量 操作 结果
1 参考音频起始状态 给参考音频加前置静音后再合成 开头 0~0.15s 仍有哼声(rms 7392),只是后面的静音变长了
2 随机性 种子 1234 → 42 开头仍有哼声(服务端种子固定,1234 下为确定性复现)
3 参考音频自身起音 裁掉参考音频开头 0.05~0.1s 的低电平起音(rms 1008,ASR 都忽略的气声段) 开头仍有 0~0.25s 的哼声

第 3 组实验来自一个具体假设:参考音频自己开头就有一段低电平气声,模型可能把它「学」进去并在生成时复现。实验结果是否掉------这个假设很自然,但错了。

再加一组横向证据(不做新推理,直接翻历史):

记录 文本 开头签名
125735 左边克隆一个音色... 0~0.15s 有声 → 0.2~0.5s 静音 → 正文
125652 完全不同的文本 0~0.15s 有声 → 0.2~0.5s 静音 → 正文

同音色、不同文本、相同签名 → 伪影与文本无关,是这个音色的确定性模型行为。

结论

Fun-CosyVoice3 的 zero_shot 对部分克隆音色会在正文前稳定哼一个短音节。参考文本、参考音频内容、参考音频起始状态、随机种子,全都不影响它。

也就是说:输入端没有 bug 可修。要么换模型,要么在输出侧裁掉。


5. 修复:按「特征签名」裁切,而不是按「静音」裁切

5.1 签名定义

伪影的可观测特征是稳定的三段结构:

复制代码
[短有声段 ≤0.35s] [静音间隙 ≥0.08s] [正文...]

只有三者同时成立才动手。这就是「误伤面小」的来源------真人朗读几乎不会以「短音节 + 明显停顿」开场。

5.2 三道安全阀

裁剪逻辑本身很短,但它必须宁可不动,也不能把话吃掉:

  1. 长度阀 :第一段有声段必须 ≤ 0.35s(长的就不是哼声,是正文);
  2. 后文阀:静音之后必须还有正文(整条音频只有一声短音节时,那可能本身就是正文,不裁);
  3. 保底阀 :裁完留 0.05s 起手静音,不贴着脸切,避免削到字头。
python 复制代码
# sse/utils/cosyvoice_tts_util.py
_LEADING_BLIP_MAX = 0.35   # 第一段有声段的上限
_LEADING_BLIP_GAP = 0.08   # 判定为「伪影后停顿」的最小静音
_BLIP_HEAD_KEEP = 0.05     # 裁完保留的起手静音

def _trim_leading_blip(path: str) -> None:
    """去掉输出开头的哼声伪影,就地把 wav 改掉。"""
    if not path or not os.path.exists(path):
        return
    try:
        import numpy as np
        import soundfile as sf
        wav, sr = sf.read(path)
        mono = wav.mean(axis=1).astype("float32") if wav.ndim > 1 else wav.astype("float32")
        silences = _silence_intervals_from_array(mono, sr, min_dur=_LEADING_BLIP_GAP)
    except Exception as e:
        logger.warning(f"[CosyVoice3] 开头伪影检测失败,保留原音频:{e}")
        return
    if not silences or silences[0][0] > _LEADING_BLIP_MAX:
        return                                  # 阀 1:开头不是短音节
    end_of_first_gap = silences[0][1]
    if end_of_first_gap >= len(mono) / sr - 0.15:
        return                                  # 阀 2:静音之后没正文
    cut = int(max(end_of_first_gap - _BLIP_HEAD_KEEP, 0) * sr)
    if cut <= 0 or cut >= len(wav):
        return
    logger.info(f"[CosyVoice3] trimmed leading blip: {cut / sr:.2f}s from {os.path.basename(path)}")
    sf.write(path, wav[cut:], sr)               # 阀 3:留 0.05s 起手静音

5.3 静音判据为什么不用 silencedetect

裁剪依赖「静音在哪」,而这个判据本身就是一次踩坑的产物:

CosyVoice 的停顿里混着气声与房间底噪(实测 -30dB 上下,只比语音低 6~8dB)。

  • 用 ffmpeg silencedetect 固定 -40dB :同一段 12.5s 的旁白只认得出 0.28s 静音,等于白裁;
  • 定高了(-20dB):开始啃字头。

所以改用帧级 RMS 的相对门限(10ms 帧 / 25ms 窗):

复制代码
语音电平 loud = p90(db)
门限 thr = max( p5(db) + 6dB,  loud - 30dB,  -34dB )
再夹一次:thr = min(thr, loud - 8dB)   # 电平平坦的素材(一声长音)别整条被判成静音

这段判据现在是 _silence_intervals_from_array,被三处共用:伪影裁切、_normalize_pacing 的首尾裁剪、句内长停顿压缩。

5.4 挂在哪个位置

裁切放在 _infer_single 的落盘之后,而不是放在最外层 generate_speech:

  • _infer_single 是唯一的落盘口------单段合成、拆尾合成的 head/tail 两段都要经过它,一处拦截全覆盖;
  • 拆尾那条路(_infer_with_tail_guard)拼接前会重新整理静音,若把裁切放在更外层,head 段的伪影会先被拼进成片再处理,判据就被污染了。

5.5 缓存必须跟着失效

这一步容易漏,漏了等于没修:

TTS 产物有内容寻址缓存(key = 文本 + 参考音频 sha256 + 参数 + 模型/算法版本 )。后处理代码的改动不会改变 key ,而 store 又是「已存在就不覆盖」------旧缓存里的伪影成品会被继续命中,重跑也写不进去。

所以这次同时把 COSYVOICE_CACHE_VERSION 从 1 升到 2。


6. 验证:单测 → 真实合成 → 端到端

层级 做法 结果
单元 构造「0.2s 短音 + 0.3s 静音 + 1.5s 正文」 2.00s → 1.57s,伪影段被裁走
单元 构造「开头直接是正文」的 2.00s 音频 不变(不该误伤)
真实合成 用「温柔姐9s」的参考音频跑 generate_speech 日志 trimmed leading blip: 0.33s,正文从 0.1s 开始
端到端 后端 /voice/tts 提交 → worker 合成 → 回调(record 125736) 产物 0.05s 直接正文,ASR 转写内容完整无缺字

端到端这一环用的是假身份(md5=...001),需要给它在 interactive_user.credits 和 credit_service(credit_limit / expire_date)两张表都补额度才能过 402;验完把两表记录和业务记录一起清掉。


7. 已知局限

诚实记录边界,比宣称「已完全解决」有用:

  1. 短应答词开头会被误裁 。像 OK,我们开始。 这种「短音节 + 停顿」的开场,签名与伪影完全相同(OK 约 0.3s)。目前的缓解是「静音后必须有正文」------它保证了不会裁空,但保不住语义。真正的解法需要文本对齐(判断开头音节是否落在文本首字上),属于下一步。
  2. 只裁一次,不做连环裁切。伪影目前只观察到一次,按未知情况不做过度设计。
  3. 依赖能量判据。极端素材(全程高噪、动态范围不足 12dB)会走「检测不出 → 保留原音频」的保守分支,不会误伤,但也不会修好。
  4. 老音色不会自动变干净。伪影是音色属性,已克隆音色靠输出侧兜底;新克隆走的是同一套输出侧逻辑,行为一致。

8. 可复用的四条方法论

  1. 先把最像的那个假设做成可证伪实验。「音文不一致」花两分钟证伪,省掉了后面所有基于错误假设的分析。
  2. 听感 → 能量 → 语义,三级观测递进。ASR 干净不代表音频干净:短音节会被语音活动检测吞掉。切出来放大、重复、再识别,是让「疑似语气词」变成「确定是嗯」的关键一步。
  3. 找到稳定复现的最小变量集,再决定改哪一侧。跨文本(历史记录)、跨种子、跨参考侧三种处理 ------ 三个变量都不影响,才敢下「输入端无解」的结论。少做一个对照,都可能是「改了输入端但其实没修好」。
  4. 修复位置要跟着因果走,缓存要跟着修复走。因果在输出侧就别在输入端堆补丁;而只要动了后处理,就必须升缓存版本------因为缓存 key 只对输入敏感,对代码不敏感,且写入是「不覆盖」的。

相关推荐
manyingAi3 小时前
AI漫剧生产系统架构解析:从文本拆解到音视频合成
人工智能·音视频
技灵AI7 小时前
MiniMax H3 能做哪些视频?商品广告、人物表演与运镜怎么选
人工智能·prompt·aigc·音视频
AIsoft_868910 小时前
职场音频文件怎么整理?录音APP有哪些用法
android·音视频
EasyCVR11 小时前
多厂区值守点分散,视频汇聚/视频融合/视频分析平台EasyCVR人员离岗AI监测怎么做才省事?
音视频·easycvr·国标gb28181
TK泰妞11 小时前
TikTok 商品视频有播放却没点击?从 4 个环节定位内容问题
音视频
Raymond168811 小时前
零样本TTS长视频配音的工程取舍:自回归、非自回归与韵律边界
人工智能·数据挖掘·回归·音视频·音频
TK泰妞12 小时前
TikTok 商品视频素材库怎么建?从文件命名到复用管理的完整方法
人工智能·音视频
枯木◊靠推文躺平版14 小时前
AI短视频制作选剪映还是TapNow:全场景创作效率实测对比
人工智能·音视频
秦禹辰14 小时前
ClipSketch AI把视频改造成图文漫画:选关键帧、生成手绘分镜、融合IP形象并自动写小红书文案
人工智能·tcp/ip·音视频