理解 MiniMind-O 的 Talker,先要理解一个问题:
模型为什么不是直接生成 wav 波形? 为什么要生成 Mimi codes?
文本模型生成的是 token id。MiniMind-O 的语音输出也走类似思路:不直接生成连续音频,而是先把音频离散化成一串 code id,让 Talker 预测这些 code id,再由 Mimi decoder 还原成声音。
1. 直接生成波形为什么难
音频是连续信号。比如 README 中提到 Mimi 支持 24 kHz 音频,这意味着 1 秒音频有 24000 个采样点。
如果让模型逐点生成波形:
1 秒 -> 24000 个点 10 秒 -> 240000 个点
这个序列太长了。对于 Transformer 来说,训练和推理都会很重。
所以很多语音生成系统会先使用神经音频 codec:
waveform -> codec encoder -> 离散 audio codes audio codes -> codec decoder -> waveform
在 MiniMind-O 里,这个 codec 是 Mimi。
2. Mimi 可以看成语音版 tokenizer
文本 tokenizer 做的是:
自然语言字符串 -> token ids
Mimi 做的是:
语音 waveform -> audio code ids
所以可以类比:
文本: "你好" -> tokenizer -> [token ids] 音频: 一段语音 -> Mimi encoder ->
反过来:
文本 token ids -> tokenizer.decode -> 字符串 audio code ids -> Mimi decoder -> 声音
MiniMind-O 让 Talker 学的不是 waveform,而是 Mimi 的 audio code ids。
3. 什么是 codebook
codebook 可以理解成一本离散声学字典。
神经 codec 会把连续音频特征映射到字典中的某个条目:
连续声学特征 -> 在 codebook 里找一个最合适的条目 -> 用这个条目的编号表示
如果一个 codebook 有 2048 个条目,那么一个 code 就大致是:
0..2047
这和文本词表很像:
文本词表: token id -> 一个文本片段 音频 codebook: code id -> 一个声学表示
当然,audio code 对应的不是人能直接读懂的字,而是 codec 学出来的声学向量。
4. 为什么是 8 路 codebook
README 中提到 MiniMind-O 使用 Mimi 的 8 层 codebook。一个音频时间片不是只用一个 code 表示,而是用 8 个 code 一起表示:
frame_t = [ codebook_0 的 code, codebook_1 的 code, codebook_2 的 code, codebook_3 的 code, codebook_4 的 code, codebook_5 的 code, codebook_6 的 code, codebook_7 的 code ]
你可以把它理解成多本字典共同描述同一帧音频。
一种常见直觉是“前面的 codebook 更粗,后面的 codebook 补细节”。这有帮助,但不要把它严格理解成:
codebook 0 = 低频 codebook 7 = 高频
它不是传统信号处理里的分频滤波器组。更准确地说,它们是神经 codec 学出来的多级离散表示,类似多级残差量化:
第 1 本字典表示一部分主要信息 第 2 本字典补充剩余信息 后面的字典继续补充细节
5. MiniMind-O 里的 audio_vocab_size
在 OmniConfig 中有:
self.audio_vocab_size = kwargs.get("audio_vocab_size", 2112)
常规 Mimi code 大致使用:
0..2047
2048 以后留给特殊 token,比如:
2049 -> audio_pad_token 2050 -> audio_stop_token 2051 -> audio_spk_token
这些特殊 token 的作用类似文本里的 pad/eos,只是用在音频 code 序列里。
6. TalkerHead 为什么输出 8 组 logits
Talker 不是输出一个音频 token,而是输出 8 路 codebook 的预测分布:
audio_logits[0] -> codebook 0 audio_logits[1] -> codebook 1 ... audio_logits[7] -> codebook 7
每一路 logits 的最后一维大小都是 audio_vocab_size。
训练时:
audio_logits[i] 和 audio_labels[:, i, :] 计算交叉熵
推理时:
从每一路 logits 中采样一个 code 8 个 code 合起来构成一个 audio frame
7. 什么是错位补齐
如果一个音频 frame 需要 8 个 code,那么最直接的办法是同一时刻并行预测 8 个 code。
MiniMind-O 采用了错位方式。可以先看成把 8 路 codebook 斜着排开:
step cb0 cb1 cb2 cb3 cb4 cb5 cb6 cb7 --------------------------------------------------------------------- 1 c0_0 pad pad pad pad pad pad pad 2 c0_1 c1_0 pad pad pad pad pad pad 3 c0_2 c1_1 c2_0 pad pad pad pad pad 4 c0_3 c1_2 c2_1 c3_0 pad pad pad pad ... 8 c0_7 c1_6 c2_5 c3_4 c4_3 c5_2 c6_1 c7_0
到第 8 步时,frame_0 才凑齐:
frame_0 = [c0_0, c1_0, c2_0, c3_0, c4_0, c5_0, c6_0, c7_0]
代码中会按对角线把同一个音频时间片取出来。
为什么这样做?
1. 保持自回归生成形式。 2. 让不同 codebook 之间不是完全独立。 3. 预热几步后可以流式输出 audio frame。
8. 8 路 codebook 在时域上对齐吗
要分两个时间轴:
音频时间轴: 送进 Mimi decoder 前,8 路 codebook 是对齐的。 生成时间轴: Talker 自回归生成时,8 路 codebook 是错位展开的。
所以最终音频 frame 上是对齐的:
frame_0 = [c0_0, c1_0, ..., c7_0]
只是生成时为了自回归和流式,被临时错开了。
9. 本文小结
MiniMind-O 的语音输出链路可以总结成:
Talker 预测 Mimi audio codes Mimi decoder 把 codes 还原成 waveform
其中:
一个音频 frame = 8 路 codebook 各出一个 code Talker 输出 = 8 组 codebook logits 错位补齐 = 生成时间轴上的延迟展开,最终再对齐成音频 frame
下一篇我们看这些 audio codes 如何和文本对话一起被放进 OmniDataset,最终变成训练样本。