diff options
| author | ntzyz <i@ntzyz.io> | 2026-07-07 12:51:48 +0800 |
|---|---|---|
| committer | ntzyz <i@ntzyz.io> | 2026-07-07 12:51:48 +0800 |
| commit | bcafdfe7aa5e076610e1951f35b11898a83f1391 (patch) | |
| tree | 76776b5c1eaf83040a782d80542d72357bce6195 | |
| parent | 582cf29169bceaed8754cf6f7fefcd37ac4506b8 (diff) | |
feat: add Hermes plugin architecture (plugin.yaml, __init__.py, provider.py)
- Add plugin.yaml with proper kind/requires_env/provides fields
- Add __init__.py and provider.py for STT provider registration
- Split README.md (overview) and INSTALL.md (install for agents)
- Add setup.sh (reference only) and .gitignore
- Add Windows PowerShell equivalents and reuse tip for MiMo API key
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | INSTALL.md | 176 | ||||
| -rw-r--r-- | README.md | 36 | ||||
| -rw-r--r-- | __init__.py | 14 | ||||
| -rw-r--r-- | plugin.yaml | 9 | ||||
| -rw-r--r-- | provider.py | 184 | ||||
| -rwxr-xr-x | setup.sh | 73 |
7 files changed, 416 insertions, 84 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c41f54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +*.pyo +.env +*.wav +*.mp3 +output.txt +test_audio/ @@ -1,68 +1,67 @@ -# Xiaomi MiMo ASR (mimo-v2.5-asr) — Hermes STT 安装说明 +# MiMo V2.5 ASR — 安装说明(给 Agent 看) -将小米 MiMo ASR 语音识别模型接入 Hermes 的语音转文字功能。 - -本版本针对 **Windows + Hermes Desktop** 进行了适配,使用纯 Python 实现,不依赖 bash/git-bash 环境。 - ---- +> **提示**:如果你已经安装过其他 MiMo 插件(如 mimo-tts、mimo-websearch)并已配置过 `XIAOMI_API_KEY` 和 `XIAOMI_BASE_URL`,可以直接复用已有的环境变量配置,无需重复询问 API Key 和端点。 ## 前置条件 -- Hermes Agent 已安装且正常运行 -- Xiaomi MiMo API Key([https://platform.xiaomimimo.com](https://platform.xiaomimimo.com) 获取) +- Hermes Agent 已安装并可运行 +- Xiaomi MiMo API Key(获取地址:https://platform.xiaomimimo.com) - ffmpeg(用于非 wav/mp3 格式的自动转换) - - 可在 [gyan.dev](https://www.gyan.dev/ffmpeg/builds/) 下载,或 `winget install Gyan.FFmpeg` - - 确保 `ffmpeg` 在系统 PATH 中 -- Python(Hermes 自带,无需额外安装) - ---- - -## 工作原理 - -Xiaomi ASR **不使用**标准的 `/v1/audio/transcriptions` 端点,而是通过 `/v1/chat/completions` 接口 + `input_audio` 格式工作。该脚本负责: -- 音频格式检测(wav/mp3 直接发送,其他格式通过 ffmpeg 转为 16kHz 单声道 WAV) -- Base64 编码音频数据 -- 构造 chat completions JSON payload -- 解析响应提取转录文本 +## 安装 -### 为什么用 Python 而不是 Shell 脚本? +> **注意**:`setup.sh` 仅供参考,请勿直接运行。请阅读以下步骤,根据用户的环境自行完成安装。 +> 以下命令中的 `~/.hermes` 仅为 Unix 典型路径,**不要硬编码**。安装前请先确认 Hermes 的实际安装目录(如 `$HERMES_HOME`、`$env:HERMES_HOME` 等)。 -Hermes 在 Windows 上通过 **cmd.exe** 执行 command provider,cmd.exe **不识别**: -- `~` 波浪号(不会展开为用户目录) -- `.sh` 扩展名(不知道用什么程序打开) +### 方式一:Plugin 模式(推荐) -Python 脚本(`.py`)可以被 `python` 解释器直接调用,无上述问题。 +将插件文件复制到 Hermes 插件目录并启用: ---- +```bash +mkdir -p ~/.hermes/plugins/stt/mimo +cp plugin.yaml __init__.py provider.py ~/.hermes/plugins/stt/mimo/ +hermes plugins enable stt/mimo +``` -## 安装步骤 +```powershell +# Windows (PowerShell) +mkdir -Force "$env:USERPROFILE\.hermes\plugins\stt\mimo" +cp plugin.yaml,__init__.py,provider.py "$env:USERPROFILE\.hermes\plugins\stt\mimo\" +hermes plugins enable stt/mimo +``` -### 1. 复制脚本 +安装后配置: -将 `xiaomi-asr.py` 复制到 Hermes 的脚本目录: +```bash +hermes config set stt.provider mimo-asr +hermes config set stt.providers.mimo-asr.type plugin +# 重启 session 生效(/reset) +``` -```powershell -# 创建脚本目录(如果不存在) -mkdir $env:USERPROFILE\.hermes\scripts -Force +### 方式二:Command 模式 -# 复制脚本 -copy .\xiaomi-asr.py $env:USERPROFILE\.hermes\scripts\xiaomi-asr.py -``` +适用于不希望以插件形式加载,或需要自定义脚本路径的场景。 -或在 bash (git-bash) 中: +#### 1. 复制脚本 ```bash mkdir -p ~/.hermes/scripts cp xiaomi-asr.py ~/.hermes/scripts/xiaomi-asr.py +cp xiaomi-asr.sh ~/.hermes/scripts/xiaomi-asr.sh +chmod +x ~/.hermes/scripts/xiaomi-asr.* +``` + +```powershell +# Windows (PowerShell) — 仅需要 .py 脚本 +mkdir -Force "$env:USERPROFILE\.hermes\scripts" +cp xiaomi-asr.py "$env:USERPROFILE\.hermes\scripts\xiaomi-asr.py" ``` -### 2. 配置环境变量 +#### 2. 配置环境变量 -在 `D:\Program Files\Hermes\.env`(或 `$env:HERMES_HOME\.env`,视你的安装位置而定)中添加: +在 Hermes 的 `.env` 文件中添加: ```env -# Xiaomi MiMo ASR XIAOMI_API_KEY=你的API_Key XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1 ``` @@ -70,77 +69,86 @@ XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1 > 中国站用户:`https://token-plan-cn.xiaomimimo.com/v1` > 国际站用户:`https://api.xiaomimimo.com/v1` -### 3. 修改 Hermes 配置 +#### 3. 修改 Hermes 配置 -编辑 `D:\Program Files\Hermes\config.yaml`(或 `$env:HERMES_HOME\config.yaml`)的 `stt` 部分: +编辑 Hermes 的 `config.yaml` 的 `stt` 部分: ```yaml stt: enabled: true - provider: xiaomi-asr + provider: mimo-asr providers: - xiaomi-asr: + mimo-asr: type: command - command: python C:/Users/<你的用户名>/.hermes/scripts/xiaomi-asr.py {input_path} {output_path} mimo-v2.5-asr {language} + command: python ~/.hermes/scripts/xiaomi-asr.py {input_path} {output_path} mimo-v2.5-asr {language} language: zh format: txt timeout: 120 ``` -> **注意**:请将 `<你的用户名>` 替换为实际的 Windows 用户名,并用**正斜杠**(`/`)代替反斜杠。 -> -> 也可用 `hermes config set` 来快速配置: -> ```bash -> hermes config set stt.provider xiaomi-asr -> hermes config set stt.providers.xiaomi-asr.type command -> hermes config set stt.providers.xiaomi-asr.command "python C:/Users/<你的用户名>/.hermes/scripts/xiaomi-asr.py {input_path} {output_path} mimo-v2.5-asr {language}" -> hermes config set stt.providers.xiaomi-asr.language zh -> hermes config set stt.providers.xiaomi-asr.format txt -> hermes config set stt.providers.xiaomi-asr.timeout 120 -> ``` +> **Windows 注意**:命令路径需用正斜杠,如 `python C:/Users/<用户名>/.hermes/scripts/xiaomi-asr.py ...` -### 4. 重启 Hermes +或使用 `hermes config set`: -- **Desktop**:关闭并重新打开 -- **Gateway**:运行 `/restart` +```bash +hermes config set stt.provider mimo-asr +hermes config set stt.providers.mimo-asr.type command +hermes config set stt.providers.mimo-asr.command "python ~/.hermes/scripts/xiaomi-asr.py {input_path} {output_path} mimo-v2.5-asr {language}" +hermes config set stt.providers.mimo-asr.language zh +hermes config set stt.providers.mimo-asr.format txt +hermes config set stt.providers.mimo-asr.timeout 120 +``` ---- +#### 4. 重启 Hermes -## 验证 +- **Desktop**:关闭并重新打开 +- **Gateway**:运行 `/restart` -创建一个测试音频文件并用脚本手动测试: +## 独立使用(不依赖 Hermes) ```bash -# 创建 3 秒 440Hz 正弦波测试音频 -ffmpeg -y -f lavfi -i "sine=frequency=440:duration=3" -ar 16000 -ac 1 test.wav - -# 手动测试(确保 XIAOMI_API_KEY 和 XIAOMI_BASE_URL 已导出为环境变量) -python C:/Users/<你的用户名>/.hermes/scripts/xiaomi-asr.py test.wav output.txt mimo-v2.5-asr zh +export XIAOMI_API_KEY="your-key-here" +export XIAOMI_BASE_URL="https://token-plan-cn.xiaomimimo.com/v1" -# 查看结果 +python3 xiaomi-asr.py audio.wav output.txt mimo-v2.5-asr zh cat output.txt ``` ---- +```powershell +# Windows (PowerShell) +$env:XIAOMI_API_KEY="your-key-here" +$env:XIAOMI_BASE_URL="https://token-plan-cn.xiaomimimo.com/v1" -## 注意事项 +python xiaomi-asr.py audio.wav output.txt mimo-v2.5-asr zh +type output.txt +``` -| 问题 | 说明 | -|------|------| -| 音频格式 | 只支持 wav 和 mp3,其他格式自动通过 ffmpeg 转换 | -| 纯音乐 | 无法识别纯音乐内容,会返回空结果 | -| 语言 | 主要针对中文优化,其他语言效果较差 | -| ffmpeg | 非 wav/mp3 格式必须安装 ffmpeg | -| Windows 路径 | 建议在配置中使用**正斜杠**(`/`),Python 脚本内部会自动转换反斜杠 | -| 环境变量 | `XIAOMI_API_KEY` 必须在 `.env` 文件中,Hermes 会自动加载到子进程环境 | +## 卸载 ---- +```bash +hermes plugins disable stt/mimo +rm -rf ~/.hermes/plugins/stt/mimo +rm -f ~/xiaomi-asr.py ~/xiaomi-asr.sh +``` -## 文件清单 +```powershell +# Windows (PowerShell) +hermes plugins disable stt/mimo +rm -Recurse -Force "$env:USERPROFILE\.hermes\plugins\stt\mimo" +rm -Force "$env:USERPROFILE\xiaomi-asr.py","$env:USERPROFILE\xiaomi-asr.sh" +``` + +## 验证 +```bash +ffmpeg -y -f lavfi -i "sine=frequency=440:duration=3" -ar 16000 -ac 1 test.wav +python3 xiaomi-asr.py test.wav output.txt mimo-v2.5-asr zh +cat output.txt ``` -mimo-asr-for-hermes/ -├── INSTALL.md # 本安装说明 -├── xiaomi-asr.py # STT 适配脚本(Python) -└── xiaomi-asr.sh # STT 适配脚本(Bash,仅 Linux/macOS) + +```powershell +# Windows (PowerShell) +ffmpeg -y -f lavfi -i "sine=frequency=440:duration=3" -ar 16000 -ac 1 test.wav +python xiaomi-asr.py test.wav output.txt mimo-v2.5-asr zh +type output.txt ``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4deeee --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# MiMo V2.5 ASR — Hermes Agent Plugin + +将小米 MiMo ASR 语音识别模型接入 Hermes 的语音转文字功能。可作为 Hermes 插件加载,也可在 command 模式下独立使用。 + +## 工作原理 + +Xiaomi ASR **不使用**标准的 `/v1/audio/transcriptions` 端点,而是通过 `/v1/chat/completions` 接口 + `input_audio` 格式工作。 + +- 音频格式检测(wav/mp3 直接发送,其他格式通过 ffmpeg 转为 16kHz 单声道 WAV) +- Base64 编码音频数据 +- 构造 chat completions JSON payload +- 解析响应提取转录文本 + +## 注意事项 + +| 问题 | 说明 | +|------|------| +| 音频格式 | 只支持 wav 和 mp3,其他格式自动通过 ffmpeg 转换 | +| 纯音乐 | 无法识别纯音乐内容,会返回空结果 | +| 语言 | 主要针对中文优化,其他语言效果较差 | +| ffmpeg | 非 wav/mp3 格式必须安装 ffmpeg | +| 环境变量 | `XIAOMI_API_KEY` 必须在 `.env` 文件中,Hermes 会自动加载到子进程环境 | + +## 文件说明 + +``` +mimo-asr-for-hermes/ +├── README.md # 本文档(介绍) +├── INSTALL.md # 安装说明(给 Agent 看) +├── plugin.yaml # Hermes 插件清单 +├── setup.sh # 安装/卸载脚本(仅供参考) +├── __init__.py # 插件注册入口 +├── provider.py # STTProvider 实现 +├── xiaomi-asr.py # 独立脚本(Python,全平台) +└── xiaomi-asr.sh # 独立脚本(Bash,仅 Linux/macOS) +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..34e67f1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +"""Xiaomi MiMo ASR plugin. + +Provider class lives in ``provider.py``; ``__init__.py::register(ctx)`` +registers an instance. +""" + +from __future__ import annotations + +from plugins.stt.mimo.provider import MiMoSTTProvider + + +def register(ctx) -> None: + """Register the MiMo ASR (STT) provider with the plugin context.""" + ctx.register_stt_provider(MiMoSTTProvider()) diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..9a87a03 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,9 @@ +name: stt-mimo +version: 1.0.0 +description: "Xiaomi MiMo ASR — speech-to-text via MiMo's chat/completions API with input_audio format. Requires XIAOMI_API_KEY (https://platform.xiaomimimo.com)." +author: ntzyz <i@ntzyz.io> +kind: backend +provides_stt_providers: + - mimo-asr +requires_env: + - XIAOMI_API_KEY diff --git a/provider.py b/provider.py new file mode 100644 index 0000000..cfd69dd --- /dev/null +++ b/provider.py @@ -0,0 +1,184 @@ +"""Xiaomi MiMo ASR — plugin form. + +Subclasses :class:`agent.stt_provider.STTProvider`. Sends audio to MiMo's +chat/completions API with ``input_audio`` format and returns transcribed text. + +Xiaomi ASR does **not** use the standard ``/v1/audio/transcriptions`` endpoint. +Audio must be wav or mp3; other formats are auto-converted via ffmpeg. + +Auth env var:: + + XIAOMI_API_KEY=... # https://platform.xiaomimimo.com + XIAOMI_BASE_URL=... # optional, defaults to https://api.xiaomimimo.com/v1 +""" + +from __future__ import annotations + +import base64 +import json +import logging +import os +import ssl +import subprocess +import tempfile +import urllib.error +import urllib.request +from typing import Any, Dict, List, Optional + +from agent.stt_provider import STTProvider + +logger = logging.getLogger(__name__) + +DEFAULT_MODEL = "mimo-v2.5-asr" +DEFAULT_LANGUAGE = "zh" +_DEFAULT_BASE_URL = "https://api.xiaomimimo.com/v1" + +_MODELS: List[Dict[str, Any]] = [ + {"id": "mimo-v2.5-asr", "display": "MiMo V2.5 ASR", "languages": ["zh", "en"]}, +] + + +def _get_api_base() -> str: + return os.environ.get("XIAOMI_BASE_URL", _DEFAULT_BASE_URL).rstrip("/") + + +class MiMoSTTProvider(STTProvider): + """Xiaomi MiMo ASR (Speech-to-Text) provider.""" + + @property + def name(self) -> str: + return "mimo-asr" + + @property + def display_name(self) -> str: + return "MiMo ASR (Xiaomi)" + + def is_available(self) -> bool: + return bool(os.environ.get("XIAOMI_API_KEY")) + + def list_models(self) -> List[Dict[str, Any]]: + return _MODELS + + def default_model(self) -> Optional[str]: + return DEFAULT_MODEL + + def default_language(self) -> Optional[str]: + return DEFAULT_LANGUAGE + + def get_setup_schema(self) -> Dict[str, Any]: + return { + "name": "MiMo ASR (Xiaomi)", + "badge": "paid", + "tag": "V2.5 — speech-to-text, optimized for Chinese.", + "env_vars": [ + { + "key": "XIAOMI_API_KEY", + "prompt": "Xiaomi MiMo API key", + "url": "https://platform.xiaomimimo.com", + }, + ], + } + + def transcribe( + self, + audio_path: str, + *, + model: Optional[str] = None, + language: Optional[str] = None, + **extra: Any, + ) -> str: + 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://platform.xiaomimimo.com" + ) + + model_id = model or DEFAULT_MODEL + model_id = model_id.strip() + language = language or DEFAULT_LANGUAGE + + audio_path = audio_path.replace("\\", "/") # normalize Windows paths + + ext = os.path.splitext(audio_path)[1].lower() + if ext in (".wav",): + fmt = "wav" + audio_file = audio_path + elif ext in (".mp3",): + fmt = "mp3" + audio_file = audio_path + else: + fmt = "wav" + try: + subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True) + except (FileNotFoundError, subprocess.CalledProcessError): + raise RuntimeError( + f"ffmpeg is required to convert {ext} to wav/mp3 for Xiaomi ASR" + ) + + fd, audio_file = tempfile.mkstemp(suffix=".wav", prefix="hermes-asr-") + os.close(fd) + try: + subprocess.run( + ["ffmpeg", "-y", "-i", audio_path, "-ar", "16000", "-ac", "1", + "-f", "wav", audio_file], + capture_output=True, check=True, + ) + except subprocess.CalledProcessError as e: + os.unlink(audio_file) + raise RuntimeError( + f"ffmpeg conversion failed: {e.stderr.decode(errors='replace')}" + ) from e + + try: + with open(audio_file, "rb") as f: + audio_b64 = base64.b64encode(f.read()).decode() + + payload = json.dumps({ + "model": model_id, + "messages": [{ + "role": "user", + "content": [ + {"type": "input_audio", "input_audio": {"data": audio_b64, "format": fmt}}, + ] + }] + }).encode() + + req = urllib.request.Request( + f"{_get_api_base()}/chat/completions", + data=payload, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + ) + ctx = ssl.create_default_context() + + logger.info("MiMo ASR: transcribing %s with model=%s language=%s", audio_path, model_id, language) + + resp = urllib.request.urlopen(req, context=ctx, timeout=300) + response_data = json.loads(resp.read().decode()) + + choices = response_data.get("choices", []) + if not choices: + error_info = response_data.get("error", {}) + raise RuntimeError( + error_info.get("message", "No transcription result") + ) + + msg = choices[0].get("message", {}) + transcript = msg.get("content", "").strip() + + if not transcript: + raise RuntimeError("Xiaomi ASR returned empty transcript") + + return transcript + + except urllib.error.HTTPError as e: + body = e.read().decode(errors="replace") + raise RuntimeError(f"HTTP {e.code}: {body}") from e + except urllib.error.URLError as e: + raise RuntimeError(f"URL error: {e.reason}") from e + finally: + if 'audio_file' in locals() and audio_file != audio_path and os.path.exists(audio_file): + os.unlink(audio_file) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..7255a43 --- /dev/null +++ b/setup.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# MiMo V2.5 ASR — Hermes Agent Plugin Setup +# 用法: bash setup.sh [--uninstall] + +set -euo pipefail + +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +PLUGIN_DIR="$HERMES_HOME/plugins/stt/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 ASR plugin..." + command -v hermes &>/dev/null && hermes plugins disable stt/mimo 2>/dev/null || true + rm -rf "$PLUGIN_DIR" + rm -f "$HOME/xiaomi-asr.py" "$HOME/xiaomi-asr.sh" + ok "Done. Restart Hermes session (/reset) to take effect." + exit 0 +fi + +# ── Install ──────────────────────────────────────────────────────── +echo "Installing MiMo V2.5 ASR 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 ASR" +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" +cp "$SCRIPT_DIR/provider.py" "$PLUGIN_DIR/provider.py" +ok "Plugin installed → $PLUGIN_DIR" + +# 3. Copy standalone scripts +cp "$SCRIPT_DIR/xiaomi-asr.py" "$HOME/xiaomi-asr.py" +chmod +x "$HOME/xiaomi-asr.py" +cp "$SCRIPT_DIR/xiaomi-asr.sh" "$HOME/xiaomi-asr.sh" +chmod +x "$HOME/xiaomi-asr.sh" +ok "Standalone scripts → $HOME/xiaomi-asr.{py,sh}" + +# 4. Enable in Hermes +if command -v hermes &>/dev/null; then + hermes plugins enable stt/mimo 2>/dev/null && ok "Enabled in Hermes" \ + || warn "Run manually: hermes plugins enable stt/mimo" +else + warn "hermes not in PATH — run: hermes plugins enable stt/mimo" +fi + +# 5. Done +echo "" +echo "── Next steps ──────────────────────────────────────" +echo " hermes config set stt.provider mimo-asr" +echo " hermes config set stt.providers.mimo-asr.type plugin" +echo "" +echo " Or use command mode (standalone script):" +echo " hermes config set stt.providers.mimo-asr.type command" +echo " hermes config set stt.providers.mimo-asr.command \"python \$HOME/xiaomi-asr.py {input_path} {output_path} mimo-v2.5-asr {language}\"" +echo "" +echo " /reset # restart session" +echo "────────────────────────────────────────────────────" |
