机器人已经想好了要说什么:「好的,少冰珍珠奶茶一杯,十八块」。现在要把这 15 个字变成一段能播的声音。
最直接的想法是让模型直接吐波形。算一下就不行了。
这句话念出来 3 秒,24 kHz 采样就是 72000 个数字。模型每吐一个数要跑一遍完整前向 —— 72000 步,每步就算只要 5 毫秒,也得 6 分钟。
问题出在粒度:一个采样点几乎不携带信息,逐点决定等于让模型做七万次没信息量的决策。
出路是中间垫一层。
先让模型吐一种「粗一点」的东西,一秒几十个,几十步就够。再拿一个小网络把这几十个还原成七万个采样点 —— 这个小网络不用一个一个吐,一次就算 完。
它叫声码器(vocoder)。
这就是两段式,现在所有能用的 TTS 都是这个结构。
一、中间那层到底是什么
先把「粗一点的东西」说清楚,因为后面所有取舍都从它来。
音频进模型有两条路,选哪条不看审美,看这段音频是要被听懂还是要被生成:
路线 B 的「可逆」是它存在的全部理由。如果只做识别,没人会愿意先把音频压有损再喂给模型 —— 这也是为什么 ASR 模型几乎清一色走 mel,而 TTS 模型清一色走 codec。
生成侧必须走右边那条:只有离散 id 能被解码器还原成波形,连续向量不行。
那怎么把一小段声音变成整数?靠一张码本 —— 一张查找表,来一个向量就找最近的一行,记下行号。
问题是一张 1024 行的表只能表达 1024 种声音状态,编码一帧语音远远不够。把表做大?表越大查得越慢、越难训,做到十万行就不现实了。
RVQ(Residual Vector Quantization,残差向量量化)换了个方向 :一张表不够就串几张,每张负责修正前面几张留下的误差。
RVQ 的层间依赖是后面所有工程复杂度的根:如果各层独立,一帧就可以用 C 个并行的分类头一次算完;正因为有序,才必须在「一帧内串行解 C 次」和「把 C 层错开时间轴」之间做选择。
几个真实 codec 的规格,都能从各自 README 的码率反推:
| codec | 采样率 | 帧率 | 码本数 | 每秒整数个数 |
|---|
| EnCodec 24 kHz @ 6 kbps | 24 kHz | 75 Hz | 8 | 600 |
| DAC 44 kHz @ 8 kbps | 44.1 kHz | ~86 Hz | 9 | ~775 |
| SNAC 24 kHz @ 0.98 kbps | 24 kHz | 多尺度分层 | 3 层 | ~120 |
| Higgs Tokenizer | 24 kHz | 25 Hz | 8 | 200 |
| MOSS-Audio-Tokenizer-v2 | 48 kHz 立体声 | 12.5 Hz | 12 | 150 |
后两行是这两年的方向:帧率往下压,码本数往上加。原因很实际 —— 自回归的步数由帧率决定,而一帧内的多个码本可以在一步里算完。75 Hz 降到 12.5 Hz,同样长度的音频少跑六分之五的步数。
二、一帧几个码本,怎么塞进一维序列
新问题:RVQ 给出的是 [帧数, 码本数] 的二维网格,而模型只会读写一维序列。
摊平只有两个方向 —— 沿时间走(每步吐一帧的全部码本),还是沿码本层走(把各层错开到不同时间步)。现有模型就照这两个方向分成了两派:
方案 B 的 22 ms → 4 ms 不是模型变快了,是把上百次微小的 kernel launch 打包成一次 CUDA Graph 回放。这类「算得不多但发得太碎」的瓶颈是多码本 TTS 的通病,07 篇会展开。
这两派各自的官方架构图放在一起看最清楚。先是 delay pattern 那一路:
出处:sglang-omni docs/_static/image/higgs-architecture.png。左下角的 Ref Text 与 Ref Audio 就是 03 篇讲的克隆前缀;右侧 Multi-turn Generation 里 T 和 A 两种方块交替出现,是「一个序列同时承载文本和音频」的直接图示。
再是帧内并行那一路,骨干每帧只吐一个隐状态,由一个小的帧内 transformer 展开成固定深度的 RVQ 块:
出处:sglang-omni docs/_static/image/moss-tts-arch-local.png。同一支团队的 delay 版和 local 版参数量与 codec 都相同,是这个取舍难得的 A/B 对照。
三、两段各自的脾气
中间那层用的就是 02 篇的 codec token:一秒二三十帧,每帧几个整数。模型吐它,跟吐文字 token 一模一样。
关键是这两段的脾气正好相反 —— 而这个「相反」正是后面所有调度麻烦的源头: