tags:
- OpenMAIC
- 语音合成
- MiMo
- TTS
- 工具使用
date: 2026-09-10
OpenMAIC 接入小米 MiMo TTS 语音合成
记录给 OpenMAIC 接入小米 MiMo-V2.5-TTS 的完整过程:协议调研 → 架构定位 → 代码改造 → 配置启用 → 验证与限制。
一、背景与目标
- OpenMAIC 内置 TTS 服务商为 OpenAI / Azure / GLM / Qwen / Doubao / MiniMax / ElevenLabs / VoxCPM2 / Lemonade / 浏览器原生,没有小米 MiMo。
- OpenMAIC 提供"添加自定义语音合成"入口,但该入口只实现 OpenAI 兼容的
POST {baseUrl}/audio/speech协议。 - 小米 MiMo TTS 表面上是"OpenAI 兼容",实际走的是 chat/completions + audio 参数 ,并非
/audio/speech,因此光靠界面配置无法接入,必须写原生 provider。
目标:让 OpenMAIC 里出现 "Xiaomi MiMo TTS",可选 mimo-v2.5-tts 模型与官方 9 个预置音色,并能在设置页试听、在课堂生成中作为语音讲解使用。
二、MiMo TTS 接口协议调研
来源:https://mimo.mi.com/docs (MiMo-TTS 系列 - OpenAI API 兼容)
2.1 请求
POST https://api.xiaomimimo.com/v1/chat/completions
api-key: $MIMO_API_KEY
Content-Type: application/json
json
{
"model": "mimo-v2.5-tts",
"messages": [
{ "role": "assistant", "content": "要朗读的文本" }
],
"audio": { "format": "wav", "voice": "mimo_default" },
"stream": false
}
关键点:
| 项 | 说明 |
|---|---|
| 待合成文本位置 | 必须在 role: "assistant" 的 message 中,不能放 user |
user 消息 |
可选,用于传入自然语言风格/语气指令(不朗读其内容);voicedesign 模型必填 |
| 认证 | 官方示例用 api-key 头;用 OpenAI SDK(传 api_key)时实际发的是 Authorization: Bearer,两种都可用 |
| 音频参数 | audio.format 支持 wav / mp3 / pcm / pcm16;audio.voice 传预置音色 ID |
| 流式 | stream: true 时输出 pcm16,自行拼接;非流式直接拿完整 base64 |
2.2 响应
json
{
"choices": [
{
"message": {
"role": "assistant",
"audio": { "id": "...", "data": "base64Data" }
}
}
]
}
音频为 base64 ,位于 choices[0].message.audio.data,需自行解码成字节。
2.3 模型与预置音色
| Model ID | 用途 |
|---|---|
mimo-v2.5-tts |
预置精品音色(本次接入的模型) |
mimo-v2.5-tts-voicedesign |
用文本描述设计音色 |
mimo-v2.5-tts-voiceclone |
用音频样本复刻音色 |
mimo-v2.5-tts 预置音色:
| 音色名 | Voice ID | 语言 | 性别 |
|---|---|---|---|
| MiMo 默认 | mimo_default |
视集群(中国集群=冰糖,其他=Mia) | - |
| 冰糖 | 冰糖 |
中文 | 女 |
| 茉莉 | 茉莉 |
中文 | 女 |
| 苏打 | 苏打 |
中文 | 男 |
| 白桦 | 白桦 |
中文 | 男 |
| Mia | Mia |
英文 | 女 |
| Chloe | Chloe |
英文 | 女 |
| Milo | Milo |
英文 | 男 |
| Dean | Dean |
英文 | 男 |
!note 中文音色的 Voice ID 就是中文词本身(如
冰糖),这是官方定义,不是笔误。
2.4 风格控制方式(后续可用)
- 自然语言控制:写在
user消息里,例如"用轻快上扬的语调朗读,语速稍快"。 - 标签控制:写在
assistant文本开头或句中,例如(磁性)...、(东北话)...、(唱歌)歌词、(叹气)、(语速加快)。 - 因此 MiMo 没有独立的语速数值参数,语速/情绪都通过文本指令表达(见第五节限速映射实现)。
三、OpenMAIC 的 TTS 架构速览
接入前必须知道这几层:
| 层 | 文件 | 作用 |
|---|---|---|
| 类型定义 | lib/audio/types.ts |
BuiltInTTSProviderId 联合类型 + TTSProviderConfig 结构 |
| 服务商注册表 | lib/audio/constants.ts |
TTS_PROVIDERS、DEFAULT_TTS_VOICES、DEFAULT_TTS_MODELS |
| 合成实现 | lib/audio/tts-providers.ts |
generateTTS() switch 分发到各服务商实现 |
| 服务端配置 | lib/server/provider-config.ts |
环境变量前缀 → provider id 映射、key/baseUrl/model 解析、禁用开关 |
| 设置存储 | lib/store/settings.ts |
ttsProvidersConfig 默认条目(apiKey/baseUrl/modelId/enabled) |
| 显示名 | lib/audio/provider-display.ts + lib/i18n/locales/*.json |
provider 多语言名称 |
| 设置界面 | components/settings/tts-settings.tsx |
服务商列表、模型/音色选择、试听、请求地址提示 |
| 请求链路 | app/api/generate/tts/route.ts |
客户端配置 → resolveTTSApiKey/BaseUrl/Model → generateTTS() |
一次语音合成的实际流程:
- 客户端把
ttsProviderId / ttsVoice / ttsModelId / ttsApiKey / ttsBaseUrl发给/api/generate/tts。 - 服务端判断该 provider 是否"服务端托管"(env/YAML 配了 key),托管则忽略客户端 key。
resolveTTSApiKey()取 key,resolveTTSBaseUrl()取 baseUrl(没有则回退注册表defaultBaseUrl),resolveTTSModel()取模型。generateTTS(config, text)按 provider 分发到具体实现,返回{ audio: Uint8Array, format }。
四、接入步骤(逐文件)
4.1 类型:新增 provider id
lib/audio/types.ts:
ts
export type BuiltInTTSProviderId =
| 'openai-tts'
| 'azure-tts'
| 'glm-tts'
| 'qwen-tts'
| 'voxcpm-tts'
| 'doubao-tts'
| 'elevenlabs-tts'
| 'minimax-tts'
| 'xiaomi-tts' // 新增
| 'lemonade-tts'
| 'browser-native-tts';
4.2 注册表:模型与音色
lib/audio/constants.ts 的 TTS_PROVIDERS 中新增:
ts
'xiaomi-tts': {
id: 'xiaomi-tts',
name: 'Xiaomi MiMo TTS',
requiresApiKey: true,
defaultBaseUrl: 'https://api.xiaomimimo.com/v1',
icon: '/logos/xiaomi.svg',
models: [{ id: 'mimo-v2.5-tts', name: 'MiMo-V2.5-TTS' }],
defaultModelId: 'mimo-v2.5-tts',
voices: [
{ id: 'mimo_default', name: 'MiMo 默认', language: 'zh-CN', gender: 'neutral' },
{ id: '冰糖', name: '冰糖', language: 'zh-CN', gender: 'female' },
{ id: '茉莉', name: '茉莉', language: 'zh-CN', gender: 'female' },
{ id: '苏打', name: '苏打', language: 'zh-CN', gender: 'male' },
{ id: '白桦', name: '白桦', language: 'zh-CN', gender: 'male' },
{ id: 'Mia', name: 'Mia', language: 'en-US', gender: 'female' },
{ id: 'Chloe', name: 'Chloe', language: 'en-US', gender: 'female' },
{ id: 'Milo', name: 'Milo', language: 'en-US', gender: 'male' },
{ id: 'Dean', name: 'Dean', language: 'en-US', gender: 'male' },
],
supportedFormats: ['wav', 'mp3', 'pcm'],
speedRange: { min: 0.5, max: 2.0, default: 1.0 },
},
同时补两个 Record<BuiltInTTSProviderId, string> 映射(缺一个就会类型报错):
ts
export const DEFAULT_TTS_VOICES = { /* ... */ 'xiaomi-tts': 'mimo_default' };
export const DEFAULT_TTS_MODELS = { /* ... */ 'xiaomi-tts': 'mimo-v2.5-tts' };
4.3 合成实现:chat/completions + base64 解码
lib/audio/tts-providers.ts 的 switch 增加分支,并实现:
ts
case 'xiaomi-tts':
return await generateXiaomiMiMoTTS(config, text, signal);
ts
async function generateXiaomiMiMoTTS(
config: TTSModelConfig,
text: string,
signal: AbortSignal,
): Promise<TTSGenerationResult> {
const baseUrl = (config.baseUrl || TTS_PROVIDERS['xiaomi-tts'].defaultBaseUrl || '').replace(
/\/$/,
'',
);
if (!baseUrl) throw new Error('Xiaomi MiMo TTS base URL is required');
const apiKey = config.apiKey?.trim();
if (!apiKey) throw new Error('API key required for Xiaomi MiMo TTS');
const format = config.format || 'wav';
const voice = config.voice?.trim() || 'mimo_default';
const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
const speed = config.speed && config.speed !== 1 ? config.speed : undefined;
if (speed) {
// MiMo 没有数值语速参数,用自然语言 user 指令表达
const factor = Math.round(speed * 100) / 100;
messages.push({
role: 'user',
content:
factor < 1
? `请用更慢的语速朗读,约为原来的 ${factor} 倍。`
: `请用更快的语速朗读,约为原来的 ${factor} 倍。`,
});
}
messages.push({ role: 'assistant', content: text });
const response = await fetch(`${baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'api-key': apiKey, // 官方文档写法
Authorization: `Bearer ${apiKey}`, // OpenAI SDK 风格,兼容两种 key
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
model: config.modelId || TTS_PROVIDERS['xiaomi-tts'].defaultModelId,
messages,
audio: { format, voice },
stream: false,
}),
signal,
});
if (!response.ok) {
throwIfTtsRateLimited('Xiaomi MiMo', response.status);
throw new Error(`Xiaomi MiMo TTS API error: ${await readTTSApiError(response)}`);
}
const data = (await response.json().catch(() => null)) as {
choices?: Array<{ message?: { audio?: { data?: string } } }>;
} | null;
const audioBase64 = data?.choices?.[0]?.message?.audio?.data;
if (!audioBase64 || typeof audioBase64 !== 'string') {
throw new Error(`Xiaomi MiMo TTS error: no audio returned. Response: ${JSON.stringify(data ?? null)}`);
}
return {
audio: new Uint8Array(Buffer.from(audioBase64, 'base64')),
format,
};
}
4.4 服务端环境变量映射
lib/server/provider-config.ts 的 TTS_ENV_MAP:
ts
TTS_XIAOMI: 'xiaomi-tts',
TTS_MIMO: 'xiaomi-tts',
映射后自动获得:
TTS_XIAOMI_API_KEY/TTS_XIAOMI_BASE_URL/TTS_XIAOMI_MODELSTTS_XIAOMI_ENABLED=false运营级禁用开关- 同名别名
TTS_MIMO_*
4.5 显示名与国际化
lib/audio/provider-display.ts:
ts
const TTS_PROVIDER_NAME_KEYS = {
// ...
'xiaomi-tts': 'settings.providerXiaomiTTS',
};
lib/i18n/locales/*.json(12 个语言文件)在 providerMiniMaxTTS 后新增:
json
"providerXiaomiTTS": "小米 MiMo TTS"
英文等语言用 "Xiaomi MiMo TTS"。改完执行 node scripts/check-i18n-keys.mjs 校验多语言键对齐。
4.6 客户端默认配置
lib/store/settings.ts 的 getDefaultAudioConfig().ttsProvidersConfig(每个内置 provider 都要有条目):
ts
'xiaomi-tts': { apiKey: '', baseUrl: '', modelId: 'mimo-v2.5-tts', enabled: true },
4.7 设置页请求地址提示
components/settings/tts-settings.tsx 的 endpointPath 增加分支,让界面显示正确的请求路径:
ts
case 'xiaomi-tts':
return '/chat/completions';
4.8 环境变量文档
.env.example 的 TTS 段落新增:
env
# Xiaomi MiMo TTS (mimo-v2.5-tts, chat-completions based)
TTS_XIAOMI_API_KEY=
TTS_XIAOMI_BASE_URL=https://api.xiaomimimo.com/v1
# Token Plan 区域端点:
# TTS_XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
# TTS_XIAOMI_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
# TTS_XIAOMI_BASE_URL=https://token-plan-ams.xiaomimimo.com/v1
# TTS_MIMO_* 是 TTS_XIAOMI_* 的别名
五、配置与启用
.env.local:
env
TTS_XIAOMI_API_KEY=你的MiMo密钥
TTS_XIAOMI_BASE_URL=https://api.xiaomimimo.com/v1
使用 Token Plan(tp- 开头密钥)时把 BASE_URL 换成本区域的 token-plan-{cn,sgp,ams}.xiaomimimo.com/v1。
然后:
- 重启
pnpm dev/ 重新pnpm build。 - 打开 设置 → 语音合成。
- 选择 "Xiaomi MiMo TTS",模型
MiMo-V2.5-TTS,选音色(推荐中文用冰糖,英文用Mia)。 - 点试听按钮确认出声;在课堂生成中该 provider 会自动进入可选音色池。
!warning 常见误区
对话模型的
XIAOMI_API_KEY不会自动用于语音合成,必须单独配TTS_XIAOMI_API_KEY。也不要再走"添加自定义语音合成"入口,那个入口打的是
/audio/speech,MiMo 不支持。
六、验证清单
| 验证项 | 命令 / 操作 | 期望结果 |
|---|---|---|
| 类型检查 | pnpm exec tsc --noEmit -p tsconfig.json |
0 错误 |
| 多语言键 | node scripts/check-i18n-keys.mjs |
key alignment check passed |
| 服务商识别 | 启动后访问 /api/server-providers |
配置 key 后出现 xiaomi-tts |
| 界面 | 设置 → 语音合成 | 出现 Xiaomi MiMo TTS 与 9 个音色 |
| 试听 | 设置页试听按钮 | 正常出声,无 401/500 |
| 生产构建 | pnpm build |
编译 + 类型检查通过 |
排查要点:
- 返回
MISSING_API_KEY→.env.local里没配TTS_XIAOMI_API_KEY,或改了没重启。 - 返回 401/403 → key 类型与 baseUrl 不匹配(按量付费 sk- 配
api.xiaomimimo.com;Token Plan tp- 配区域端点)。 - 返回 "no audio returned" → 模型名写错,或把待合成文本放进了 user 消息。
- 想直连排查协议,可用官方 Python/OpenAI SDK 按第二节的请求体先验证 key 是否可用。
七、已知限制与后续扩展
- 只接入了预置音色模型
mimo-v2.5-tts;voicedesign(文本设计音色)与voiceclone(音色复刻)未暴露到界面。 - 语速用自然语言 user 指令近似映射(0.5--2.0 倍),并非官方数值参数。
- 风格/情绪 目前依赖文本标签:需要在朗读文本里自带
(磁性)、(东北话)这类标签,或在user消息里写风格描述。 - 单说话人模型;多人对话场景由 OpenMAIC 逐条文本分别合成。
如需继续扩展:
- voicedesign :注册第二个模型,把"音色描述"作为
user消息内容、不传audio.voice,可在 providerOptions 里增加stylePrompt字段,并在设置页做输入框。 - voiceclone :
audio.voice传data:audio/wav;base64,<样本>(样本 ≤ 10MB,支持 mp3/wav),需要在设置页增加样本上传与 base64 编码,并注意不要把样本写进日志。
八、经验总结
- 先核对协议再动手 :很多"OpenAI 兼容"的语音接口其实走 chat/completions 的 audio 通道,通用
/audio/speech适配器一定失败。 - 认证头两手准备 :官方文档用
api-key,OpenAI SDK 发Authorization: Bearer,两个都带上最省事(实测互不冲突)。 - 默认输出 wav:非流式场景下 wav 最省事,不需要自己拼 PCM。
- 改 provider 是"多点同步":类型、注册表、默认值映射、实现、env 映射、显示名、i18n、客户端默认配置,漏一处就编译不过或界面不显示。
- 编译期与运行期分开 :
NEXT_PUBLIC_*之外的服务端 Key 放.env.local即可,但改完必须重启进程。 - 接入完成必做验证:类型检查 + i18n 校验 + 界面试听 + 真实合成,四步都过才算完成。