上一篇从 forward 看到了 Thinker-Talker 的整体协作。
这一篇专门看 Talker。
Talker 的职责可以先用一句话概括:
在 Thinker 的语义 hidden 条件下,自回归生成 Mimi 的 8 路 audio codes。
它不是传统 TTS,也不是简单地把文本字符串读出来。
1. Talker 不是外置 TTS
传统 TTS 流程是:
LLM 生成完整文本 -> tokenizer.decode 成字符串 -> TTS 朗读字符串
MiniMind-O 不是这样。
它的流程更接近:
Thinker 生成文本 logits 和 bridge_states -> Talker 读取 bridge_states + 历史 audio codes -> 预测 Mimi audio codes -> Mimi decoder 还原声音
从听感上,Talker 确实像是在“把 Thinker 的回答念出来”。但从实现上,它没有等完整文本字符串出来后再调用 TTS,而是在 hidden state 条件下直接生成音频 token。
2. TalkerModule 里有什么
TalkerModule 在 model/model_omni.py 中。
主要模块:
embed_tokens: 把历史 8 路 audio codes 变成 embedding。 embed_proj: 把 Thinker 的 bridge_states 投影到 Talker hidden size。 codec_proj: 把历史 audio embedding 投影到 Talker hidden size。 layers: Talker 自己的 Transformer blocks,默认 4 层。 lm_head: 输出 8 路 codebook logits。 spk_proj: 把 speaker embedding 注入 Talker,用于音色控制。
可以画成:
bridge_states -----> embed_proj ----+
|
+-> Talker layers -> audio_logits
audio_ids -> embed_tokens -> codec_proj ----+
其中 text_scale 和 audio_scale 是两个可学习系数,用来调节语义条件和历史音频条件的比例。
3. 为什么 Talker 要接收 audio_ids
audio_ids 不是用户输入语音。用户输入语音叫 audio_inputs。
audio_ids 是 Talker 自己的历史输出,也就是之前已经生成过的 Mimi codes。
这和文本自回归很像:
文本 LLM: previous text tokens -> next text token Talker: previous audio codes + bridge_states -> next audio codes
如果 Talker 只看 bridge_states,它知道“要说什么”,但不知道“前面已经怎么说了”。历史 audio codes 能帮助保持:
音频连续性 停顿和韵律 发音状态 音色延续 8 路 codebook 的历史依赖
4. 音色条件怎么进入 Talker
如果有 speaker embedding,代码会用 audio_spk_token 找到占位位置:
spk_mask = (audio_ids[:, 0, :] == self.audio_spk_token).unsqueeze(-1)
然后用:
self.talker.spk_proj(spk_emb)
替换对应位置的 Talker embedding。
可以理解成:
audio_spk_token 是一个占位符; 真正的音色信息来自 spk_emb; spk_proj 把 spk_emb 映射到 Talker hidden space。
README 里提到 MiniMind-O 支持内置音色、unseen 音色和参考音频音色克隆,这部分能力就和 ref codes / speaker embedding 条件有关。
5. 推理时一次 forward 输出什么
严格说,forward 不直接输出 token,而是输出 logits。
推理单步时,可以近似理解为:
Thinker: 输出下一个文本 token 的分布。 Talker: 输出 8 个 codebook 的下一个 code 分布。
然后 stream_generate 会采样:
text_token = sample(out.logits) audio_code_i = sample(out.audio_logits[i])
文本 token 会接回 input_ids,音频 codes 会写回 audio_buffer,供下一步继续生成。
6. 为什么音频生成有延迟
代码里有:
audio_step = step - 1
普通模式下,音频比文本稍微延迟。这很好理解:
模型先确定一点文本语义, Talker 再开始渲染对应音频 codes。
同时,因为一个 audio frame 需要 8 路 codebook,Talker 还要用错位方式逐步补齐 8 路 codes。
7. stream_generate 如何拼 audio frame
Talker 会维护:
audio_codes = [[] for _ in range(8)]
每一路 codebook 都有自己的历史序列。
当 8 路足够补齐一个 frame 时,代码会取:
frame = [audio_codes[i][step - 7 + i] for i in range(8)]
这就是从错位生成缓存中,取出同一个音频时间片的 8 个 code。
拿到 frame 后,就可以交给 Mimi decoder 逐步恢复音频。
README 中说 MiniMind-O 支持流式语音生成,本质上就是:
Talker 不必等完整回答结束; 预热几步后,每步都能继续吐出新的 audio frame。
8. Thinking 模式会不会念出思考过程
不会默认念出。
如果开启 open_thinking,代码会等待生成:
</think>\n\n
在这个标记出现之前,Talker 只填 audio pad,不产生有效音频。
所以流程是:
Thinker: 可以生成 <think>...</think> Talker: 等 </think> 之后再开始说正式回答
这和实际体验也一致:思考过程不应该被读出来。
9. Talker 什么时候停止
Thinker 和 Talker 有各自的停止信号。
Thinker 文本结束:
text_token == eos_token_id
Talker 音频结束:
8 路 codebook 都出现 stop/special code
代码会等两边都完成:
文本结束 AND 8 路音频都结束 -> 整个生成结束
所以 Talker 的终止不一定和 Thinker 的 EOS 落在同一个位置。文本说完后,音频可能还要补尾音、停顿和 stop code。
10. 本文小结
Talker 可以理解为语音 token 生成器:
输入: Thinker bridge_states 历史 audio_ids 可选 spk_emb 输出: 8 路 Mimi codebook logits
它的效果像“把回答说出来”,但实现上不是外置 TTS,而是端到端地生成 Mimi audio codes。
下一篇我们看训练流程:为什么 MiniMind-O 要分成 T2A、A2A audio_proj、A2A full 三个阶段。