diff options
| -rw-r--r-- | INSTALLATION.md | 146 | ||||
| -rw-r--r-- | __init__.py | 298 | ||||
| -rwxr-xr-x | mimo_tts.py | 246 | ||||
| -rw-r--r-- | plugin.yaml | 4 | ||||
| -rwxr-xr-x | setup.sh | 67 |
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 "────────────────────────────────────────────────────" |
