summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorntzyz <i@ntzyz.io>2026-07-05 18:40:58 +0800
committerntzyz <i@ntzyz.io>2026-07-05 18:40:58 +0800
commit582cf29169bceaed8754cf6f7fefcd37ac4506b8 (patch)
tree27e819544242053d8837e4f0fe76aaee86b9cce5
parentee2db04cf4582dc3297fc54f7e15ce516b8fdf10 (diff)
support windows
-rw-r--r--INSTALL.md110
-rw-r--r--xiaomi-asr.py140
-rw-r--r--xiaomi-asr.sh4
3 files changed, 229 insertions, 25 deletions
diff --git a/INSTALL.md b/INSTALL.md
index f090991..f3bcae0 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -2,47 +2,77 @@
将小米 MiMo ASR 语音识别模型接入 Hermes 的语音转文字功能。
+本版本针对 **Windows + Hermes Desktop** 进行了适配,使用纯 Python 实现,不依赖 bash/git-bash 环境。
+
+---
+
## 前置条件
-- Hermes Agent 已安装
-- Xiaomi MiMo API Key(https://platform.xiaomimimo.com 获取)
-- ffmpeg(用于音频格式转换):`brew install ffmpeg`
+- Hermes Agent 已安装且正常运行
+- Xiaomi MiMo API Key([https://platform.xiaomimimo.com](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)
+Xiaomi ASR **不使用**标准的 `/v1/audio/transcriptions` 端点,而是通过 `/v1/chat/completions` 接口 + `input_audio` 格式工作。该脚本负责:
+
+- 音频格式检测(wav/mp3 直接发送,其他格式通过 ffmpeg 转为 16kHz 单声道 WAV)
- Base64 编码音频数据
- 构造 chat completions JSON payload
- 解析响应提取转录文本
-Hermes 的 **command provider** 机制允许通过 shell 命令接入任意 STT 后端,
-无需编写 Python 插件。
+### 为什么用 Python 而不是 Shell 脚本?
+
+Hermes 在 Windows 上通过 **cmd.exe** 执行 command provider,cmd.exe **不识别**:
+- `~` 波浪号(不会展开为用户目录)
+- `.sh` 扩展名(不知道用什么程序打开)
+
+Python 脚本(`.py`)可以被 `python` 解释器直接调用,无上述问题。
+
+---
## 安装步骤
### 1. 复制脚本
+将 `xiaomi-asr.py` 复制到 Hermes 的脚本目录:
+
+```powershell
+# 创建脚本目录(如果不存在)
+mkdir $env:USERPROFILE\.hermes\scripts -Force
+
+# 复制脚本
+copy .\xiaomi-asr.py $env:USERPROFILE\.hermes\scripts\xiaomi-asr.py
+```
+
+或在 bash (git-bash) 中:
+
```bash
-cp xiaomi-asr.sh ~/.hermes/scripts/xiaomi-asr.sh
-chmod +x ~/.hermes/scripts/xiaomi-asr.sh
+mkdir -p ~/.hermes/scripts
+cp xiaomi-asr.py ~/.hermes/scripts/xiaomi-asr.py
```
### 2. 配置环境变量
-在 `~/.hermes/.env` 中添加(如果还没有):
+在 `D:\Program Files\Hermes\.env`(或 `$env:HERMES_HOME\.env`,视你的安装位置而定)中添加:
-```
-XIAOMI_API_KEY=your_api_key_here
+```env
+# Xiaomi MiMo ASR
+XIAOMI_API_KEY=你的API_Key
XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
```
-> 如果你的 API Key 绑定的是国际站,用 `https://api.xiaomimimo.com/v1`
+> 中国站用户:`https://token-plan-cn.xiaomimimo.com/v1`
+> 国际站用户:`https://api.xiaomimimo.com/v1`
### 3. 修改 Hermes 配置
-在 `~/.hermes/config.yaml` 的 `stt` 部分:
+编辑 `D:\Program Files\Hermes\config.yaml`(或 `$env:HERMES_HOME\config.yaml`)的 `stt` 部分:
```yaml
stt:
@@ -51,27 +81,48 @@ stt:
providers:
xiaomi-asr:
type: command
- command: ~/.hermes/scripts/xiaomi-asr.sh {input_path} {output_path} mimo-v2.5-asr {language}
+ command: python C:/Users/<你的用户名>/.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
+> ```
+
### 4. 重启 Hermes
-- Desktop / TUI:关闭并重新打开
-- Gateway:运行 `/restart`
+- **Desktop**:关闭并重新打开
+- **Gateway**:运行 `/restart`
+
+---
## 验证
+创建一个测试音频文件并用脚本手动测试:
+
```bash
-# 手动测试脚本
-export XIAOMI_API_KEY=your_key
-export XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
-~/.hermes/scripts/xiaomi-asr.sh /path/to/audio.wav /tmp/output.txt mimo-v2.5-asr zh
-cat /tmp/output.txt
+# 创建 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
+
+# 查看结果
+cat output.txt
```
+---
+
## 注意事项
| 问题 | 说明 |
@@ -80,3 +131,16 @@ cat /tmp/output.txt
| 纯音乐 | 无法识别纯音乐内容,会返回空结果 |
| 语言 | 主要针对中文优化,其他语言效果较差 |
| ffmpeg | 非 wav/mp3 格式必须安装 ffmpeg |
+| Windows 路径 | 建议在配置中使用**正斜杠**(`/`),Python 脚本内部会自动转换反斜杠 |
+| 环境变量 | `XIAOMI_API_KEY` 必须在 `.env` 文件中,Hermes 会自动加载到子进程环境 |
+
+---
+
+## 文件清单
+
+```
+mimo-asr-for-hermes/
+├── INSTALL.md # 本安装说明
+├── xiaomi-asr.py # STT 适配脚本(Python)
+└── xiaomi-asr.sh # STT 适配脚本(Bash,仅 Linux/macOS)
+```
diff --git a/xiaomi-asr.py b/xiaomi-asr.py
new file mode 100644
index 0000000..5635e63
--- /dev/null
+++ b/xiaomi-asr.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+"""Xiaomi MiMo ASR wrapper for Hermes command STT provider.
+
+Usage (called by Hermes via placeholders):
+ xiaomi-asr.py <input_audio> <output_txt> [model] [language]
+
+Xiaomi ASR uses chat/completions API with input_audio format,
+NOT the standard /v1/audio/transcriptions endpoint.
+Only wav and mp3 are accepted — other formats are auto-converted
+to 16kHz mono WAV via ffmpeg.
+"""
+
+import base64
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import urllib.error
+import urllib.request
+import ssl
+
+
+def main():
+ if len(sys.argv) < 3:
+ print(f"usage: {sys.argv[0]} <input> <output> [model] [language]", file=sys.stderr)
+ sys.exit(1)
+
+ input_path = sys.argv[1]
+ output_path = sys.argv[2]
+ model = sys.argv[3] if len(sys.argv) > 3 else "mimo-v2.5-asr"
+ language = sys.argv[4] if len(sys.argv) > 4 else "zh"
+
+ api_key = os.environ.get("XIAOMI_API_KEY")
+ if not api_key:
+ print("XIAOMI_API_KEY is not set", file=sys.stderr)
+ sys.exit(1)
+
+ base_url = os.environ.get("XIAOMI_BASE_URL", "https://api.xiaomimimo.com/v1")
+
+ # Normalize Windows path: backslashes -> forward slashes
+ input_path = input_path.replace("\\", "/")
+
+ # Determine audio format from extension
+ ext = os.path.splitext(input_path)[1].lower()
+ if ext in (".wav",):
+ fmt = "wav"
+ audio_file = input_path
+ elif ext in (".mp3",):
+ fmt = "mp3"
+ audio_file = input_path
+ else:
+ # Auto-convert to WAV via ffmpeg
+ fmt = "wav"
+ try:
+ subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
+ except (FileNotFoundError, subprocess.CalledProcessError):
+ print(f"ffmpeg is required to convert {ext} to wav/mp3 for Xiaomi ASR",
+ file=sys.stderr)
+ sys.exit(1)
+
+ fd, audio_file = tempfile.mkstemp(suffix=".wav", prefix="hermes-asr-")
+ os.close(fd)
+ try:
+ subprocess.run(
+ ["ffmpeg", "-y", "-i", input_path, "-ar", "16000", "-ac", "1",
+ "-f", "wav", audio_file],
+ capture_output=True, check=True
+ )
+ except subprocess.CalledProcessError as e:
+ print(f"ffmpeg conversion failed: {e.stderr.decode(errors='replace')}",
+ file=sys.stderr)
+ os.unlink(audio_file)
+ sys.exit(1)
+
+ try:
+ # Read and encode audio
+ with open(audio_file, "rb") as f:
+ audio_b64 = base64.b64encode(f.read()).decode()
+
+ # Build request
+ payload = json.dumps({
+ "model": model,
+ "messages": [{
+ "role": "user",
+ "content": [
+ {"type": "input_audio", "input_audio": {"data": audio_b64, "format": fmt}},
+ ]
+ }]
+ }).encode()
+
+ req = urllib.request.Request(
+ f"{base_url}/chat/completions",
+ data=payload,
+ headers={
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ },
+ )
+ ctx = ssl.create_default_context()
+
+ resp = urllib.request.urlopen(req, context=ctx, timeout=300)
+ response_data = json.loads(resp.read().decode())
+
+ # Extract transcript
+ choices = response_data.get("choices", [])
+ if not choices:
+ error_info = response_data.get("error", {})
+ print(error_info.get("message", "No transcription result"), file=sys.stderr)
+ sys.exit(1)
+
+ msg = choices[0].get("message", {})
+ transcript = msg.get("content", "").strip()
+
+ if not transcript:
+ print("Xiaomi ASR returned empty transcript", file=sys.stderr)
+ sys.exit(1)
+
+ # Write output
+ with open(output_path, "w", encoding="utf-8") as f:
+ f.write(transcript)
+ f.write("\n")
+
+ print(transcript)
+
+ except urllib.error.HTTPError as e:
+ body = e.read().decode(errors="replace")
+ print(f"HTTP {e.code}: {body}", file=sys.stderr)
+ sys.exit(1)
+ except urllib.error.URLError as e:
+ print(f"URL error: {e.reason}", file=sys.stderr)
+ sys.exit(1)
+ finally:
+ # Clean up temp file if we created one
+ if 'audio_file' in locals() and audio_file != input_path and os.path.exists(audio_file):
+ os.unlink(audio_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/xiaomi-asr.sh b/xiaomi-asr.sh
index 532233d..2452c45 100644
--- a/xiaomi-asr.sh
+++ b/xiaomi-asr.sh
@@ -41,7 +41,7 @@ if [ "$FORMAT" = "wav" ] && [[ ! "$EXT" =~ ^[Ww][Aa][Vv]$ ]] && [[ ! "$EXT" =~ ^
fi
# Build JSON payload and call API using Python to avoid arg-length limits
-RESPONSE=$(python3 - "$AUDIO_INPUT" "$MODEL" "$FORMAT" "$API_KEY" "$BASE_URL" << 'PYEOF'
+RESPONSE=$(python - "$AUDIO_INPUT" "$MODEL" "$FORMAT" "$API_KEY" "$BASE_URL" << 'PYEOF'
import base64, json, sys, urllib.request, urllib.error, ssl
input_path = sys.argv[1]
@@ -83,7 +83,7 @@ PYEOF
)
# Extract the transcribed text from the response
-TRANSCRIPT=$(python3 -c "
+TRANSCRIPT=$(python -c "
import json, sys
resp = json.loads(sys.stdin.read())
choices = resp.get('choices', [])