Bundle
dsh-voice-transcribe
给 dsh 的本地语音/视频转写:把 QQ/微信那种 SILK 语音解出来,再用 SenseVoice(sherpa-onnx,int8)转成文字,全程不花 API 钱。
- Source
- JackZo400
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-voice-transcribe
[English](README.en.md) | 简体中文
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)用的**本地语音/视频转写**插件:
让 Agent「听得见」——把一个音频或视频文件交给它,它把里面的话转成文字。
引擎是 **SenseVoice**(sherpa-onnx 跑 int8 onnx)。**全程本地跑,不花 API 钱**,
约 0.1 秒 / 条,模型 239 MB。
> **同类里更成熟的选择**:桌面麦克风那类(比如 [likhonmain/voice-input](https://github.com/likhonmain/voice-input))比这份成熟得多——
> 你要的是「对着电脑说话、转成字」,去装那些。
> 我们这份专做**别人在 QQ / 微信里发过来的语音**:SILK 解码、QQ 文件头多出来的那个字节、本地 SenseVoice 转写,一条龙。
> 还有一条事实要说清:**官方 QQ 机器人路线不需要这一套**——平台自带识别文字(`asr_refer_text`);只有个人号路线(OneBot:NapCat / Lagrange / SnowLuma)才用得着它。
---
## 为什么需要它
**它真正解决的问题,不是「转写」。**
调模型谁都会。麻烦的是**前面那一步**,尤其是中文 IM 生态里:
### 坑 1:QQ / 微信发出来的语音是 SILK,不是 amr
QQ 里真人语音的文件头是 `#!SILK_V3`,但文件名常写着 `.amr`。
你以为喂给 ffmpeg 就行——**不行**。
### 坑 2:大多数 ffmpeg 构建根本没有 SILK 解码器
```bash
$ ffmpeg -decoders | grep silk
# (空)
$ ffmpeg -i voice.amr out.wav
# Invalid data found when processing input
```
看着像文件坏了,其实是没人认这个格式。于是你开始怀疑是不是文件截断了、
是不是要用 QQ 的私有库——都不是。
### 坑 3:QQ 存下来的文件常常在最前面多一个字节
用十六进制看:
```
00000000: 0223 2153 494c 4b5f .#!SILK_
```
那个 `02` 是 QQ 自己塞的(`03` 也见过)。**不剥掉它,连专门的 SILK 解码库都会拒绝你的文件。**
这三件事都在 `py/silk.py` 里处理掉了——用 `pilk`(纯 Python,不需要编译任何东西)解成 PCM,
自己封标准 WAV。顺手把一个 `.amr` 丢进去也能用,`is_silk()` 会告诉你它到底是什么。
更完整的"踩坑笔记"(适用范围、怎么给 SILK 写一个不作假的自检)在 [docs/silk-notes.md](docs/silk-notes.md)。
### 那用哪个引擎?
whisper 谁都会调,我们一开始也是它。同一条 1.7 秒的真实群语音:
whisper-medium 试了 5 组参数(beam 1/5 × 词表提示 / 口语 / 无提示),**全部**听岔;
SenseVoice 一次就对,和官方转写**一字不差**。速度也不是一个量级:约 0.1 秒 / 条(whisper 5-8 秒),
模型 239 MB int8(whisper medium 约 1.5 GB)。
所以这个插件里**只有 SenseVoice 一条路**:whisper 的开关、参数、依赖都删干净了,
不留半截开关让人以为还能切回去。数字都在下面「测试(实测数据)」里。
## 安装
**1. 装插件**
```bash
dsh plugin --profile web add github:JackZo400/dsh-voice-transcribe
```
**2. 装 Python 那半边**(装在 `pythonPath` 指的那个解释器里)
```bash
pip install -r py/requirements.txt # sherpa-onnx + numpy + pilk
```
**镜像提醒:清华源(`pypi.tuna.tsinghua.edu.cn`)里没有 `sherpa-onnx` 这个包**,会直接报
`No matching distribution found`(实测过)。装不上就换阿里源或官方源:
`pip install -i https://mirrors.aliyun.com/pypi/simple -r py/requirements.txt`
**3. 系统里要有 ffmpeg**(非 SILK 的音频/视频靠它转码)
**4. 下 SenseVoice 模型**(约 163 MB 的压缩包,解开是 239 MB 的 int8 模型)
```bash
curl -LO https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2
tar xjf sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2
```
解出来的 `sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17/` 就是模型目录,
里面要有 `model.int8.onnx` 和 `tokens.txt`。**放哪都行**,但要写进配置的 `modelDir`
(或环境变量 `AILIN_ASR_SV_DIR`)——代码里没有任何写死的路径。
这个模型认中文(普通话)/ 英文 / 日文 / 韩文 / 粤语五种语言,
想换别的版本看 [sherpa-onnx 的模型发布页](https://github.com/k2-fsa/sherpa-onnx/releases/tag/asr-models)。
## 配置
```yaml
- insert:
- id: voice-transcribe
name: dsh-voice-transcribe
config:
pythonPath: python3 # 装了上面那些包的解释器
# scriptPath: '' # 留空 = 用包内自带的 py/transcribe.py
modelDir: /path/to/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17
maxSeconds: 120 # 只转前 N 秒
language: auto # auto / zh / en / ja / ko / yue
fix: '' # 专有名词纠错表,见下
maxFiles: 4 # 一次最多转几个(模型只加载一次)
```
**`modelDir`(或环境变量 `AILIN_ASR_SV_DIR`)**:模型目录在哪由你说了算,代码不猜路径。
没配的话转写会明确报「没给 SenseVoice 模型目录」,不会静默失败。
**`fix`(或环境变量 `AILIN_ASR_FIX`)**:专有名词纠错表。SenseVoice **没有词表接口**
(whisper 的 `initial_prompt` 那种),名字听岔了只能在结果上纠一道。格式 `错=对,错=对`:
```yaml
fix: '小张=张三,星海=星海项目'
```
表是**从左往右**替换的,短词会吃掉长词的前缀——**长的写前面**。
默认是**空表**:谁的名字谁自己填。
## 用法
### 在 dsh 里(Agent 自己调)
装好之后 Agent 多一个工具 `transcribe_media`:
```
把 /tmp/voice.amr 转成文字
```
返回每个文件一条结果:`file` / `ok` / `text` / `language` / `duration`。
一次也可以给多个文件——**模型只加载一次**,批量比一个个转快得多。
别的插件想用,可以调服务:
```js
const svc = ctx.get('voiceTranscribe')
const { results } = await svc.transcribe(['/tmp/voice.amr'])
```
### 单独用(不需要 dsh)
只要 SILK 解码:
```bash
python py/silk.py --check voice.amr # 先看看是不是 SILK
python py/silk.py voice.amr --out-dir out/ # → out/voice.wav(16k 单声道)
```
要转写:
```bash
python py/transcribe.py zh.wav --language zh --model-dir /path/to/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17
# {"file": "zh.wav", "dur": 5.59, "ok": true, "text": "开饭时间早上9点至下午5点。", "engine": "sensevoice", "lang": "zh", "sec": 1.0, "load_sec": 0.7, "total_sec": 1.0}
```
(上面这行输出是模型自带测试音频 `test_wavs/zh.wav` 的真实结果,你可以自己复现。)
`transcribe.py` 的输出是**一行 JSON**,方便被任何程序调用(dsh 插件就是这么接的):
出错时退出码仍是 0,错误放在 JSON 的 `error` 字段里。
## 测试
```bash
python py/test_silk.py # SILK 识别的逻辑(不需要真语音样本,秒级)
python py/test_transcribe.py # 转写那条路:参数/纠错/懒加载;没模型会自动跳过
python py/test_silk_real.py # 用 pilk 现造真 SILK,验解码和 QQ 那个 0x02 前缀
node test/plugin-selftest.mjs # 插件接线(用假转写脚本,不需要模型)
```
`py/test_silk.py` 覆盖的就是那三个坑:干净头、`0x02`/`0x03` 前缀、空文件、
mp3 头、短文件不崩、不是 SILK 时抛错。
`py/test_transcribe.py` 不需要模型也不需要联网:先验参数解析和纠错表,
再拿桩替掉 `sherpa_onnx` 走一遍完整流程(顺便证明它真的是**用到才 import**),
最后**装了 sherpa-onnx 且配了模型目录才会**再跑一遍真模型。
没有模型、没装包的环境下它只打 `SKIP` 并说明原因——**不会假装通过**。
`py/test_silk_real.py` 补的是前两个够不着的那一段:仓库里不带音频,它就用 `pilk` 的 encoder
**现造**一个真 SILK(音源优先取 SenseVoice 模型包自带、可公开分发的测试音频,没有就自己合成一段音调),
再验 QQ 那个「前面多一个 `0x02`」的坑——人为加一个字节后解出来的 PCM 和不加那次**逐字节一致**;
装了 sherpa-onnx 且配了模型目录,它还会把解出来的声音真转一遍。缺依赖时同样只打 `SKIP`
并说清缺什么,退出码仍是 0。
想看真效果,拿手上任意一条 QQ/微信语音跑 `python py/silk.py 你的文件`。
## 测试(实测数据)
本机(14 核 CPU、int8)真实语音样本:
| 项目 | 数字 |
| --- | --- |
| 模型体积 | 239 MB(int8 onnx;whisper medium 约 1.5 GB) |
| 模型加载 | 约 0.9 秒(一个进程只加载一次;whisper medium 约 1.6 秒) |
| 同一条 1.7 秒真实群语音 | 约 0.1 秒出字,一次就对(whisper-medium 试了 5 组参数全听岔) |
| SILK 解码 | 约 0.2 秒 / 条 |
| 3.6 秒真实语音 | 出字:「那样人太刷屏了 我直接给踢了不是说了吗」 |
那条 1.7 秒的语音,SenseVoice 和 QQ 官方转写**一字不差**;whisper-medium 的 5 组参数一组都没对。
**零 API 花费**,费的是 CPU。CPU 越强越快。
## 已知局限
- **只吃本地文件**。URL 请先自己下载——插件不替你做网络请求(少一个 SSRF 面)。
- **靠子进程**:模型崩了、超时了都杀得掉,但它不共享内存,每次调用有一次 Python 启动开销。
- **视频只转音轨**,不抽帧——画面理解是另一件事。
- **纯静音 / 纯音乐不保证是空串**:SenseVoice 的幻觉比 whisper 收敛得多,但实测给一段纯数字静音,
它偶尔还是会冒一两个没意义的字。空的就如实是空的,但别把「有字」当成一定有话。
- **SenseVoice 没有词表接口**:专有名词只能靠 `fix` 那张表兜,得自己攒。
- **长音频靠切片**:超过 28 秒会在最安静的地方切片再拼;切点挑得再小心也偶尔会把词切开。
- **只认 5 种语言**:中文(普通话)/ 英文 / 日文 / 韩文 / 粤语,别的语言请另找模型。
## License
MIT © 2026 JackZo400
---
## English
→ Full English README: [README.en.md](README.en.md)
Install
dsh plugin --profile web add github:JackZo400/dsh-voice-transcribe
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-voice-transcribe from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.