决定语音产品体感的,是首包延迟,不是总合成时间
大多数团队在做语音功能时,一开始量的就是错的指标:合成一句话,掐整个请求的表,记下一个「1.4 秒」。上线之后用户说慢,可 1.4 秒写在纸面上明明不慢。
错位来自一个很朴素的事实:人感知不到总合成时间,人感知到的是沉默。
用户真正在掐的那块表
一个人对语音助手说完话,他心里的秒表就开始走。这块表在他听到第一声回应的瞬间就停了。第一个音频到耳朵之后发生的一切——剩下 90% 的句子还在合成、缓冲、播放——对这块表都是不可见的,因为那时他已经在听了。
所以两个总合成时间完全一样的系统,体感可以天差地别:
- 系统 A:花 1.4s 把整句合成完,再开始播。感知等待 1.4s。
- 系统 B:90ms 吐出第一个音频包,边播边合成剩下的。感知等待 90ms。
同样的工作量,同样的总算力。一个像是坏了,一个像是秒回。
区分它们的指标叫首包延迟——从你发出文本,到第一个可播放的音频块回来,中间隔了多久。这是唯一一个能对应上人类真实体验的延迟数字。
为什么非流式接口修不好这件事
请求/响应式的 TTS 接口有个结构性问题:它必须拿到全部,才能返回任何东西。 响应体是一个音频文件,而音频文件不完整就是无效的。于是感知延迟的下限就被钉死在完整合成时间上,不管模型多快。
把模型做快当然有用,但它在跟错误的约束较劲。把合成时间从 1.4s 砍到 0.7s 是一笔很大的工程投入,而换来的等待仍然清晰可闻。把同样这 1.4s 的合成改成流式吐出,则几乎消掉了全部感知等待,而模型速度一点没动。
这也是 MiraEcho 把合成做成 WebSocket 流式、而不是只提供文件接口的原因:音频边生成边逐块吐出。在 Flash 档位上,首包在 100 毫秒以内返回——低于大多数听者能察觉到「有停顿」的阈值。
容易被忽略的另一半:播放不能追上生成
流式解决了开头,同时在结尾引入了一个新问题。如果你的合成速度慢于实时播放速度,播放器会在句子中间把缓冲区吸干,然后卡顿。一个词读到一半的卡顿,比干净的 1.4s 等待更糟——前者读起来像故障,后者读起来像在思考。
所以流式实现需要同时满足两个条件,不是一个:
- 首包足够低 —— 播放才能尽早开始。
- 生成快于播放 —— 开始之后缓冲区才不会见底。
第二条才是让你能在 90ms 起播、并且把一段三十秒的内容一口气读完不断的原因。评估流式 TTS 供应商时,两个都要量。一个首包漂亮但生成慢于实时的供应商,实际听感会输给一个首包一般但从不卡顿的。
一个实操的验证办法:合成一段长文本,记录每个 chunk 的到达时间戳,然后把「已收到音频的累计时长」对「已流逝的墙上时间」作比较。如果音频时长曲线始终在墙上时间之上,说明你有余量;如果两条线在收敛,那你离一次网络抖动导致的卡顿就只差一步。
什么场景下最要紧
输出越长,总时间和首包时间的差距就越大,所以收益跟你的产品「说多少话」成正比:
- 对话式助手与呼叫自动化——每一轮都要付一次延迟成本,在一整段对话里会累积。这类场景流式不是可选项。
- 实时旁白与交互式应用——用户正盯着某件事发生,沉默会被读成卡死。
- 视频配音、有声书这类批量合成——这里总吞吐确实比首包延迟更重要,因为没有人在实时等待。该优化的是另一个数字。
最后这条值得明说,因为该看哪个指标,取决于有没有人在等。如果你是通宵渲染一本两小时的有声书,首包延迟毫无意义,你该量的是每小时音频的成本。
结论
凡是有人在等的场景,量首包延迟,别量总合成时间。然后确认生成速度跑赢播放速度,别让「起播快」变成「中间卡」。这两个数字合在一起,比任何单一吞吐指标都更能预测一个语音产品的体感。
想看看你现在的方案落在哪个位置,MiraEcho 的流式合成可以免费开始用——发一个请求,把 chunk 时间戳打出来,跟你手上那套比一比。