MiniMind-O 学习笔记 18:Talker 如何把回答变成语音

上一篇从 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 三个阶段。

发表评论