主题
模型与依赖管理
本文引用的源码文件
本文引用的文件
引言
语音输入的可用性由两件独立可安装的东西决定:运行时依赖(sherpa-onnx-node,约 300MB+)与识别模型包(每个几百 MB)。两者都刻意做成「按需下载、失败可降级、卸载不影响主链路」。
本文说明落盘布局、模型包的校验契约、下载与续传策略,以及依赖检测如何在「不想为未安装用户白付一次 ONNX 加载」这个前提下保持准确。
目录
落盘布局
~/.ai-helper-agent/
├── asr/
│ ├── asr-config.json # 用户配置(开关 / 语言 / 模型 id / 空闲卸载)
│ ├── hash-cache.json # 模型文件 sha256 缓存
│ └── models/<id>/ # 模型包:manifest.json + ONNX + tokens 等
│ └── manifest.json
├── asr-install-state.json # 依赖安装任务状态
└── asr-model-download-state.json# 模型下载任务状态模型目录固定不开放配置:用户需要的是「模型放哪」的可读提示,不是可选项。展示时把 home 前缀折叠为 ~,避免配置页出现含用户名的长绝对路径。
模型包契约
scanModels 只认 <models>/<id>/manifest.json。校验按顺序短路,第一个失败即返回原因:
| 顺序 | 检查 | 失败原因码 |
|---|---|---|
| 1 | manifest.json 存在 | manifest_missing |
| 2 | 是合法 JSON 且为对象 | manifest_invalid_json / manifest_not_object |
| 3 | schema === 1 | manifest_schema_unsupported |
| 4 | engine === 'sherpa-onnx' | engine_unsupported |
| 5 | id 非空 | manifest_invalid_id |
| 6 | kind ∈ {online, offline, vad} | manifest_invalid_kind |
| 7 | files 非空,且每项都在包目录内、存在、非空 | manifest_no_files / file_outside_pack / file_missing / file_empty |
| 8 | sha256(可选)与文件实际摘要一致 | file_unreadable / sha256_mismatch |
| 9 | 按 kind 必需的文件定位键齐全 | manifest_missing_model_or_tokens 等 |
file_outside_pack 这一条是安全边界:files 里写 ../../etc/passwd 会被拒绝,包只能引用自身目录内的文件。
sha256 字段是可选的——本地自建模型包往往没有现成摘要,此时跳过哈希校验但仍要求文件存在。manifest.schema 与 model-pack.js 的 MANIFEST_SCHEMA 必须一致,升级格式即等于拒绝旧包。
内置推荐目录
内置清单是自动下载的唯一事实来源,调用方不能传任意 id,因此不存在拼路径穿越的入口:
| id | kind | 用途 |
|---|---|---|
sense-voice-zh-en-ja-ko-yue | offline | 松手后整段高精度转写,自动带标点、数字规整 |
streaming-zipformer-bilingual-zh-en | online | 边说边出的低延迟草稿 |
下载端点默认 https://hf-mirror.com(官方站点在部分网络下直连不可达),可用 AI_HELPER_ASR_HF_ENDPOINT 指向官方站点或自建代理。每个文件的 size 与 sha256 都以本地实测跑通的包为准,不按上游目录动态推导——上游新增文件不应该改变加载参数。
清单之外还有一条完全离线的分发路径:在能上网的机器上备好同构目录,用「从文件夹导入」或直接拷进 models/。
下载与续传
| 机制 | 说明 |
|---|---|
| 逐文件下载 | 不走 .tar.bz2(Node 无内置 bzip2),HF 支持单文件 HTTP 取 |
| 临时目录 | 落 models/.<id>.part/;点开头的目录被扫描跳过,半成品对外不可见 |
| 原子发布 | 全部文件就绪后写 manifest.json,再 rename 成 models/<id>/ |
| 秒级续传 | 已存在且字节数与哈希都匹配的文件直接跳过,中断重试不从零开始 |
| 边写边哈希 | 逐块算 sha256 与写入同步,不额外读一遍盘;不匹配即删该文件 |
| 无数据超时 | 60 秒收不到数据即失败,不让用户干等 |
| 状态持久化 | 代理重启后把「运行中」收敛为「已中止」,不出现假进度 |
哈希缓存
单个模型包可达数百 MB,每次扫描全量重算不可接受。sha256OfFile 用 (path, size, mtime) 作缓存键,文件一变即失效,结果落 hash-cache.json。
计算本身是分块读:openSync + 4MB readSync 循环,内存峰值固定 4MB。整包 readFileSync 在 MAX_MODEL_FILE_BYTES(8GB)这种上限下会先撞上 Node 的 Buffer 上限(约 2GB)并抛 ERR_FS_FILE_TOO_LARGE,而这里只需要顺序过一遍算摘要。
依赖检测
detect.js 与 RAG 的检测刻意不同,原因是 ASR 依赖的 ONNX 运行时加载即常驻数百 MB:
- 零成本磁盘判定:
asr-config.json或node_modules/sherpa-onnx-node/package.json任一缺失 → 直接判不可用,不起子进程。 - 子进程确认:只有「看起来装过」才
spawn探测脚本require一次,超时 3 秒(RAG 是 60 秒,但这里只require一个包)。 - 不自动重装:只记录
everSucceeded,状态里提示「依赖已被清理,请点击重新安装」。
AI_HELPER_ASR_DEPS=0 会强制判为不可用,且磁盘判定也同步返回 false,避免状态里出现「ready=false 但 packageOnDisk=true」的矛盾组合。
依赖一键安装
安装清单来自 agent 包自身 package.json 的 asrDependencies(当前为 sherpa-onnx-node: ^2.0.0),不接受调用方传入包名:
| 设计点 | 说明 |
|---|---|
| 显式 spec | 命令携带由清单构建的 pkg@spec,不接受外部输入,无注入面 |
| 无 shell | 非 Windows 严格 shell: false;Windows 优先用当前 Node 直执行 npm-cli.js,探测不到才降级并加引号 |
| 单任务 + 安装锁 | 同一时刻仅一个安装任务,超时上限 10 分钟 |
| 进程组杀 | POSIX 以 detached: true 启动,终止走进程树组杀 |
| 状态持久化 | asr-install-state.json,重启后「运行中」收敛为「已中止」 |
| 完成后复检 | 在子进程重新探测,防「装上但加载失败」的假成功 |
--with-asr 安装参数(install.sh)走的是同一条清单,区别只是发生在 Agent 首次安装期间。
环境变量
| 变量 | 作用 |
|---|---|
AI_HELPER_ASR_ROOT | 覆盖 ASR 数据根目录(测试隔离 / 自定义位置) |
AI_HELPER_ASR_DEPS | 置 0 强制判依赖不可用 |
AI_HELPER_ASR_HF_ENDPOINT | 覆盖模型下载端点 |
AI_HELPER_ASR_MAX_RECORDING_MS | 单次录音上限,默认 300000 |
AI_HELPER_ASR_SESSION_IDLE_MS | 会话空闲回收,默认 30000 |
AI_HELPER_ASR_MAX_SESSIONS | 并发会话上限,默认 1 |
AI_HELPER_ASR_RSS_LIMIT | RSS 保护阈值,默认 3.5GB |
AI_HELPER_ASR_OFFLINE_TIMEOUT_MS | 单次定稿推理超时,默认 180000 |
结论
模型与依赖管理遵守一条共同原则:失败要可解释、可重试、且不污染主链路。所以每个校验失败都有明确原因码、下载中断可以续传、安装失败不会留下半成品目录、依赖缺失时按钮直接隐藏而不是点了报错。新增模型来源时,应复用这套契约而不是另开一条路径。