VoiceClone 让 AI Agent 开口说话

通过 MiMo TTS VoiceClone,让任意 AI 助手具备语音克隆能力,将文本转化为自然语音并发送。


🔧 前置准备

1. 获取API Key

  • 访问小米 MiMo 开放平台申请 API Key
  • 当前使用的 API 地址: https://token-plan-cn.xiaomimimo.com/v1

2. 准备参考音频

  • 格式:MP3 或 WAV
  • 建议:16kHz 采样率,64kbps 码率(约 80KB)
  • 内容:清晰的说话音频,10-30 秒为宜
  • 优化:文件越小,API 响应越快
# 转换参考音频为优化格式
ffmpeg -y -i original.wav -ar 16000 -ac 1 -b:a 64k optimized.mp3

3. 环境依赖

  • Python 3.x
  • ffmpeg(用于音频格式转换)

📝 核心脚本

保存为 mimo_tts.py:

#!/usr/bin/env python3
"""
MiMo TTS Provider
Supports: mimo-v2.5-tts (preset), mimo-v2.5-tts-voicedesign (description), mimo-v2.5-tts-voiceclone (sample)
"""
import argparse, base64, json, os, subprocess, sys, tempfile, urllib.request, urllib.error

# 参考音频路径(修改为你的路径)
REF_VOICE_PATH = os.path.expanduser("~/reference_voice.mp3")

def get_api_url():
    url = os.environ.get("MIMO_TTS_URL")
    if url:
        return url
    base = os.environ.get("XIAOMI_BASE_URL", "https://token-plan-cn.xiaomimimo.com/v1")
    return base + "/chat/completions"

def get_api_key():
    # 优先从环境变量读取
    key = os.environ.get("XIAOMI_API_KEY")
    if key and key != "***":
        return key
    # 或者从 .env 文件读取
    env_path = os.path.expanduser("~/.env")
    if os.path.exists(env_path):
        with open(env_path) as f:
            for line in f:
                line = line.strip()
                if line.startswith("#"):
                    continue
                parts = line.split("=", 1)
                if len(parts) == 2 and parts[0].strip() == "XIAOMI_API_KEY":
                    val = parts[1].strip().strip('"').strip("'")
                    if val and val != "***":
                        return val
    return None

_ref_voice_cache = None

def load_ref_voice():
    """Load and base64-encode the reference voice file (cached in memory)."""
    global _ref_voice_cache
    if _ref_voice_cache is not None:
        return _ref_voice_cache
    if not os.path.exists(REF_VOICE_PATH):
        return None
    with open(REF_VOICE_PATH, "rb") as f:
        _ref_voice_cache = base64.b64encode(f.read()).decode("utf-8")
    return _ref_voice_cache

def call_mimo_tts(text, model="mimo-v2.5-tts-voiceclone", voice=None,
                   style_instruction=None, api_key=None, api_url=None):
    if not api_key:
        raise ValueError("XIAOMI_API_KEY not set")
    if not api_url:
        api_url = get_api_url()

    messages = []

    if model == "mimo-v2.5-tts-voiceclone":
        ref_b64 = load_ref_voice()
        if not ref_b64:
            raise RuntimeError(f"Reference voice not found: {REF_VOICE_PATH}")
        # 根据文件扩展名设置 MIME 类型
        ext = os.path.splitext(REF_VOICE_PATH)[1].lower()
        mime = "audio/mp3" if ext == ".mp3" else "audio/wav"
        voice_data = f"data:{mime};base64,{ref_b64}"
        if style_instruction:
            messages.append({"role": "user", "content": style_instruction})
        else:
            messages.append({"role": "user", "content": ""})
    elif model == "mimo-v2.5-tts-voicedesign":
        voice_data = None
        voice_desc = style_instruction or "一个年轻女孩的声音,音色柔和温润,语气轻松自在,自然放松"
        messages.append({"role": "user", "content": voice_desc})
    else:
        voice_data = voice or os.environ.get("MIMO_TTS_VOICE", "冰糖")

    messages.append({"role": "assistant", "content": text})

    audio_cfg = {"format": "wav"}
    if model == "mimo-v2.5-tts-voiceclone":
        audio_cfg["voice"] = voice_data
    elif model == "mimo-v2.5-tts":
        audio_cfg["voice"] = voice_data

    payload = {"model": model, "messages": messages, "audio": audio_cfg}

    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(api_url, data=data,
        headers={"Content-Type": "application/json", "api-key": api_key}, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"MiMo TTS API error {e.code}: {body}") from e
    except urllib.error.URLError as e:
        raise RuntimeError(f"MiMo TTS connection error: {e.reason}") from e

    try:
        audio_b64 = result["choices"][0]["message"]["audio"]["data"]
    except (KeyError, IndexError, TypeError) as e:
        raise RuntimeError(f"Unexpected API response") from e

    return base64.b64decode(audio_b64)

def wav_to_ogg_opus(wav_bytes):
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
        f.write(wav_bytes)
        wav_path = f.name
    ogg_path = wav_path.replace(".wav", ".ogg")
    try:
        r = subprocess.run(["ffmpeg", "-y", "-i", wav_path, "-c:a", "libopus", "-b:a", "64k", ogg_path],
                          capture_output=True, text=True, timeout=30)
        if r.returncode != 0:
            raise RuntimeError(f"ffmpeg error: {r.stderr}")
        with open(ogg_path, "rb") as f:
            return f.read()
    finally:
        for p in (wav_path, ogg_path):
            try: os.unlink(p)
            except: pass

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", "-i", required=True)
    parser.add_argument("--output", "-o", required=True)
    parser.add_argument("--voice", "-v", default=None)
    parser.add_argument("--style", "-s", default=None)
    parser.add_argument("--model", "-m", default=None)
    parser.add_argument("--format", "-f", default=None)
    args = parser.parse_args()

    with open(args.input, "r", encoding="utf-8") as f:
        text = f.read().strip()
    if not text:
        print("Error: empty text", file=sys.stderr)
        sys.exit(1)

    model = args.model or os.environ.get("MIMO_TTS_MODEL", "mimo-v2.5-tts-voiceclone")
    voice = args.voice or os.environ.get("MIMO_TTS_VOICE", "冰糖")
    api_key = get_api_key()
    if not api_key:
        print("Error: XIAOMI_API_KEY not found", file=sys.stderr)
        sys.exit(1)

    output_path = args.output
    output_format = args.format
    if not output_format:
        ext = os.path.splitext(output_path)[1].lower().lstrip(".")
        output_format = ext if ext in ("wav", "mp3", "ogg") else "ogg"

    try:
        wav_bytes = call_mimo_tts(text=text, model=model, voice=voice,
                                   style_instruction=args.style, api_key=api_key, api_url=get_api_url())
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

    if output_format == "ogg":
        audio_bytes = wav_to_ogg_opus(wav_bytes)
    else:
        audio_bytes = wav_bytes

    os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
    with open(output_path, "wb") as f:
        f.write(audio_bytes)
    print(f"OK: {len(audio_bytes)} bytes -> {output_path}")

if __name__ == "__main__":
    main()

🚀 使用方法

命令行调用

# 基本用法
python3 mimo_tts.py --input text.txt --output output.ogg

# 指定模型
python3 mimo_tts.py --input text.txt --output output.ogg --model mimo-v2.5-tts-voiceclone

# 添加风格指令
python3 mimo_tts.py --input text.txt --output output.ogg --style "语气轻松自然"

环境变量配置

export XIAOMI_API_KEY="your-api-key-here"
export XIAOMI_BASE_URL="https://token-plan-cn.xiaomimimo.com/v1"

📌 关键注意事项

1. API 地址

当前使用:https://token-plan-cn.xiaomimimo.com/v1 如有变动,设置环境变量 XIAOMI_BASE_URL 覆盖

2. 认证方式

  • 支持两种方式:
    • Header:api-key: YOUR_KEY
    • Header:Authorization: Bearer YOUR_KEY

3. 参考音频要求

  • VoiceClone 模式必须提供参考音频
  • 音频会被 base64 编码后上传(每次调用都上传)
  • 文件越大,API 响应越慢

4. 音频格式 MIME 类型

# MP3 文件
voice_data = f"data:audio/mp3;base64,{ref_b64}"

# WAV 文件
voice_data = f"data:audio/wav;base64,{ref_b64}"

5. 响应格式

{
  "choices": [{
    "message": {
      "audio": {
        "data": "base64-encoded-audio..."
      }
    }
  }]
}

6. 输出格式

  • API 返回 WAV 格式
  • 如需 OGG(用于 Telegram 语音),用 ffmpeg 转换:
ffmpeg -y -i input.wav -c:a libopus -b:a 64k output.ogg

🎯 MiMo TTS 三种模型

模型用途特点
mimo-v2.5-tts预设声音冰糖/茉莉/苏打/白桦,支持唱歌
mimo-v2.5-tts-voicedesign描述生成声音每次生成不同声音,非确定性
mimo-v2.5-tts-voiceclone克隆声音从参考音频克隆,声音一致性好

📱 平台兼容性

Telegram

  • 支持语音气泡:将生成的 OGG 语音直接发送,Telegram 会以语音气泡形式呈现
  • 这是目前体验最完整的平台,适合做 AI 助手的语音回复

其他平台

  • 微信:目前只能以文件/消息形式发送,不支持原生语音气泡播放
  • 钉钉 / 飞书 / 企业微信:同样不支持原生语音消息,只能作为音频文件转发
  • 通用方案:所有平台都可通过上传 OGG/WAV 文件实现语音播放,只是不在聊天界面内显示为"语音气泡"

如果你主要用 Telegram,这套方案的体验最接近原生语音助手。


⚠️ 常见问题

Q:声音每次都不太一样?

A:VoiceClone 是概率模型,每次生成会有细微差异,但整体音色保持一致。

Q:API 响应很慢?

A:主要瓶颈在网络和参考音频大小。优化参考音频(MP3 16kHz 64kbps)可以显著提速。

Q:如何改善克隆效果?

A:

  • 使用高质量参考音频(清晰、无噪音)
  • 参考音频时长 10-30 秒为宜
  • 避免过度压缩

Q:中文支持如何?

A:MiMo 支持中文,效果不错。这是选择它的重要原因之一。


🔄 备选方案(支持中文)

方案价格中文效果特点
Fish Audio~0.1-0.2元/条⭐⭐⭐⭐⭐国内平台,中文最专业
ElevenLabs免费10000字/月⭐⭐⭐⭐国际标杆
MiniMax按字符计费⭐⭐⭐⭐国内大厂
本地部署(GPT-SoVITS)免费(需GPU)⭐⭐⭐⭐⭐效果最好,需要 8GB+ 显存

文档版本:v1.0
最后更新:2026-06-25
适用模型:MiMo V2.5 TTS