feat(asr): 本地语音转写接入为一级路由 + 并行工作流合并提交
按用户指示做**一包提交**,不按工作流拆分。本提交刻意混合了多条并行线:
· 本地 ASR 接管:audio 成为与 chat/embed/image/video 同等的路由类别
(IsLocalRoute 单一判据、audio 健康探测、default_audio_route、
auto 占位、GET /api/ai/routes/audio、回退云端时界面明示「音频已出网」)
· LLM 调用层:ctx 贯穿、ToolCall/ToolSchema、EmptyCompletionError /
TransientUpstreamError(按错误类型而非文案判重试)
· 编排 Agent:general_assistant orchestrate/persistence/spec_driver
· 联网搜索:internal/search(playwright)
· 网盘:backend + 前端
· 前端 UI:导航/路由/工作台若干页
· 交付文档:DELIVERY.md / AR04 / 部署文档的「无 Python」表述据实改写,
新增 eai_agentplatform-asr.service、asr.env、clonezilla-cleanup 清 ~/asr-poc
不分拆的原因:dev 早期,粒度不该打断工作节奏。且实测过——这些改动
**在编译上是同一个单元**(llm.go 的 ctx 签名变更牵动 12 个调用点,
chat_message.go 的 ctx 改动又与编排重写同处一个 hunk),拆出来的中间态编不过。
详见 TOP_CODING_RULES.md G14.5 与 bugs_and_errors.md E09。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# bugs and errors
|
||||
|
||||
> **用途**:记录开发过程中**真实发生过**的错误与坑。
|
||||
> 每条必须能回答四件事:**症状 → 根因 → 怎么修 → 怎么早点发现**。
|
||||
>
|
||||
> 不写流水账,不写「今天很顺利」。这份文件的价值只在于:下一个踩同一个坑的人,
|
||||
> 能在五分钟内认出自己遇到的是同一件事,而不是花两小时重新推导一遍。
|
||||
>
|
||||
> 关联 `TOP_CODING_RULES.md`(G02 错误不得静默、G04 完成判定靠事实)。
|
||||
|
||||
## 索引
|
||||
|
||||
| # | 日期 | 区域 | 一句话 |
|
||||
|---|---|---|---|
|
||||
| E01 | 2026-09-26 | 本地 ASR 依赖 | `torchaudio>=2.9` 删了 pyannote 3.x 要的三个 API,装完 `import` 才炸 |
|
||||
| E02 | 2026-09-26 | 模型下载 | HuggingFace 不通 + pyannote repo gated,hf-mirror 403,只有魔搭能下 |
|
||||
| E03 | 2026-09-26 | 模型下载 | `jonatasgrosman/…-chinese-zh-cn` 没有 `model.safetensors`,只有 `.bin` |
|
||||
| E04 | 2026-09-26 | 依赖安装 | whisperx 的依赖树(lightning/optuna/opentelemetry)解析十几分钟不收敛 |
|
||||
| E05 | 2026-09-26 | 进度测量 | `/proc/PID/io` 的 `read_bytes` **不算 socket 读**,据此误判「下载卡死」 |
|
||||
| E06 | 2026-09-26 | 本地 ASR 显存 | 8G 卡被 llama-server 占了 5G,large-v3 的 float16 一加载就 CUDA OOM |
|
||||
| E07 | 2026-09-26 | 本地 ASR 加载 | pyannote 的 `from_pretrained` **只认文件不认目录**,给目录被当成 HF repo id |
|
||||
| E08 | 2026-09-26 | 本地 ASR 加载 | torch≥2.6 把 `weights_only` 默认翻成 True,pyannote 旧权重直接拒载 |
|
||||
|
||||
---
|
||||
|
||||
## E01 `torchaudio>=2.9` 删了 pyannote 3.x 要的 API
|
||||
|
||||
**日期**:2026-09-26 **区域**:`~/asr-poc` 本地 ASR 环境 **影响**:整个本地 ASR 跑不起来
|
||||
|
||||
### 症状
|
||||
|
||||
装完一切正常(`Successfully installed …` 无任何警告),第一次导入就硬失败:
|
||||
|
||||
```
|
||||
$ ./venv/bin/python -c "import pyannote.audio"
|
||||
File ".../pyannote/audio/__init__.py", line 29, in <module>
|
||||
from .core.inference import Inference
|
||||
File ".../pyannote/audio/core/inference.py", line 35, in <module>
|
||||
from pyannote.audio.core.io import AudioFile
|
||||
File ".../pyannote/audio/core/io.py", line 60, in <module>
|
||||
) -> torchaudio.AudioMetaData:
|
||||
AttributeError: module 'torchaudio' has no attribute 'AudioMetaData'
|
||||
```
|
||||
|
||||
当时的版本:`torch 2.14.0` / `torchaudio 2.11.0` / `pyannote.audio 3.4.0`。
|
||||
|
||||
### 根因
|
||||
|
||||
`pyannote.audio` 3.x 有三处依赖 torchaudio 的 **backend-dispatch I/O API**:
|
||||
|
||||
| 位置 | 用的是 |
|
||||
|---|---|
|
||||
| `pyannote/audio/core/io.py:60` | `torchaudio.AudioMetaData` 作返回注解 |
|
||||
| `pyannote/audio/core/io.py:81` | `torchaudio.list_audio_backends()` |
|
||||
| `pyannote/audio/core/io.py:85` | `torchaudio.info(...)` |
|
||||
|
||||
而 **torchaudio 从 2.9 起把这套 API 整体移除了**(I/O 交给 `torchcodec`),只留下 `load`。
|
||||
本机实测 torchaudio 2.11.0:
|
||||
|
||||
```python
|
||||
>>> [n for n in ('load','info','list_audio_backends','AudioMetaData') if hasattr(torchaudio,n)]
|
||||
['load']
|
||||
```
|
||||
|
||||
**修复后拿到了 torchaudio 官方的一手佐证** —— 降到 2.8.0 之后 import pyannote.audio,
|
||||
它自己把这条弃用警告打了出来,等于承认了根因:
|
||||
|
||||
```
|
||||
UserWarning: torchaudio._backend.list_audio_backends has been deprecated. This deprecation
|
||||
is part of a large refactoring effort to transition TorchAudio into a maintenance phase.
|
||||
The decoding and encoding capabilities of PyTorch for both audio and video are being
|
||||
consolidated into TorchCodec. ... It will be removed from the 2.9 release.
|
||||
```
|
||||
|
||||
「It will be removed from the 2.9 release」—— 正是我们装的 2.11 里发生的事。
|
||||
|
||||
两个放大伤害的细节:
|
||||
|
||||
1. `io.py` 没有 `from __future__ import annotations`,所以第 60 行的**注解在 import 时求值** ——
|
||||
不是用到才炸,是 `import pyannote.audio` 这一行就炸。
|
||||
2. 这决定了 **任何 pyannote.audio 3.x 都没救**,不是某个小版本的问题。
|
||||
要么降 torchaudio,要么整个换到 pyannote 4。
|
||||
|
||||
### 为什么会踩
|
||||
|
||||
- 最初是裸装:`pip install torch torchaudio faster-whisper "pyannote.audio>=3.1,<4" …`
|
||||
—— **torch/torchaudio 没钉版本**,拿到当时最新的 2.14.0 / 2.11.0。
|
||||
- pip 全程没有任何警告,`Successfully installed` 一长串看着完全正常。
|
||||
- 讽刺的是:正因为**绕开 whisperx 手工装**(见 E04)才丢掉了版本约束 ——
|
||||
而 whisperx 自己钉的是 `torch~=2.8.0`,等于生态早就知道该用 2.8。
|
||||
|
||||
### 修法
|
||||
|
||||
钉到 pyannote 3.4 那一代的组合:
|
||||
|
||||
```bash
|
||||
pip install --index-url https://mirrors.aliyun.com/pypi/simple \
|
||||
"torch==2.8.0" "torchaudio==2.8.0"
|
||||
```
|
||||
|
||||
**没走的那条路**:升到 `pyannote.audio 4.0.7`(它已不再依赖 torchaudio)。
|
||||
`pip install --dry-run` 的结果:
|
||||
|
||||
```
|
||||
Would install … opentelemetry-api/-sdk/-proto/-exporter-*(10 个)
|
||||
pyannoteai-sdk-0.4.0 torchcodec-0.16.0 safetensors-0.8.0
|
||||
pyannote-core-6.0.1 pyannote-database-6.1.1 pyannote-metrics-4.1
|
||||
```
|
||||
|
||||
否决理由:多出十几个包,其中 **`pyannoteai-sdk` 是厂商云 SDK、opentelemetry 是遥测上报** ——
|
||||
与「100% 本地化部署」的原则相抵;且 4.x 吃不吃 `version: 3.1.0` 的 config **没验证过**,
|
||||
正好是当初选 pyannote 3.x 时就想避开的那个未知数。
|
||||
|
||||
### 怎么早点发现
|
||||
|
||||
**装完立刻 import 一遍,别等跑数据。**
|
||||
|
||||
```bash
|
||||
./venv/bin/python -c "import torch, faster_whisper, pyannote.audio; print(torch.__version__, pyannote.audio.__version__)"
|
||||
```
|
||||
|
||||
一秒的事,能省掉 4.2GB venv + 4.1GB pip 缓存 + 约 2.5GB 重下的往返。
|
||||
已固化成 `~/asr-poc/selfcheck.sh`,装完/重建后跑一次。
|
||||
|
||||
**通用教训**:GPU 生态里「装成功」和「能用」是两件事 ——
|
||||
`pip install` 只证明包落盘了,不证明 ABI/API 对得上。
|
||||
凡是带 C 扩展的包(torch / torchaudio / ctranslate2 / torchcodec),装完必须真 import 一次。
|
||||
|
||||
---
|
||||
|
||||
## E02 HuggingFace 不通 + gated,只有魔搭能下
|
||||
|
||||
**日期**:2026-09-26 **区域**:模型下载
|
||||
|
||||
**症状**:`huggingface.co` 上 `pyannote/segmentation-3.0` 等 repo 是 **gated**(需登录并接受条款),
|
||||
本机连 `huggingface.co` 都连不上(curl 返回 `000`);退而用 `hf-mirror.com` 代理,**一律 403**。
|
||||
|
||||
**根因**:gated 是账号态的授权,代理站点没有你的 HF 账号,自然过不了。
|
||||
|
||||
**修法**:改用 **`modelscope.cn`(魔搭)** —— 它把这些 gated repo 整个镜像到了**官方命名空间**
|
||||
(路径就是 `pyannote/segmentation-3.0`),且在魔搭上不 gated。
|
||||
|
||||
同一文件同一时刻的实测:
|
||||
|
||||
| 站点 | 结果 |
|
||||
|---|---|
|
||||
| `huggingface.co` | `000`(不通) |
|
||||
| `hf-mirror.com` | `403` |
|
||||
| `www.modelscope.cn` | `206`(分片续传正常) |
|
||||
|
||||
**注意**:**gated 不等于许可变更**(许可仍是 MIT / CC-BY-4.0),
|
||||
但镜像确实绕过了「在 HF 上点接受条款」那一步,交付前值得再确认一次(关联 G19.10 / P06.11)。
|
||||
|
||||
**怎么早点发现**:探测顺序应该是「官方 → 官方镜像 → 国内镜像」,而不是「官方失败就放弃」。
|
||||
且要用**具体文件**去探,不是探首页 —— 首页 200 不代表文件可下(hf-mirror 就是首页通、文件 403)。
|
||||
|
||||
---
|
||||
|
||||
## E03 这个中文对齐 repo 没有 `model.safetensors`
|
||||
|
||||
**日期**:2026-09-26 **区域**:模型下载
|
||||
|
||||
**症状**:按惯例去取 `model.safetensors` → 404。
|
||||
|
||||
**根因**:`jonatasgrosman/wav2vec2-large-xlsr-53-chinese-zh-cn`
|
||||
**只发布了 `pytorch_model.bin`**,没有 safetensors 版本。
|
||||
|
||||
**修法**:下 `pytorch_model.bin`。已写进 `download_models.sh` 的注释与下载清单,
|
||||
免得下一个人照 safetensors 的惯例又踩一次。
|
||||
|
||||
**怎么早点发现**:下之前先列 repo 文件清单
|
||||
(魔搭 `…/repo/files?Revision=master`),别按别的 repo 的文件名去猜。
|
||||
|
||||
---
|
||||
|
||||
## E04 whisperx 的依赖树解析不收敛
|
||||
|
||||
**日期**:2026-09-26 **区域**:依赖安装
|
||||
|
||||
**症状**:`pip install whisperx` 跑了 **16 分钟以上**仍在 `Resolving dependencies`,
|
||||
没有失败、没有输出进度,看起来像卡死。
|
||||
|
||||
**根因**:whisperx 3.8.6 依赖 `pyannote-audio>=4.0.0` 与 `torch~=2.8.0`,
|
||||
连带拖进 `lightning` / `optuna` / `opentelemetry` / `aiohttp` 一大棵树,回溯空间极大。
|
||||
|
||||
**修法**:放弃 whisperx,只装实际用到的那几个,并**逐条钉版本**:
|
||||
|
||||
```
|
||||
torch==2.8.0 torchaudio==2.8.0 faster-whisper pyannote.audio>=3.1,<4
|
||||
fastapi uvicorn python-multipart
|
||||
```
|
||||
|
||||
说话人分离用 pyannote 直接做,不需要 whisperx 那层封装。
|
||||
|
||||
**怎么早点发现**:`pip install` 长时间无输出就是危险信号。此时应看
|
||||
`/tmp/pip-unpack-*/` 目录是否在长(见 E05),而不是凭感觉判断「卡死了」。
|
||||
|
||||
---
|
||||
|
||||
## E05 `/proc/PID/io` 的 `read_bytes` 不算 socket 读
|
||||
|
||||
**日期**:2026-09-26 **区域**:进度测量(**我自己犯的错**)
|
||||
|
||||
**症状**:pip 下载中,我读 `/proc/<pid>/io` 的 `read_bytes` 得到 0 KB/s,
|
||||
据此向用户报告「下载卡死了」,并切了一次源。
|
||||
|
||||
**根因**:`read_bytes` 统计的是**块设备 I/O**,即真正落到磁盘的字节。
|
||||
pip 是边下边写临时文件,套接字收包不计入该字段 —— 这个数字在下载期间本来就接近 0,
|
||||
它**不是**一个「下载进度」指标。
|
||||
|
||||
**修法**:量 pip 的下载进度要看**临时目录体积的增长**:
|
||||
|
||||
```bash
|
||||
du -sb /tmp/pip-unpack-*/ # 隔几秒看两次,差值才是真实速度
|
||||
```
|
||||
|
||||
**后果**:因这一次误判,我多发了一次无谓的切换(源其实没问题)。
|
||||
结论是「源慢」而不是「源死」,两者的处置完全不同。
|
||||
|
||||
**怎么早点发现**:用指标前先确认这个指标**定义的是什么**。
|
||||
一个恒为 0 的读数,先怀疑指标选错了,再怀疑被测对象。
|
||||
|
||||
---
|
||||
|
||||
## E06 8G 显存被 llama-server 占掉 5G,large-v3 装不下
|
||||
|
||||
**日期**:2026-09-26 **区域**:本地 ASR(`~/asr-poc`)
|
||||
|
||||
**症状**:`selfcheck.sh` 全绿、模型文件也都在位,一跑就炸:
|
||||
|
||||
```
|
||||
File "/home/eaiadmin/asr-poc/asr_core.py", line 71, in _fw_model
|
||||
_FW_CACHE["m"] = WhisperModel(str(FW_DIR), device="cuda", compute_type="float16", ...)
|
||||
File ".../faster_whisper/transcribe.py", line 689, in __init__
|
||||
self.model = ctranslate2.models.Whisper(...)
|
||||
RuntimeError: CUDA failed with error out of memory
|
||||
```
|
||||
|
||||
**根因**:不是代码问题,是卡上真的没地方了。`nvidia-smi` 实测:
|
||||
|
||||
```
|
||||
memory.total 8192 MiB, memory.used 5106 MiB, memory.free 2681 MiB
|
||||
3418, /app/llama-server, 4778 MiB
|
||||
3449, /app/llama-server, 232 MiB
|
||||
```
|
||||
|
||||
那 5 GiB 是仓库 README 里 8080/8081 那两个**常驻 llama.cpp 服务**占的 ——
|
||||
属于用户的服务,不是我们这次起的。而 large-v3 的 float16 权重本身就要 3 GB。
|
||||
|
||||
**修法**:不动别人的进程,改自己的取用方式(默认 `int8_float16`,实测 2.5 GiB 空闲下装得下,
|
||||
质量损失可接受;`ASR_COMPUTE_TYPE=float16` 可在显存宽裕时调回):
|
||||
|
||||
```python
|
||||
_COMPUTE_LADDER = [os.environ.get("ASR_COMPUTE_TYPE", "int8_float16"), "int8"]
|
||||
```
|
||||
|
||||
连带的第二个坑:**whisper 和 pyannote 不能同时在卡上**。
|
||||
只剩两三百 MB 余量,两个都驻留必炸 —— 而这两步本来就是先后关系。
|
||||
所以转写完立刻 `_free_fw()`(`gc.collect()` + `torch.cuda.empty_cache()`)再上 pyannote。
|
||||
实测释放后空闲回到 2.50 GiB,分离正常。
|
||||
|
||||
**怎么早点发现**:**报「CUDA out of memory」时第一件事是 `nvidia-smi` 看谁占着**,
|
||||
而不是去调自己的 batch size。本机只有 8G 且常年被别的服务占着,
|
||||
「模型本身装不下」这种情况会反复出现,值得一开始就把 `mem_get_info()` 打进日志
|
||||
(现在加载前会打一行 `当前空闲显存 x.xx GiB`)。
|
||||
|
||||
---
|
||||
|
||||
## E07 pyannote 的 `from_pretrained` 只认文件,不认目录
|
||||
|
||||
**日期**:2026-09-26 **区域**:本地 ASR 加载
|
||||
|
||||
**症状**:把 HF repo id 换成本地绝对路径(一个**目录**)之后,加载报的错看着像路径写错了,
|
||||
其实完全不是那个意思:
|
||||
|
||||
```
|
||||
HFValidationError: Repo id must be in the form 'repo_name' or 'namespace/repo_name':
|
||||
'/home/.../models/pyannote/segmentation-3.0'. Use `repo_type` argument if needed.
|
||||
```
|
||||
|
||||
**根因**:pyannote 判「这是本地路径还是 HF repo id」**只看它是不是一个文件**:
|
||||
|
||||
- `core/model.py:588` —— `if os.path.isfile(checkpoint): path_for_pl = checkpoint`
|
||||
- `core/pipeline.py:78` —— `if os.path.isfile(checkpoint_path): config_yml = checkpoint_path`
|
||||
|
||||
目录两条都不成立,于是落到 else 分支去当 repo id 校验,报出上面那句
|
||||
「repo id 格式不对」——**它压根没打算读目录**。
|
||||
|
||||
**修法**:给到**文件**。管线给 `config.yaml`,子模型给 `pytorch_model.bin`:
|
||||
|
||||
```python
|
||||
Pipeline.from_pretrained(str(DIA_DIR / "config.yaml")) # 管线:config 文件
|
||||
Model.from_pretrained(checkpoint=str(SEG_DIR / "pytorch_model.bin")) # 子模型:权重文件
|
||||
```
|
||||
|
||||
config.yaml 里那两项也得跟着变成 dict 形式(`getter.py:81` 走
|
||||
`Model.from_pretrained(**dict)` 那一支):
|
||||
|
||||
```yaml
|
||||
segmentation:
|
||||
checkpoint: <dir>/pytorch_model.bin
|
||||
```
|
||||
|
||||
**而且不能给 `hparams_file`** —— 权重里自带 pytorch-lightning 的 hparams,
|
||||
repo 里那份 `config.yaml` 是模型结构配置、没有 `task:` 段,塞进去只会换来
|
||||
`ConfigAttributeError: Missing key setup / full_key: task.setup`。
|
||||
|
||||
**怎么早点发现**:**路径参数报「格式不对」,先去看那个函数的判定条件是什么**,
|
||||
别顺着报错字面去改路径写法(我第一反应是路径要加引号/要相对路径,全错)。
|
||||
另外:`Pipeline.from_pretrained` 有**两个**同名参数路径,管线和模型各判各的文件类型,
|
||||
改了一处不代表另一处也通了 —— 这次就是改完管线还在模型那处炸。
|
||||
|
||||
---
|
||||
|
||||
## E08 torch≥2.6 的 `weights_only` 默认值翻了个面
|
||||
|
||||
**日期**:2026-09-26 **区域**:本地 ASR 加载
|
||||
|
||||
**症状**:路径问题解决后,加载 pyannote 权重时换了一个完全不同的错:
|
||||
|
||||
```
|
||||
_pickle.UnpicklingError: Weights only load failed. ...
|
||||
(1) In PyTorch 2.6, we changed the default value of the `weights_only` argument
|
||||
in `torch.load` from `False` to `True`. ...
|
||||
WeightsUnpickler error: Unsupported global: GLOBAL torch.torch_version.TorchVersion
|
||||
was not an allowed global by default.
|
||||
```
|
||||
|
||||
**根因**:torch 2.6 起 `torch.load` 的 `weights_only` 默认由 `False` 翻成 `True`,
|
||||
pickle 里没在白名单上的全局符号一律拒载。pyannote 3.4 的 `.bin` 里存着 4 个数据类,
|
||||
逐个不在默认白名单里 —— 而且它是**一次只报一个**,放行一个再报下一个。
|
||||
|
||||
**修法**:不要图省事用 `weights_only=False`(那等于把反序列化变成任意代码执行),
|
||||
而是把这 4 个**数据类**逐个放行:
|
||||
|
||||
```python
|
||||
torch.serialization.add_safe_globals([
|
||||
torch.torch_version.TorchVersion,
|
||||
pyannote.audio.core.task.Specifications,
|
||||
pyannote.audio.core.task.Problem,
|
||||
pyannote.audio.core.task.Resolution,
|
||||
])
|
||||
```
|
||||
|
||||
放行的全是数据类、没有可调用对象;权重是我们自己下的、过了 SHA256SUMS,
|
||||
不存在「不可信来源」的问题。
|
||||
|
||||
**怎么早点发现**:这 4 个是**实测枚举出来的**(捕获异常 → 正则抠出 `GLOBAL x.y.Z` →
|
||||
import 回来 → 再放行 → 重试,直到加载成功),不是猜的。下次遇到同类拒载,
|
||||
可以用同一招把清单跑出来,别一个个手试。
|
||||
|
||||
**遗留疑问**:这 4 个是**这一次加载路径**上遇到的;换模型/换 pyannote 版本可能还要加。
|
||||
所以 `selfcheck.sh` 里那条「模型文件在位」之外,**首次加载必须真跑一次 diarize** ——
|
||||
只 import 不加载是查不出来的(import 一路都是好的)。
|
||||
|
||||
---
|
||||
|
||||
## 待沉淀(还没写进规则的)
|
||||
|
||||
- [ ] **装完必须导入自检**:带 C 扩展的包(torch / torchaudio / ctranslate2)装完立刻 import 一次(见 E01)。
|
||||
是否升格为 `TOP_CODING_RULES.md` 的一条(放在 G19 依赖那节,或新开一条「依赖装完必须自检」)待定。
|
||||
- [ ] **E08 的通用形状**:`torch.load(weights_only=True)` 是 2.6 之后的默认行为,
|
||||
凡是加载 2.6 之前产出的 `.bin` / `.ckpt` 都会撞上,且与具体项目无关 ——
|
||||
候选升格为 P06 的一条(「旧权重在新 torch 上要先放行 unpickle 白名单」)。
|
||||
- [ ] **E06 的通用形状**:GPU 是共享资源,跑之前先 `nvidia-smi` 看**别人**占了多少,
|
||||
再决定自己的精度档位 —— 候选升格为 P06 的一条。
|
||||
Reference in New Issue
Block a user