summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorntzyz <i@ntzyz.io>2026-07-07 11:58:03 +0800
committerntzyz <i@ntzyz.io>2026-07-07 11:58:03 +0800
commitda5187bba6a459629e696c56b7b6335e017eae33 (patch)
tree6c8c9feed9deda4cc491ce7f924430d966abf890
initial commit
-rw-r--r--INSTALLATION.md146
-rw-r--r--__init__.py298
-rwxr-xr-xmimo_tts.py246
-rw-r--r--plugin.yaml4
-rwxr-xr-xsetup.sh67
5 files changed, 761 insertions, 0 deletions
diff --git a/INSTALLATION.md b/INSTALLATION.md
new file mode 100644
index 0000000..3918798
--- /dev/null
+++ b/INSTALLATION.md
@@ -0,0 +1,146 @@
+# MiMo V2.5 TTS — Hermes Agent Plugin
+
+Xiaomi MiMo V2.5 语音合成插件,为 Hermes Agent 提供高质量中英文 TTS 能力。
+
+## 功能
+
+- 9 个内置音色(4 中文 + 4 英文 + 1 自动)
+- 3 种模型:内置音色 / 语音设计 / 声音克隆
+- 自然语言风格控制(播音员、讲故事、方言、情绪等)
+- 流式输出支持
+- 兼容 Hermes 语音气泡投递(Telegram 等)
+
+## 音色列表
+
+| ID | 名称 | 语言 | 性别 |
+|----|------|------|------|
+| `mimo_default` | 默认 | 自动 | - |
+| `冰糖` | Bingtang | 中文 | 女 |
+| `茉莉` | Moli | 中文 | 女 |
+| `苏打` | Suda | 中文 | 男 |
+| `白桦` | Baihua | 中文 | 男 |
+| `Mia` | Mia | 英文 | 女 |
+| `Chloe` | Chloe | 英文 | 女 |
+| `Milo` | Milo | 英文 | 男 |
+| `Dean` | Dean | 英文 | 男 |
+
+## 前置条件
+
+- Hermes Agent 已安装并可运行
+- Xiaomi MiMo API Key(获取地址:https://mimo.mi.com)
+
+## 安装步骤
+
+运行安装脚本:
+
+```bash
+bash ~/mimo-tts-hermes-plugin/setup.sh
+```
+
+脚本会自动完成:
+1. 将插件文件复制到 `~/.hermes/plugins/tts/mimo/`
+2. 在 Hermes 中启用插件(`hermes plugins enable tts/mimo`)
+3. 将独立脚本复制到 `~/mimo_tts.py`
+
+安装后需要手动配置(脚本会提示):
+
+```bash
+# 设置 TTS provider
+hermes config set tts.provider mimo
+
+# 设置默认音色(可选,不设则用 mimo_default)
+hermes config set tts.voice Mia
+
+# 重启 session 生效
+# 在 Hermes 中执行 /reset
+```
+
+## 使用方式
+
+### Hermes 内置调用
+
+配置完成后,Hermes 的 `text_to_speech` 工具会自动走 MiMo:
+
+```
+text_to_speech(text="你好世界")
+```
+
+### 独立脚本调用(不依赖 Hermes)
+
+```bash
+# 基本用法
+python3 ~/mimo_tts.py "你好世界" --voice 冰糖
+
+# 英文
+python3 ~/mimo_tts.py "Hello world" --voice Mia
+
+# 指定输出路径
+python3 ~/mimo_tts.py "测试" --voice 茉莉 -o output.wav
+
+# 语音设计模型(通过文字描述生成音色)
+python3 ~/mimo_tts.py "Once upon a time" --model voicedesign \
+ --style "A warm storyteller voice with British accent"
+
+# 流式模式
+python3 ~/mimo_tts.py "你好" --stream -o output.wav
+
+# 列出音色
+python3 ~/mimo_tts.py --list-voices
+
+# 列出模型
+python3 ~/mimo_tts.py --list-models
+```
+
+### Python API 调用
+
+```python
+import sys, os
+sys.path.insert(0, os.path.expanduser("~/.hermes/hermes-agent"))
+os.environ.setdefault("XIAOMI_API_KEY", "your_key")
+os.environ.setdefault("XIAOMI_BASE_URL", "https://token-plan-cn.xiaomimimo.com/v1")
+
+# 加载插件
+import importlib.util
+spec = importlib.util.spec_from_file_location(
+ "mimo_tts", os.path.expanduser("~/.hermes/plugins/tts/mimo/__init__.py")
+)
+mod = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(mod)
+
+provider = mod.MiMoTTSProvider()
+provider.synthesize("你好世界", "output.wav", voice="冰糖")
+```
+
+## 环境变量
+
+| 变量 | 必需 | 说明 |
+|------|------|------|
+| `XIAOMI_API_KEY` | ✅ | MiMo API Key |
+| `XIAOMI_BASE_URL` | 可选 | API 端点(默认 `https://api.xiaomimimo.com/v1`,国内用户可能需要设为 `https://token-plan-cn.xiaomimimo.com/v1`) |
+
+这些变量应配置在 `~/.hermes/.env` 中。
+
+## 卸载
+
+```bash
+bash ~/mimo-tts-hermes-plugin/setup.sh --uninstall
+```
+
+或手动:
+
+```bash
+hermes plugins disable tts/mimo
+rm -rf ~/.hermes/plugins/tts/mimo
+rm -f ~/mimo_tts.py
+```
+
+## 文件说明
+
+```
+mimo-tts-hermes-plugin/
+├── INSTALLATION.md # 本文档
+├── setup.sh # 安装/卸载脚本
+├── plugin.yaml # Hermes 插件清单
+├── __init__.py # Hermes TTS Provider 实现
+└── mimo_tts.py # 独立命令行脚本
+```
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..a49839c
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,298 @@
+"""MiMo V2.5 TTS Provider Plugin for Hermes Agent.
+
+Integrates Xiaomi's MiMo V2.5 Text-to-Speech API as a Hermes TTS provider.
+Supports three models:
+ - mimo-v2.5-tts — built-in high-quality voices
+ - mimo-v2.5-tts-voicedesign — custom voice from text description
+ - mimo-v2.5-tts-voiceclone — replicate voice from audio samples
+
+Requires: XIAOMI_API_KEY environment variable.
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import logging
+import os
+from typing import Any, Dict, List, Optional
+
+from agent.tts_provider import TTSProvider, DEFAULT_OUTPUT_FORMAT, VALID_OUTPUT_FORMATS
+
+logger = logging.getLogger(__name__)
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+_API_BASE_DEFAULT = "https://api.xiaomimimo.com/v1"
+
+
+def _get_api_base() -> str:
+ """Return the MiMo API base URL.
+
+ Checks XIAOMI_BASE_URL env var first (matches the LLM provider config),
+ then falls back to the public endpoint.
+ """
+ return os.environ.get("XIAOMI_BASE_URL", _API_BASE_DEFAULT).rstrip("/")
+
+
+_MODEL_DEFAULT = "mimo-v2.5-tts"
+_VOICE_DEFAULT = "mimo_default"
+
+# Format mapping: Hermes format → MiMo API format
+_FORMAT_MAP = {
+ "mp3": "wav", # MiMo doesn't output mp3; fall back to wav
+ "wav": "wav",
+ "ogg": "wav",
+ "opus": "wav",
+ "flac": "wav",
+}
+
+_BUILTIN_VOICES: List[Dict[str, Any]] = [
+ {"id": "mimo_default", "display": "MiMo Default (auto-select cluster default)", "language": "auto", "gender": ""},
+ {"id": "冰糖", "display": "Bingtang (冰糖)", "language": "zh", "gender": "female"},
+ {"id": "茉莉", "display": "Moli (茉莉)", "language": "zh", "gender": "female"},
+ {"id": "苏打", "display": "Suda (苏打)", "language": "zh", "gender": "male"},
+ {"id": "白桦", "display": "Baihua (白桦)", "language": "zh", "gender": "male"},
+ {"id": "Mia", "display": "Mia", "language": "en", "gender": "female"},
+ {"id": "Chloe", "display": "Chloe", "language": "en", "gender": "female"},
+ {"id": "Milo", "display": "Milo", "language": "en", "gender": "male"},
+ {"id": "Dean", "display": "Dean", "language": "en", "gender": "male"},
+]
+
+_MODELS: List[Dict[str, Any]] = [
+ {"id": "mimo-v2.5-tts", "display": "MiMo V2.5 TTS (built-in voices)", "max_text_length": 10000},
+ {"id": "mimo-v2.5-tts-voicedesign", "display": "MiMo V2.5 TTS Voice Design", "max_text_length": 10000},
+ {"id": "mimo-v2.5-tts-voiceclone", "display": "MiMo V2.5 TTS Voice Clone", "max_text_length": 10000},
+]
+
+
+# ---------------------------------------------------------------------------
+# Provider
+# ---------------------------------------------------------------------------
+
+class MiMoTTSProvider(TTSProvider):
+ """Xiaomi MiMo V2.5 Text-to-Speech provider."""
+
+ @property
+ def name(self) -> str:
+ return "mimo"
+
+ @property
+ def display_name(self) -> str:
+ return "MiMo TTS (Xiaomi)"
+
+ def is_available(self) -> bool:
+ return bool(os.environ.get("XIAOMI_API_KEY"))
+
+ def list_voices(self) -> List[Dict[str, Any]]:
+ return _BUILTIN_VOICES
+
+ def list_models(self) -> List[Dict[str, Any]]:
+ return _MODELS
+
+ def default_model(self) -> Optional[str]:
+ return _MODEL_DEFAULT
+
+ def default_voice(self) -> Optional[str]:
+ return _VOICE_DEFAULT
+
+ def get_setup_schema(self) -> Dict[str, Any]:
+ return {
+ "name": "MiMo TTS (Xiaomi)",
+ "badge": "paid",
+ "tag": "V2.5 — built-in voices, voice design, voice cloning",
+ "env_vars": [
+ {
+ "key": "XIAOMI_API_KEY",
+ "prompt": "Xiaomi MiMo API key",
+ "url": "https://mimo.mi.com",
+ },
+ ],
+ }
+
+ @property
+ def voice_compatible(self) -> bool:
+ return True
+
+ def synthesize(
+ self,
+ text: str,
+ output_path: str,
+ *,
+ voice: Optional[str] = None,
+ model: Optional[str] = None,
+ speed: Optional[float] = None,
+ format: str = DEFAULT_OUTPUT_FORMAT,
+ **extra: Any,
+ ) -> str:
+ """Synthesize text via MiMo V2.5 TTS API.
+
+ The text is placed in the ``assistant`` message (per MiMo API spec).
+ Style instructions (from ``extra["style"]``) go in the ``user`` message.
+ """
+ import urllib.request
+
+ api_key = os.environ.get("XIAOMI_API_KEY")
+ if not api_key:
+ raise RuntimeError(
+ "XIAOMI_API_KEY environment variable is not set. "
+ "Get your key at https://mimo.mi.com"
+ )
+
+ model_id = model or _MODEL_DEFAULT
+ voice_id = voice or _VOICE_DEFAULT
+ mimo_format = _FORMAT_MAP.get(format, "wav")
+
+ # Build messages
+ messages: list = []
+
+ # Style instruction in user message (optional)
+ style = extra.get("style")
+ if style:
+ messages.append({"role": "user", "content": str(style)})
+
+ # Text to synthesize in assistant message
+ messages.append({"role": "assistant", "content": text})
+
+ # Build request body
+ # voice-design and voice-clone models don't accept the voice field
+ audio_config: Dict[str, Any] = {"format": mimo_format}
+ if "voicedesign" not in model_id and "voiceclone" not in model_id:
+ audio_config["voice"] = voice_id
+ body: Dict[str, Any] = {
+ "model": model_id,
+ "messages": messages,
+ "audio": audio_config,
+ }
+
+ # Speed control via style instructions if speed is set
+ # (MiMo doesn't have a direct speed param; we prepend to style)
+ if speed is not None and speed != 1.0 and not style:
+ speed_desc = "faster" if speed > 1.0 else "slower"
+ body["messages"].insert(0, {
+ "role": "user",
+ "content": f"Speak at a slightly {speed_desc} pace than normal.",
+ })
+
+ data = json.dumps(body).encode("utf-8")
+
+ req = urllib.request.Request(
+ f"{_get_api_base()}/chat/completions",
+ data=data,
+ headers={
+ "Content-Type": "application/json",
+ "api-key": api_key,
+ },
+ method="POST",
+ )
+
+ logger.info("MiMo TTS: synthesizing %d chars with model=%s voice=%s", len(text), model_id, voice_id)
+
+ try:
+ with urllib.request.urlopen(req, timeout=60) as resp:
+ resp_data = json.loads(resp.read().decode("utf-8"))
+ except Exception as exc:
+ raise RuntimeError(f"MiMo TTS API request failed: {exc}") from exc
+
+ # Extract audio from response
+ try:
+ audio_b64 = resp_data["choices"][0]["message"]["audio"]["data"]
+ except (KeyError, IndexError, TypeError) as exc:
+ raise RuntimeError(
+ f"MiMo TTS API returned unexpected response structure: {exc}\n"
+ f"Response: {json.dumps(resp_data, ensure_ascii=False)[:500]}"
+ ) from exc
+
+ audio_bytes = base64.b64decode(audio_b64)
+
+ # Ensure output has correct extension for the actual format
+ # MiMo always returns wav regardless of what we asked
+ if not output_path.endswith(".wav"):
+ output_path = os.path.splitext(output_path)[0] + ".wav"
+
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
+ with open(output_path, "wb") as f:
+ f.write(audio_bytes)
+
+ logger.info("MiMo TTS: wrote %d bytes to %s", len(audio_bytes), output_path)
+ return output_path
+
+ def stream(
+ self,
+ text: str,
+ *,
+ voice: Optional[str] = None,
+ model: Optional[str] = None,
+ format: str = "opus",
+ **extra: Any,
+ ):
+ """Stream synthesized audio chunks via MiMo V2.5 TTS streaming API."""
+ import urllib.request
+
+ api_key = os.environ.get("XIAOMI_API_KEY")
+ if not api_key:
+ raise RuntimeError("XIAOMI_API_KEY environment variable is not set.")
+
+ model_id = model or _MODEL_DEFAULT
+ voice_id = voice or _VOICE_DEFAULT
+
+ messages: list = []
+ style = extra.get("style")
+ if style:
+ messages.append({"role": "user", "content": str(style)})
+ messages.append({"role": "assistant", "content": text})
+
+ audio_config: Dict[str, Any] = {"format": "pcm16"}
+ if "voicedesign" not in model_id and "voiceclone" not in model_id:
+ audio_config["voice"] = voice_id
+ body = {
+ "model": model_id,
+ "messages": messages,
+ "audio": audio_config,
+ "stream": True,
+ }
+
+ data = json.dumps(body).encode("utf-8")
+ req = urllib.request.Request(
+ f"{_get_api_base()}/chat/completions",
+ data=data,
+ headers={
+ "Content-Type": "application/json",
+ "api-key": api_key,
+ },
+ method="POST",
+ )
+
+ logger.info("MiMo TTS streaming: synthesizing %d chars", len(text))
+
+ with urllib.request.urlopen(req, timeout=120) as resp:
+ buffer = b""
+ for line in resp:
+ line = line.strip()
+ if not line:
+ continue
+ # SSE format: "data: {...}"
+ if line.startswith(b"data: "):
+ payload = line[6:]
+ if payload == b"[DONE]":
+ break
+ try:
+ chunk = json.loads(payload)
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
+ audio_data = delta.get("audio", {})
+ if isinstance(audio_data, dict) and "data" in audio_data:
+ pcm_bytes = base64.b64decode(audio_data["data"])
+ yield pcm_bytes
+ except (json.JSONDecodeError, KeyError, IndexError):
+ continue
+
+
+# ---------------------------------------------------------------------------
+# Plugin entry point
+# ---------------------------------------------------------------------------
+
+def register(ctx):
+ """Hermes plugin entry point — register the MiMo TTS provider."""
+ ctx.register_tts_provider(MiMoTTSProvider())
diff --git a/mimo_tts.py b/mimo_tts.py
new file mode 100755
index 0000000..6d9d770
--- /dev/null
+++ b/mimo_tts.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+"""MiMo V2.5 TTS — 独立语音合成脚本
+
+用法:
+ # 基本用法
+ python3 mimo_tts.py "要合成的文字"
+
+ # 指定音色和输出路径
+ python3 mimo_tts.py "Hello world" --voice Mia --output hello.wav
+
+ # 使用语音设计模型(通过文字描述生成音色)
+ python3 mimo_tts.py "Once upon a time" --model voicedesign \
+ --style "A warm storyteller voice with British accent"
+
+ # 列出所有可用音色
+ python3 mimo_tts.py --list-voices
+
+ # 流式输出(PCM16 24kHz)
+ python3 mimo_tts.py "你好世界" --stream --output output.wav
+
+环境变量:
+ XIAOMI_API_KEY — 必需,小米 MiMo API Key
+ XIAOMI_BASE_URL — 可选,API 端点(默认 https://api.xiaomimimo.com/v1)
+
+获取 API Key: https://mimo.mi.com
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+import os
+import sys
+import urllib.request
+from pathlib import Path
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+API_BASE_DEFAULT = "https://api.xiaomimimo.com/v1"
+
+VOICES = [
+ {"id": "mimo_default", "display": "默认 (auto)", "lang": "auto", "gender": "-"},
+ {"id": "冰糖", "display": "冰糖 Bingtang", "lang": "zh", "gender": "♀"},
+ {"id": "茉莉", "display": "茉莉 Moli", "lang": "zh", "gender": "♀"},
+ {"id": "苏打", "display": "苏打 Suda", "lang": "zh", "gender": "♂"},
+ {"id": "白桦", "display": "白桦 Baihua", "lang": "zh", "gender": "♂"},
+ {"id": "Mia", "display": "Mia", "lang": "en", "gender": "♀"},
+ {"id": "Chloe", "display": "Chloe", "lang": "en", "gender": "♀"},
+ {"id": "Milo", "display": "Milo", "lang": "en", "gender": "♂"},
+ {"id": "Dean", "display": "Dean", "lang": "en", "gender": "♂"},
+]
+
+MODELS = {
+ "tts": "mimo-v2.5-tts",
+ "voicedesign": "mimo-v2.5-tts-voicedesign",
+ "voiceclone": "mimo-v2.5-tts-voiceclone",
+}
+
+
+# ---------------------------------------------------------------------------
+# Core API
+# ---------------------------------------------------------------------------
+
+def get_api_base() -> str:
+ return os.environ.get("XIAOMI_BASE_URL", API_BASE_DEFAULT).rstrip("/")
+
+
+def synthesize(
+ text: str,
+ output_path: str,
+ *,
+ voice: str = "mimo_default",
+ model: str = "mimo-v2.5-tts",
+ style: str | None = None,
+ api_key: str | None = None,
+) -> str:
+ """合成语音并写入文件,返回文件路径。"""
+ api_key = api_key or os.environ.get("XIAOMI_API_KEY")
+ if not api_key:
+ raise RuntimeError("XIAOMI_API_KEY is not set. Get one at https://mimo.mi.com")
+
+ messages: list = []
+ if style:
+ messages.append({"role": "user", "content": style})
+ messages.append({"role": "assistant", "content": text})
+
+ audio_config: dict = {"format": "wav"}
+ if "voicedesign" not in model and "voiceclone" not in model:
+ audio_config["voice"] = voice
+
+ body = {
+ "model": model,
+ "messages": messages,
+ "audio": audio_config,
+ }
+
+ data = json.dumps(body).encode()
+ req = urllib.request.Request(
+ f"{get_api_base()}/chat/completions",
+ data=data,
+ headers={"Content-Type": "application/json", "api-key": api_key},
+ method="POST",
+ )
+
+ with urllib.request.urlopen(req, timeout=60) as resp:
+ resp_data = json.loads(resp.read().decode())
+
+ audio_b64 = resp_data["choices"][0]["message"]["audio"]["data"]
+ audio_bytes = base64.b64decode(audio_b64)
+
+ output_path = str(Path(output_path).expanduser())
+ if not output_path.endswith(".wav"):
+ output_path = os.path.splitext(output_path)[0] + ".wav"
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
+
+ with open(output_path, "wb") as f:
+ f.write(audio_bytes)
+
+ return output_path
+
+
+def synthesize_stream(
+ text: str,
+ *,
+ voice: str = "mimo_default",
+ model: str = "mimo-v2.5-tts",
+ style: str | None = None,
+ api_key: str | None = None,
+):
+ """流式合成,yield PCM16 字节块 (24kHz mono)。"""
+ api_key = api_key or os.environ.get("XIAOMI_API_KEY")
+ if not api_key:
+ raise RuntimeError("XIAOMI_API_KEY is not set.")
+
+ messages: list = []
+ if style:
+ messages.append({"role": "user", "content": style})
+ messages.append({"role": "assistant", "content": text})
+
+ audio_config: dict = {"format": "pcm16"}
+ if "voicedesign" not in model and "voiceclone" not in model:
+ audio_config["voice"] = voice
+
+ body = {"model": model, "messages": messages, "audio": audio_config, "stream": True}
+ data = json.dumps(body).encode()
+ req = urllib.request.Request(
+ f"{get_api_base()}/chat/completions",
+ data=data,
+ headers={"Content-Type": "application/json", "api-key": api_key},
+ method="POST",
+ )
+
+ with urllib.request.urlopen(req, timeout=120) as resp:
+ for line in resp:
+ line = line.strip()
+ if not line or not line.startswith(b"data: "):
+ continue
+ payload = line[6:]
+ if payload == b"[DONE]":
+ break
+ try:
+ chunk = json.loads(payload)
+ audio_data = chunk["choices"][0]["delta"].get("audio", {})
+ if isinstance(audio_data, dict) and "data" in audio_data:
+ yield base64.b64decode(audio_data["data"])
+ except (json.JSONDecodeError, KeyError, IndexError):
+ continue
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+def list_voices():
+ print(f"{'ID':<15} {'Display':<20} {'Lang':<6} {'Gender'}")
+ print("-" * 50)
+ for v in VOICES:
+ print(f"{v['id']:<15} {v['display']:<20} {v['lang']:<6} {v['gender']}")
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="MiMo V2.5 TTS — 语音合成",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+ parser.add_argument("text", nargs="?", help="要合成的文字")
+ parser.add_argument("-v", "--voice", default="mimo_default",
+ help="音色 ID (默认: mimo_default)")
+ parser.add_argument("-m", "--model", default="tts",
+ choices=["tts", "voicedesign", "voiceclone"],
+ help="模型 (默认: tts)")
+ parser.add_argument("-s", "--style", default=None,
+ help="风格/语气描述 (user message)")
+ parser.add_argument("-o", "--output", default=None,
+ help="输出文件路径 (默认: ~/mimo_tts_output.wav)")
+ parser.add_argument("--stream", action="store_true",
+ help="流式模式 (输出 PCM16 WAV)")
+ parser.add_argument("--list-voices", action="store_true",
+ help="列出所有可用音色")
+ parser.add_argument("--list-models", action="store_true",
+ help="列出所有可用模型")
+
+ args = parser.parse_args()
+
+ if args.list_voices:
+ list_voices()
+ return
+ if args.list_models:
+ for key, mid in MODELS.items():
+ print(f" {key:<15} → {mid}")
+ return
+
+ if not args.text:
+ parser.error("请提供要合成的文字")
+
+ model_id = MODELS[args.model]
+ output = args.output or os.path.expanduser("~/mimo_tts_output.wav")
+
+ if args.stream:
+ print(f"流式合成中... voice={args.voice} model={model_id}")
+ collected = bytearray()
+ for chunk in synthesize_stream(args.text, voice=args.voice, model=model_id, style=args.style):
+ collected.extend(chunk)
+ print(f" 收到 {len(chunk)} 字节", end="\r")
+ # Write as WAV (PCM16 24kHz mono)
+ import struct, wave
+ output = output if output.endswith(".wav") else output + ".wav"
+ with wave.open(output, "wb") as wf:
+ wf.setnchannels(1)
+ wf.setsampwidth(2)
+ wf.setframerate(24000)
+ wf.writeframes(bytes(collected))
+ print(f"\n✅ 已保存: {output} ({len(collected)} 字节 PCM)")
+ else:
+ print(f"合成中... voice={args.voice} model={model_id}")
+ result = synthesize(args.text, output, voice=args.voice, model=model_id, style=args.style)
+ size_kb = os.path.getsize(result) / 1024
+ print(f"✅ 已保存: {result} ({size_kb:.1f} KB)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugin.yaml b/plugin.yaml
new file mode 100644
index 0000000..42ddc63
--- /dev/null
+++ b/plugin.yaml
@@ -0,0 +1,4 @@
+name: mimo-tts
+version: 1.0.0
+description: "Xiaomi MiMo V2.5 TTS — high-quality speech synthesis with built-in voices, voice design, and voice cloning."
+author: user
diff --git a/setup.sh b/setup.sh
new file mode 100755
index 0000000..0beefc1
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+# MiMo V2.5 TTS — Hermes Agent Plugin Setup
+# 用法: bash setup.sh [--uninstall]
+
+set -euo pipefail
+
+HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
+PLUGIN_DIR="$HERMES_HOME/plugins/tts/mimo"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
+ok() { echo -e "${GREEN}✓${NC} $*"; }
+warn() { echo -e "${YELLOW}⚠${NC} $*"; }
+fail() { echo -e "${RED}✗${NC} $*" >&2; }
+
+# ── Uninstall ──────────────────────────────────────────────────────
+if [[ "${1:-}" == "--uninstall" ]]; then
+ echo "Uninstalling MiMo TTS plugin..."
+ command -v hermes &>/dev/null && hermes plugins disable tts/mimo 2>/dev/null || true
+ rm -rf "$PLUGIN_DIR"
+ rm -f "$HOME/mimo_tts.py"
+ ok "Done. Restart Hermes session (/reset) to take effect."
+ exit 0
+fi
+
+# ── Install ────────────────────────────────────────────────────────
+echo "Installing MiMo V2.5 TTS plugin for Hermes Agent..."
+
+# 1. Pre-flight checks
+[[ -d "$HERMES_HOME" ]] || { fail "Hermes not found at $HERMES_HOME"; exit 1; }
+
+if [[ -f "$HERMES_HOME/.env" ]]; then
+ grep -q "^XIAOMI_API_KEY=" "$HERMES_HOME/.env" 2>/dev/null \
+ && ok "XIAOMI_API_KEY found" \
+ || warn "XIAOMI_API_KEY missing in $HERMES_HOME/.env"
+else
+ warn ".env not found — add XIAOMI_API_KEY before using TTS"
+fi
+
+# 2. Install plugin
+mkdir -p "$PLUGIN_DIR"
+cp "$SCRIPT_DIR/plugin.yaml" "$PLUGIN_DIR/plugin.yaml"
+cp "$SCRIPT_DIR/__init__.py" "$PLUGIN_DIR/__init__.py"
+ok "Plugin installed → $PLUGIN_DIR"
+
+# 3. Copy standalone script
+cp "$SCRIPT_DIR/mimo_tts.py" "$HOME/mimo_tts.py"
+chmod +x "$HOME/mimo_tts.py"
+ok "Standalone script → $HOME/mimo_tts.py"
+
+# 4. Enable in Hermes
+if command -v hermes &>/dev/null; then
+ hermes plugins enable tts/mimo 2>/dev/null && ok "Enabled in Hermes" \
+ || warn "Run manually: hermes plugins enable tts/mimo"
+else
+ warn "hermes not in PATH — run: hermes plugins enable tts/mimo"
+fi
+
+# 5. Done
+echo ""
+echo "── Next steps ──────────────────────────────────────"
+echo " hermes config set tts.provider mimo"
+echo " hermes config set tts.voice Mia # or 冰糖, 茉莉, Chloe, etc."
+echo " /reset # restart session"
+echo ""
+echo " Standalone: python3 ~/mimo_tts.py --list-voices"
+echo "────────────────────────────────────────────────────"