From 582cf29169bceaed8754cf6f7fefcd37ac4506b8 Mon Sep 17 00:00:00 2001 From: ntzyz Date: Sun, 5 Jul 2026 18:40:58 +0800 Subject: support windows --- INSTALL.md | 110 +++++++++++++++++++++++++++++++++++---------- xiaomi-asr.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xiaomi-asr.sh | 4 +- 3 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 xiaomi-asr.py 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 [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]} [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', []) -- cgit v1.3.1