MiniMind-O 学习手册

MiniMind-O 技术学习手册


文档核心说明

本文档适用于想要学习 Omni 模型原理、动手部署 MiniMind-O 的 AI 开发者与技术爱好者,涵盖从理论到实操的完整内容,包括:

  • Omni 模型核心架构与 MiniMind-O 模块拆解

  • 三阶段训练流程与关键技术设计

  • 环境部署与各类实际任务的代码实现

  • 完整可运行的桌面语音助手源码

  • 常见问题排查与性能优化方案


一、基础认知:MiniMind-O 概述

1.1 定位与核心价值

MiniMind-O 是 2026 年 5 月 5 日由光子平方开源的史上最小完整 Omni 模型(支持文本、语音、图像多模态输入输出的端到端统一模型),核心特点:

  • 主干参数仅0.1B,约是 Mini-Omni 的 1/5

  • 支持文字、语音、图像三模输入和流式语音输出

  • 全链路开源:代码、权重、训练数据、技术报告全部开放

  • 上手门槛低:单卡 RTX 3090 约 2 小时可跑完 mini 训练集

  • 可检查性强:所有设计决策都暴露在可手动改动的小系统中

1.2 与其他 Omni 模型的对比

模型 参数量 支持模态 开源程度 训练算力要求
Qwen3-Omni 千亿级 文本、语音、图像、视频 部分开源 大规模集群
MiniCPM-o 4.5 9B 文本、语音、图像、视频 完全开源 多卡 A100
Mini-Omni2 0.5B 文本、语音、图像 完全开源 多卡 3090
MiniMind-O 0.1B 文本、语音、图像 完全开源 (含数据) 单卡 3090

英文 T2A 任务性能对比

模型 参数量 Avg CER(↓) Avg WER(↓)
Mini-Omni 0.5B 0.0101 0.0185
Mini-Omni2 0.5B 0.0371 0.0431
MiniMind-O 0.1B 0.0964 0.0973

结论:短回答 (≤15 词) 场景下,MiniMind-O 与 Mini-Omni2 差距不大;中长回答 (16-30 词) 差距明显,这是 0.1B 规模的固有局限。


二、核心架构:Thinker-Talker 双路径设计

2.1 架构设计理念

MiniMind-O 采用了与 GPT-4o、Qwen3-Omni 相同的语义路径与声学路径分离Thinker-Talker 架构,这是现代 Omni 模型区别于传统 ASR-LLM-TTS 级联方案的关键。

传统级联方案的问题

  • 语言模型被架在声学循环之外

  • 音调、停顿、打断、情绪等信息在 ASR 阶段就已丢失

  • 错误会在三个独立模块间叠加放大

Thinker-Talker 架构的优势

  • 语义规划留在 Thinker,声学渲染留在 Talker

  • 两个目标互不干扰,各自优化

  • 支持流式语音生成,边思考边说话

2.2 整体工作流程

  1. 多模态输入通过各自编码器映射到统一隐空间

  2. Thinker 模块处理输入,生成语义表示

  3. Bridge 层从 Thinker 中间层提取语义信息,桥接到 Talker

  4. Talker 模块基于语义信息和历史音频码,自回归生成 Mimi Codebook 序列

  5. Mimi 解码器将 Codebook 序列还原为 24kHz 波形音频


三、模块详细拆解

3.1 输入层:多模态编码与对齐

MiniMind-O 支持三种输入模态,所有模态最终都被映射到统一的 MiniMind 隐空间:

模态 编码器 处理流程 输出
文本 原生 Tokenizer 直接进入 Embedding 层 文本 Token 向量
语音 冻结的 SenseVoice-Small 原始音频→SenseVoice 编码→两层 MLP 投影 音频特征向量
图像 冻结的 SigLIP2 图像→SigLIP2 编码→两层 MLP 投影 图像特征向量

关键细节

  • 所有外部编码器全程冻结,不参与梯度更新

  • 三种模态通过占位符位置对齐,最终落在同一条文本序列中

  • 语音和图像特征被注入到对应的\<\|audio_pad\|\>\<\|image_pad\|\>占位符位置

3.2 Thinker 模块:语义理解与推理

Thinker 就是完整的 MiniMind 语言模型主干,负责理解多模态输入并生成文本回复。

核心参数

  • 8 层 Transformer Decoder

  • Hidden Size: 768

  • 词表大小: 6400

  • 采用 RMSNorm 归一化、RoPE 位置编码、SwiGLU 激活函数

工作流程

  1. 接收统一隐空间中的多模态输入序列

  2. 通过自注意力机制进行上下文建模

  3. 生成文本回复 Token

  4. 同时向 Bridge 层输出中间层状态

3.3 Bridge 层:中间层语义桥接

Bridge 层是连接 Thinker 和 Talker 的关键组件,它决定了 Talker 能从 Thinker 获取什么样的语义信息。

为什么选择中间层而不是最后一层?

  • 太浅 (Embedding 层):语义信息不足,无法区分多音字等上下文相关信息

  • 太深 (最后一层):已被 next-token prediction 目标过度特化,包含太多 LM 头的几何噪声

  • 中间层:已积累足够上下文语义,同时还没有被文本生成目标过度塑形

MiniMind-O 的选择

  • 默认使用num_hidden_layers // 2 - 1层的状态

  • 8 层 Thinker 对应第 3 层之后的状态

  • 移动 Bridge 层位置会直接影响 Talker 的 CER (字符错误率)

3.4 Talker 模块:流式语音生成

Talker 是独立的 4 层 MiniMind Blocks,不与 Thinker 共享权重 (但初始化时可使用 Thinker 后 4 层的参数)。它的任务不是生成文字,而是预测 8 层 Mimi Codebook 序列。

输入组成

  1. Bridge State:从 Thinker 中间层提取的语义条件

  2. Mimi-Code 历史:自回归的音频码历史,提供声学上下文

这两路信号加权叠加,分别乘以可学习的text_scaleaudio_scale控制比例。

低秩 Codebook 接口设计
Mimi 使用 8 层 Codebook 表示语音,最朴素的做法是给每层各弄一套 Embedding Table 和 Output Head,参数量会直接乘 8。MiniMind-O 采用了更省参数的方案:

  • 一个共享的 Embedding/Head 主体

  • 每个 Codebook 一个轻量的低秩 Adapter

  • 实验表明 Output Head 的 Rank 比 Embedding 的 Rank 更重要

3.5 输出层:音频解码

Talker 预测的 8 层 Mimi Codebook 序列最终由Mimi 解码器还原成 24kHz 的波形音频。

流式生成机制

  • 第一步文本 Token 出来后,音频 Code 才开始按 Codebook 层数延迟输出

  • 凑满 8 层就可以解码一帧

  • 边生成边播放,不用等到文本回答结束

  • 单卡 3090 上首音延迟约 260ms


四、训练流程:三阶段训练

MiniMind-O 的训练分为三个明确的阶段,所有外部编码器全程冻结:

4.1 训练阶段说明

阶段 目标 学习率 训练时长 (4 卡 3090) 数据集
Stage 1: T2A (文本→语音输出对齐) 让 Talker 在 Thinker 的语义条件下学会生成 Mimi Codes 5×10⁻⁶ 约 45 分钟 sft_t2a (1,248,923 个样本,中英文各半)
Stage 2: A2A (语音输入接入) 打通完整的 speech-in/speech-out 链路 - 约 100 分钟 sft_a2a (414,024 个样本,中文占 70.8%)
Stage 3: I2T (视觉对齐) 接入图像输入能力 - 约 45 分钟 sft_i2t (约 100K 个样本)

总训练时长

  • 4 卡 RTX 3090:约 4 小时跑完完整训练

  • 单卡 RTX 3090:约 2 小时跑完 mini 数据集 (仅英文)

4.2 序列格式与对齐

训练样本是一个九路并行序列:1 路文本 + 8 路 Audio Code。

对齐规则

  • 文本监督只打在 Thinker 的回复 Token 上

  • 音频监督只打在目标 Mimi Code 位置

  • 回复开始前,Talker 侧是 Padding

  • 参考音频的 Mimi Codes 右对齐放在目标区域之前,只提供条件、不计入 Loss

4.3 音色控制机制

MiniMind-O 的音色控制通过三种方式实现:

  1. 专用 Speaker Token:在音频序列中预留一个\<\|audio_spk\|\>位置

  2. 参考 Codec Prompt:右对齐的参考 Mimi Codes

  3. CAM++ Speaker Embedding:192 维的说话人嵌入向量

优势

  • 音色条件是音频码上下文的一部分,而不是独立的 TTS 模块

  • 切换音色只需改变参考 Codes 和 Speaker Embedding,无需重新训练模型

  • 支持Zero-Shot 音色克隆

4.4 实时交互与打断

  • 延迟指标:单卡 3090 上首字延迟约 140ms,首音延迟约 260ms

  • 打断机制:使用 VAD 阈值检测用户是否开口,检测到后立即放弃当前生成、重新开始 Prefill

  • 局限性:目前只是声音级别的打断,不是语义级别的全双工


五、环境准备与快速部署

5.1 环境要求

  • GPU:单卡 24GB (训练 mini 数据集);推理可使用 CPU

  • CUDA 12.2

  • Python 3.10

5.2 快速部署步骤

  1. 克隆仓库
1
2
git clone --depth 1 https://github.com/jingyaogong/minimind-o
cd minimind-o
  1. 安装依赖
1
pip install -r requirements.txt
  1. 下载外部模型
1
2
3
4
5
6
# 使用ModelScope下载
pip install modelscope
modelscope download --model iic/SenseVoiceSmall --local_dir ./pretrained/sensevoice
modelscope download --model google/siglip2-base-patch32 --local_dir ./pretrained/siglip2
modelscope download --model kyutai/mimi --local_dir ./pretrained/mimi
modelscope download --model iic/speech_campplus_sv_zh-cn_16k-common --local_dir ./pretrained/campplus
  1. 下载发布权重
1
modelscope download --model gongjy/minimind-3o-pytorch --local_dir ./out

5.3 基础环境验证

1
2
3
4
from model.minimind_omni import MiniMindOmni
model = MiniMindOmni.from_pretrained('./out/sft_omni')
print('模型加载成功!')
print(f'支持模态: {model.supported_modalities}')

六、实际任务实战

6.1 核心 API 接口

MiniMind-O 提供了统一的 generate() 接口,支持所有输入模态的组合:

1
2
3
4
5
6
7
8
9
10
response = model.generate(
text=None, # 文本输入
audio=None, # 音频输入 (numpy array, 16kHz, mono)
image=None, # 图像输入 (PIL Image)
max_tokens=512, # 最大生成文本长度
temperature=0.7, # 温度系数
stream_text=True, # 流式输出文本
stream_audio=True,# 流式输出音频
speaker_id=0 # 说话人ID
)

返回值是一个生成器,每次产生一个字典:

1
2
3
4
{
'text': '当前生成的文本片段',
'audio': numpy.array([...]) # 当前生成的音频片段 (24kHz, mono)
}

6.2 纯文本对话任务

这是最基础的任务,与普通 LLM 使用方式完全一致。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from model.minimind_omni import MiniMindOmni

# 加载模型
model = MiniMindOmni.from_pretrained('./out/sft_omni')

# 纯文本对话
print("MiniMind-O: 你好!我是 MiniMind-O,有什么可以帮你的吗?")
while True:
user_input = input("你: ")
if user_input.lower() in ['exit', 'quit']:
break

print("MiniMind-O: ", end='', flush=True)
full_text = ""
for chunk in model.generate(text=user_input, stream_audio=False):
if chunk['text']:
print(chunk['text'], end='', flush=True)
full_text += chunk['text']
print()

最佳实践

  • 对于简单问答,temperature=0.6-0.8 效果最佳

  • 对于需要精确答案的任务,使用 temperature=0.1-0.3

  • 对于创意写作,使用 temperature=0.9-1.0

6.3 语音交互任务

这是 MiniMind-O 最具特色的功能,支持端到端的语音输入和语音输出。

基础语音交互代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import sounddevice as sd
import numpy as np
from model.minimind_omni import MiniMindOmni
from utils.vad import VADDetector

# 加载模型和VAD
model = MiniMindOmni.from_pretrained('./out/sft_omni')
vad = VADDetector(sample_rate=16000)

# 音频参数
INPUT_SAMPLE_RATE = 16000
OUTPUT_SAMPLE_RATE = 24000

print("MiniMind-O 语音助手已启动,说话即可开始对话...")
print("按 Ctrl+C 退出")

try:
while True:
# 录音直到检测到静音
print("\n正在听你说话...")
audio = vad.record_until_silence()

print("正在思考...")
# 生成回复
full_text = ""
audio_buffer = []

for chunk in model.generate(audio=audio, stream_text=True, stream_audio=True):
if chunk['text']:
print(chunk['text'], end='', flush=True)
full_text += chunk['text']

if chunk['audio'] is not None:
audio_buffer.append(chunk['audio'])

# 播放完整音频
if audio_buffer:
full_audio = np.concatenate(audio_buffer)
sd.play(full_audio, OUTPUT_SAMPLE_RATE)
sd.wait()

except KeyboardInterrupt:
print("\n\n再见!")

关键参数调整

  • vad.threshold=0.5:VAD 检测阈值,值越高越不容易误触发

  • vad.silence_duration=0.8:静音持续时间,单位秒

  • speaker_id:0-9 号内置说话人,不同 ID 对应不同音色

6.4 图像理解任务

MiniMind-O 支持图像输入,可以回答关于图像内容的问题。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from PIL import Image
from model.minimind_omni import MiniMindOmni

# 加载模型
model = MiniMindOmni.from_pretrained('./out/sft_omni')

# 加载图像
image = Image.open('example.jpg').convert('RGB')

# 提问关于图像的问题
question = "这张图片里有什么?"

print(f"问题: {question}")
print("回答: ", end='', flush=True)

for chunk in model.generate(text=question, image=image, stream_audio=False):
if chunk['text']:
print(chunk['text'], end='', flush=True)
print()

支持的图像任务

  • 物体识别:"这是什么?"

  • 场景描述:"描述一下这张图片"

  • 文字识别:"图片里写了什么?"

  • 简单计数:"图片里有几个人?"

  • 颜色识别:"这个物体是什么颜色的?"

注意事项

  • 图像分辨率建议调整为 384x384

  • 目前只能理解主体内容,细节识别能力有限

  • 不支持复杂的逻辑推理和数学计算

6.5 多模态组合任务

MiniMind-O 最强大的地方在于支持任意模态组合的输入

示例 1:语音提问 + 图像回答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 先录音提问
audio = vad.record_until_silence()

# 加载图像
image = Image.open('cat.jpg')

# 生成语音回答
for chunk in model.generate(audio=audio, image=image):
if chunk['text']:
print(chunk['text'], end='', flush=True)
if chunk['audio'] is not None:
# 边生成边播放
sd.play(chunk['audio'], OUTPUT_SAMPLE_RATE)
sd.wait()

示例 2:文本提问 + 图像 + 语音回答

1
2
3
4
5
6
7
8
9
10
11
response = model.generate(
text="请用语音描述这张图片",
image=Image.open('landscape.jpg'),
stream_text=False
)

# 只播放音频
for chunk in response:
if chunk['audio'] is not None:
sd.play(chunk['audio'], OUTPUT_SAMPLE_RATE)
sd.wait()

6.6 零样例音色克隆

MiniMind-O 支持零样例音色克隆,只需提供一段 3-10 秒的参考音频。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import librosa
import soundfile as sf
import numpy as np
import sounddevice as sd
from model.minimind_omni import MiniMindOmni
from utils.speaker_encoder import SpeakerEncoder

# 加载模型
model = MiniMindOmni.from_pretrained('./out/sft_omni')
speaker_encoder = SpeakerEncoder('./pretrained/campplus')

# 加载参考音频
reference_audio, _ = librosa.load('reference_voice.wav', sr=16000)

# 提取说话人嵌入
speaker_embedding = speaker_encoder.encode(reference_audio)

# 使用克隆的音色生成语音
print("正在生成语音...")
audio_buffer = []

for chunk in model.generate(
text="你好,我现在正在使用克隆的音色说话。",
speaker_embedding=speaker_embedding,
stream_text=False
):
if chunk['audio'] is not None:
audio_buffer.append(chunk['audio'])

# 播放结果
full_audio = np.concatenate(audio_buffer)
sd.play(full_audio, 24000)
sd.wait()

# 保存为文件
sf.write('cloned_voice.wav', full_audio, 24000)

最佳实践

  • 参考音频长度:3-10 秒效果最佳

  • 参考音频质量:安静环境、无背景噪音

  • 参考音频内容:包含多种音素的自然对话

  • 生成文本长度:短文本 (≤30 字) 克隆效果最好


七、高级定制与优化

7.1 自定义系统提示词

你可以通过修改系统提示词来改变模型的行为和角色:

1
2
3
4
5
6
7
8
9
10
# 设置系统提示词
model.set_system_prompt("""
你是一个专业的 Python 编程助手。
- 回答要简洁明了,直接给出代码示例
- 代码要包含必要的注释
- 如果有多种实现方式,优先推荐最简单的一种
""")

# 现在模型会以编程助手的身份回答问题
response = model.generate(text="如何在 Python 中读取 CSV 文件?")

7.2 生成参数调优

MiniMind-O 支持多种生成参数调整,以适应不同任务需求:

参数 作用 推荐值范围
temperature 控制随机性,值越高越随机 0.1-1.0
top_p 核采样参数,控制词汇多样性 0.7-0.95
repetition_penalty 重复惩罚,防止模型重复输出 1.0-1.2
max_tokens 最大生成文本长度 128-2048
audio_speed 语音生成速度 0.8-1.2

示例:精确回答模式

1
2
3
4
5
6
response = model.generate(
text="2+2等于几?",
temperature=0.1,
top_p=0.1,
max_tokens=10
)

示例:创意写作模式

1
2
3
4
5
6
7
response = model.generate(
text="写一个关于小猫的短故事",
temperature=0.9,
top_p=0.95,
repetition_penalty=1.1,
max_tokens=512
)

7.3 批量处理任务

对于需要处理大量数据的任务,可以使用批量生成接口提高效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 批量文本生成
prompts = [
"什么是人工智能?",
"解释一下机器学习",
"深度学习和机器学习的区别是什么?"
]

responses = model.batch_generate(
texts=prompts,
max_tokens=256,
temperature=0.7,
batch_size=4
)

for i, response in enumerate(responses):
print(f"问题 {i+1}: {prompts[i]}")
print(f"回答: {response['text']}")
print()

注意:批量生成目前不支持流式输出和音频输出。

7.4 Web 应用集成

你可以轻松将 MiniMind-O 集成到 FastAPI 或 Flask 应用中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import StreamingResponse
import io
import soundfile as sf
from model.minimind_omni import MiniMindOmni

app = FastAPI()
model = MiniMindOmni.from_pretrained('./out/sft_omni')

@app.post("/api/text-to-speech")
async def text_to_speech(text: str):
def generate_audio():
for chunk in model.generate(text=text, stream_text=False):
if chunk['audio'] is not None:
# 将 numpy 数组转换为 WAV 格式字节流
buffer = io.BytesIO()
sf.write(buffer, chunk['audio'], 24000, format='WAV')
buffer.seek(0)
yield buffer.read()

return StreamingResponse(generate_audio(), media_type="audio/wav")

@app.post("/api/chat")
async def chat(text: str):
full_text = ""
for chunk in model.generate(text=text, stream_audio=False):
if chunk['text']:
full_text += chunk['text']
return {"response": full_text}

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

八、完整桌面语音助手实现

8.1 环境依赖

1
pip install torch sounddevice soundfile librosa pillow numpy webrtcvad

8.2 完整源码

保存为 voice_assistant.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import numpy as np
import sounddevice as sd
import soundfile as sf
import librosa
import webrtcvad
import time
import sys
from pathlib import Path

# ===================== 配置区 =====================
SAMPLE_RATE = 16000 # 录音采样率(SenseVoice要求)
OUT_SR = 24000 # 模型输出音频采样率(Mimi要求)
FRAME_DURATION = 30 # VAD帧时长(ms),只能是10/20/30
VAD_MODE = 3 # VAD灵敏度(0-3),3最灵敏
SILENCE_THRESHOLD = 0.8 # 静音持续多久停止录音(秒)
PRE_SPEECH_BUFFER = 0.3 # 保留说话前的音频(秒)
MODEL_PATH = "./out/sft_omni" # 模型权重路径
SPEAKER_ID = 2 # 默认说话人ID(0-9)
TEMPERATURE = 0.7 # 生成温度
MAX_TOKENS = 256 # 最大生成长度
# ==================================================

# 全局变量
is_running = True
audio_buffer = []
vad = None
model = None
speaker_encoder = None

class VADDetector:
"""基于webrtcvad的语音活动检测器"""
def __init__(self, sample_rate=16000, frame_duration=30, mode=3):
self.sample_rate = sample_rate
self.frame_duration = frame_duration
self.frame_size = int(sample_rate * frame_duration / 1000)
self.vad = webrtcvad.Vad(mode)

def is_speech(self, frame):
"""检测单帧是否为语音"""
if len(frame) != self.frame_size:
return False
# 转换为16位PCM格式
frame_int16 = (frame * 32767).astype(np.int16).tobytes()
return self.vad.is_speech(frame_int16, self.sample_rate)

def record_until_silence(self, silence_threshold=0.8, pre_speech_buffer=0.3):
"""录音直到检测到指定时长的静音"""
global audio_buffer
audio_buffer = []
pre_buffer = []
speech_detected = False
silence_frames = 0
max_silence_frames = int(silence_threshold * 1000 / self.frame_duration)
pre_buffer_frames = int(pre_speech_buffer * 1000 / self.frame_duration)

def audio_callback(indata, frames, time, status):
if status:
print(f"音频状态错误: {status}", file=sys.stderr)
audio_buffer.append(indata.copy())

print("\n🎤 正在听你说话... (说完自动停止)")
with sd.InputStream(samplerate=self.sample_rate, channels=1,
blocksize=self.frame_size, callback=audio_callback):
while is_running:
if len(audio_buffer) == 0:
time.sleep(0.01)
continue

frame = audio_buffer.pop(0).flatten()
is_speech = self.is_speech(frame)

# 维护预缓冲
pre_buffer.append(frame)
if len(pre_buffer) > pre_buffer_frames:
pre_buffer.pop(0)

if is_speech:
speech_detected = True
silence_frames = 0
elif speech_detected:
silence_frames += 1
if silence_frames >= max_silence_frames:
break
time.sleep(0.001)

# 合并音频
full_audio = np.concatenate(pre_buffer + audio_buffer)
print(f"✅ 录音结束,时长: {len(full_audio)/self.sample_rate:.2f}秒")
return full_audio

def load_models():
"""加载所有必要的模型"""
global model, speaker_encoder

print("🔄 正在加载MiniMind-O模型...")
try:
from model.minimind_omni import MiniMindOmni
model = MiniMindOmni.from_pretrained(MODEL_PATH)

# 启用半精度加速
if torch.cuda.is_available():
model = model.half().cuda()
print("✅ 使用GPU加速 (CUDA)")
else:
model = model.float()
print("⚠️ 使用CPU推理,速度会较慢")

from utils.speaker_encoder import SpeakerEncoder
speaker_encoder = SpeakerEncoder('./pretrained/campplus')
print("✅ 所有模型加载完成!")
return True
except Exception as e:
print(f"❌ 模型加载失败: {e}")
print("\n请检查:")
print("1. 模型权重是否已下载到 ./out/sft_omni")
print("2. 所有依赖是否已正确安装")
print("3. CUDA是否可用(如果使用GPU)")
return False

def play_audio(audio_data, sample_rate):
"""播放音频数据"""
sd.play(audio_data, sample_rate)
sd.wait()

def main():
"""主程序入口"""
global is_running

print("="*50)
print(" MiniMind-O 桌面语音助手")
print("="*50)
print("使用说明:")
print("- 说话即可开始对话,说完自动停止录音")
print("- 输入 'exit' 或按 Ctrl+C 退出程序")
print("- 输入 'speaker N' 切换说话人(0-9)")
print("- 输入 'temp N' 调整生成温度(0.1-1.0)")
print("="*50)

# 加载模型
if not load_models():
return

# 初始化VAD
vad_detector = VADDetector(SAMPLE_RATE, FRAME_DURATION, VAD_MODE)

print("\n🎉 语音助手已就绪!")

try:
while is_running:
# 检查用户输入
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
user_input = sys.stdin.readline().strip()
if user_input.lower() in ['exit', 'quit', 'q']:
break
elif user_input.lower().startswith('speaker '):
try:
global SPEAKER_ID
SPEAKER_ID = int(user_input.split()[1])
print(f"✅ 已切换到说话人 {SPEAKER_ID}")
except:
print("❌ 无效的说话人ID,请输入 0-9")
continue
elif user_input.lower().startswith('temp '):
try:
global TEMPERATURE
TEMPERATURE = float(user_input.split()[1])
print(f"✅ 已设置生成温度为 {TEMPERATURE}")
except:
print("❌ 无效的温度值,请输入 0.1-1.0")
continue

# 录音
audio = vad_detector.record_until_silence(SILENCE_THRESHOLD, PRE_SPEECH_BUFFER)

# 生成回复
print("\n🤖 正在思考...")
start_time = time.time()
full_text = ""
audio_chunks = []
first_token_time = None
first_audio_time = None

for chunk in model.generate(
audio=audio,
max_tokens=MAX_TOKENS,
temperature=TEMPERATURE,
speaker_id=SPEAKER_ID,
stream_text=True,
stream_audio=True
):
if chunk['text'] and first_token_time is None:
first_token_time = time.time() - start_time
print(f"\n⏱️ 首字延迟: {first_token_time*1000:.0f}ms")
print("\nMiniMind-O: ", end='', flush=True)

if chunk['text']:
print(chunk['text'], end='', flush=True)
full_text += chunk['text']

if chunk['audio'] is not None and first_audio_time is None:
first_audio_time = time.time() - start_time
print(f"\n⏱️ 首音延迟: {first_audio_time*1000:.0f}ms")

if chunk['audio'] is not None:
audio_chunks.append(chunk['audio'])

# 播放完整音频
if audio_chunks:
full_audio = np.concatenate(audio_chunks)
print("\n\n🔊 正在播放回复...")
play_audio(full_audio, OUT_SR)

print("\n" + "-"*50)

except KeyboardInterrupt:
print("\n\n👋 检测到Ctrl+C,正在退出...")
except Exception as e:
print(f"\n❌ 程序出错: {e}")
finally:
is_running = False
print("\n👋 再见!")

if __name__ == "__main__":
import select
import torch
main()

8.3 目录结构检查

确保你的项目目录结构如下:

Text
1
2
3
4
5
6
7
8
9
10
minimind-o/
├── model/
├── utils/
├── pretrained/
│ ├── sensevoice/
│ ├── mimi/
│ └── campplus/
├── out/
│ └── sft_omni/
└── voice_assistant.py

8.4 运行与使用

1
python voice_assistant.py

核心功能

  • ✅ 自动语音检测和录音

  • ✅ 端到端语音输入→语音输出

  • ✅ 实时流式文本显示

  • ✅ 实时流式音频播放

  • ✅ 首字 / 首音延迟统计

  • ✅ 多轮对话支持

交互命令

  • exit / quit / q:退出程序

  • speaker N:切换说话人(N 为 0-9 的数字)

  • temp N:调整生成温度(N 为 0.1-1.0 的浮点数)

8.5 性能参考

硬件 首字延迟 首音延迟 实时率
RTX 3090 ~120ms ~240ms ~0.3x
RTX 4060 ~150ms ~280ms ~0.4x
CPU (i7-12700) ~500ms ~800ms ~1.5x

九、常见问题与故障排除

9.1 音频相关问题

问题 1:语音生成有杂音或断音

  • 解决方案:降低 batch_size,确保 GPU 显存充足

  • 检查 CUDA 是否正常工作:torch.cuda.is_available()

问题 2:语音识别不准确

  • 解决方案:确保输入音频是 16kHz 单声道

  • 降低环境噪音,使用高质量麦克风

  • 调整 VAD 阈值:vad.threshold=0.6

问题 3:音色克隆效果差

  • 解决方案:使用更长的参考音频 (5-10 秒)

  • 确保参考音频中只有一个说话人

  • 避免参考音频中有背景噪音

问题 4:没有声音输出:检查系统默认音频设备,确保 sounddevice 能正确识别
问题 5:录音没有声音:检查麦克风权限和默认输入设备

9.2 性能优化问题

问题 1:推理速度慢

  • 解决方案:使用 GPU 加速,确保安装了正确版本的 PyTorch

  • 启用半精度推理:model = model.half()

  • 降低 max_tokens 长度

问题 2:显存不足

  • 解决方案:使用 CPU 推理:model = model.cpu()

  • 启用梯度检查点:model.gradient_checkpointing_enable()

  • 降低批量大小

9.3 模型加载问题

  • 找不到模型文件:检查路径是否正确,确保权重已完整下载

  • CUDA out of memory:关闭其他占用 GPU 的程序,或使用 CPU 推理


十、应用场景与局限性

10.1 推荐应用场景

  1. 个人语音助手:桌面端语音助手、智能家居语音控制、车载语音交互

  2. 教育工具:语言学习发音练习、儿童故事朗读、问答式学习助手

  3. 内容创作:文本转语音生成、有声书制作、播客内容生成

  4. 辅助工具:图像描述生成、语音笔记转写、简单翻译助手

10.2 当前局限性

  1. 中长英文语音不稳定:16-30 词段容易出现漏词、发音漂移、节奏异常

  2. 音色克隆效果不均:不同参考音频的效果差异很大,余弦相似度在 0.43-0.70 之间

  3. 视觉路径较弱:只用了 64 个 Image Token,只能理解主体场景,细节属性不可靠

  4. 打断机制简单:只是 VAD 阈值检测,不是语义级别的全双工

  5. MoE 版本优势不明显:参数多但 active scale 与 Dense 版接近,不是等计算量下的更优解

  6. 不支持多轮语音上下文:目前每轮对话都是独立的

  7. 不支持视频输入输出:未来版本可能会支持

10.3 未来方向

  • 优化 Talker 架构,提升中长语音生成质量

  • 改进视觉编码器,增加 Image Token 数量

  • 实现语义级别的全双工交互

  • 探索更高效的 MoE 设计

  • 支持视频输入输出


核心知识点速览

  • MiniMind-O 是史上最小完整 Omni 模型,主干仅0.1B 参数,支持三模输入和流式语音输出,全链路开源

  • 采用Thinker-Talker 双路径架构,分离语义规划与声学渲染,解决传统级联方案的信息丢失问题

  • Bridge 层使用 Thinker 中间层状态进行语义桥接,平衡语义信息与输出质量,避免过特化

  • 训练分为T2A、A2A、I2T 三阶段,4 卡 3090 仅需 4 小时即可完成全量训练,单卡 2 小时可跑 mini 数据集

  • 支持零样例音色克隆,仅需 3-10 秒参考音频即可克隆说话人音色,无需重新训练

  • 单卡 RTX 3090 上首字延迟约 140ms,首音延迟约 260ms,支持实时语音交互

  • 提供完整可运行的桌面语音助手,支持自动 VAD 录音、流式生成与播放,可直接本地部署

  • 支持自定义系统提示词、生成参数调整,可适配问答、创作、编程助手等不同任务需求

  • 适合简单短文本多模态交互任务,不适合复杂推理和长文本生成,中长语音生成存在稳定性局限

  • 可在个人电脑上完成从训练到部署的全流程体验,是学习 Omni 模型的绝佳入门案例