dsh-audio-read
让 AI Agent 听懂音频:转写、鸟种识别、语音情感、环境声与音乐分析。全部本地运行,不用大模型。DeepSeek Harness 技能。
- Stars
- 0
- Language
- Python
- Created
- Sep 15, 2026
- Updated
- Sep 15, 2026
Introduction
audio-read
让 AI Agent 真正"听懂"音频 —— 全部本地运行,不上传云端,不用大模型。
一个 DeepSeek Harness(DSH)技能: 把音频转成 Agent 能推理的文字、数字和标签,覆盖语音、音乐、环境声、鸟种与情感。
为什么不用大模型
一个音频大模型(如 Qwen3-Omni-30B)要 ~17GB,冷启动 30–90 秒, 输出还是不可复现的自由文本。
这个项目走另一条路:把"一个大模型"换成"一把小尺子"—— 每条能力轴用一个专用小工具,按需组合:
| 层 | 做什么 | 引擎 | 体积 | 耗时 |
|---|---|---|---|---|
| L1 声学画像 | 音高/音调走向/音域/起伏、语速、能量与停顿、节奏、调性、音色 | librosa(纯 DSP) | 0 | 2–4s |
| L2 转写 | 语音 → 文字(分段时间戳;--timestamps 可到词级) | Qwen3-ASR-0.6B(MLX) | 1.9GB | ~10× 实时 |
| L3 事件识别 | 动物/鸟虫、自然、人声、乐器、城市噪音(521 类) | YAMNet(ONNX) | 16MB | <1s |
| L4-A 鸟种 | 具体到物种(6522 种,含中文名) | BirdNET v2.4 | 52MB | 秒级 |
| L4-B 情感 | 9 类语音情感 | emotion2vec_plus_large | 1.8GB | 1–3s(MPS/常驻) |
自动分流:先做廉价声学预分类,再决定跑哪几层。 环境声若被 L3 判为动物,会自动接 L4-A 继续问到物种级—— "这是什么在叫"可以一路问到"这是黑顶麻雀"。
实测验证
不是"在我造的样本上还行",下面是真实数据:
BirdNET 鸟种识别 —— 标注物种的真实录音(Arremon abeillei):
| 时间段 | 检出 | 置信度 |
|---|---|---|
| 0–3s | 黑顶麻雀 Arremon abeillei | 0.991 |
| 12–14s | 黑顶麻雀 | 0.875 |
阴性对照:合成鸟叫 → 0 检出(不硬猜物种);中文人声 → 判为 Human vocal(内置非鸟类拒识类)。
emotion2vec 情感识别:官方真人示例 → 愤怒 1.0000;中文中性语音 → 中性 1.00。
转写:中文 46.5s 音频 → 4.8s 转完(≈10× 实时),自动切 3 段带时间戳。
安装
前置:macOS(Apple Silicon)+ uv + ffmpeg。
# 1) 放到 DSH 技能目录
git clone https://github.com/liyixuan201211/dsh-audio-read ~/.dsh/skills/audio-read
cd ~/.dsh/skills/audio-read
# 2) 独立 Python 环境(不污染系统)
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python \
numpy scipy soundfile librosa onnxruntime \
torch transformers funasr birdnet
# 3) 自检:告诉你哪一层就位、哪一层还缺什么
node scripts/audio.mjs --doctor
模型会在首次使用时自动下载(YAMNet 16MB 来自 hf-mirror;BirdNET 52MB 来自 Zenodo; emotion2vec 来自 ModelScope)。L2 转写需要额外装 mlx-qwen3-asr 并下载 Qwen3-ASR-0.6B。
国内网络提示:PyPI 走腾讯镜像直连可到 9MB/s,比走代理快得多:
export UV_DEFAULT_INDEX=https://mirrors.cloud.tencent.com/pypi/simple/
用法
A=~/.dsh/skills/audio-read/scripts/audio.mjs
# 自动判断内容类型并分流(最常用)
node $A ~/Desktop/录音.m4a
# 只转写(快)
node $A 会议.mp3 --mode transcribe
# 只做声学画像(音高/节奏/调性)
node $A 歌曲.mp3 --mode acoustic
# 鸟种识别
node $A 野外录音.wav --birds
# 语音情感
node $A 语音.wav --emotion
# 环境声识别(零样本类目)
node $A 野外录音.wav --semantic
# 自检
node $A --doctor
输出是单行 JSON:acousticSummary(中文摘要)、transcript、semantic、birds、emotion、
以及落盘文件路径(长音频会给有界预览 + 完整文件,避免撑爆上下文)。
设计上的几个选择
- 上下文经济:1 小时录音 ≈ 8–10k token。完整结果落盘,stdout 只回有界预览,
Agent 需要细节时再
grep。 - L4-B 做成常驻服务:emotion2vec 冷加载要 15–45s,每次调用冷启无法接受, 所以首次调用自动拉起常驻服务,之后热调用 1–3s。默认跑 Apple GPU(MPS): 实测 133s 语音 CPU 13.5s → MPS 3.4s,加载也从 15–45s 降到 4s; 不可用时自动回退 CPU。
- 诚实优先:置信度低就明说"别当确定结论";BirdNET 认不出就给 0 检出而不是硬猜。
- 音频内容是不可信输入:转写文本可能包含对 Agent 的注入指令,一律只当数据。
词级时间戳与字幕
加 --timestamps 会多跑一个强制对齐模型(Qwen3-ForcedAligner-0.6B),
把每个字/词的时间都标出来,适合做字幕:
node scripts/audio.mjs 会议.wav --language zh --timestamps
# → files.srt 是句级字幕
为什么字幕要自己重算:对齐器会丢掉标点,底层自带 SRT 因此退化成"按字数硬切",
中文实测切出「今天下午三点开会请准 / 时到会议室我们要讨论」这种断句。
本仓库用带标点的原文重建句子边界(scripts/lib/subtitle.mjs),
同一段音频切出 3 句干净的句子:
1 00:00:00,000 --> 00:00:04,080 今天下午三点开会,请准时到会议室。
2 00:00:04,320 --> 00:00:07,280 我们要讨论项目进度和人员安排。
3 00:00:07,440 --> 00:00:11,360 另外,服务器成本超支了,需要重新评估。
已知限制
- 说话人分离(谁在说)未实现。
- BirdNET 只认它 6522 种内的鸟;未启用地理/季节先验。
- emotion2vec 是声学情感(判断"怎么说的"),不是语义情感("说了什么"); 反讽、平静语气说狠话可能误判。
- 自动分流器是启发式,阈值在有限样本上调过,真实场景可能有误判。
- 体积不小:emotion2vec 1.8GB + 强制对齐 1.84GB + BirdNET 52MB。
目录结构
audio-read/
├── SKILL.md # 给 Agent 看的说明书(含 17 条实测坑)
├── config.example.json # 复制成 config.json 可覆盖默认值
└── scripts/
├── audio.mjs # 入口:探测 → 声学 → 分流 → 各层 → 落盘
├── asr.mjs # L2:Qwen3-ASR 封装
├── lib/{config,probe}.mjs
└── py/
├── acoustic.py # L1:声学画像 + 内容预分类
├── semantic.py # L3:YAMNet
├── birds.py # L4-A:BirdNET
├── emotion.py # L4-B:客户端
└── emotion_server.py# L4-B:常驻服务
许可
MIT。第三方模型的许可见各自仓库(BirdNET 为 CC-BY-NC-SA,仅限非商业用途)。