Skip to content

模型与依赖管理 ​

本文引用的源码文件

本文引用的文件

引言 ​

语音输入的可用性由两件独立可安装的东西决定:运行时依赖(sherpa-onnx-node,约 300MB+)与识别模型包(每个几百 MB)。两者都刻意做成「按需下载、失败可降级、卸载不影响主链路」。

本文说明落盘布局、模型包的校验契约、下载与续传策略,以及依赖检测如何在「不想为未安装用户白付一次 ONNX 加载」这个前提下保持准确。

目录 ​

  1. 落盘布局
  2. 模型包契约
  3. 内置推荐目录
  4. 下载与续传
  5. 哈希缓存
  6. 依赖检测
  7. 依赖一键安装
  8. 环境变量
  9. 结论

落盘布局 ​

~/.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。校验按顺序短路,第一个失败即返回原因:

顺序检查失败原因码
1manifest.json 存在manifest_missing
2是合法 JSON 且为对象manifest_invalid_json / manifest_not_object
3schema === 1manifest_schema_unsupported
4engine === 'sherpa-onnx'engine_unsupported
5id 非空manifest_invalid_id
6kind ∈ {online, offline, vad}manifest_invalid_kind
7files 非空,且每项都在包目录内、存在、非空manifest_no_files / file_outside_pack / file_missing / file_empty
8sha256(可选)与文件实际摘要一致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,因此不存在拼路径穿越的入口:

idkind用途
sense-voice-zh-en-ja-ko-yueoffline松手后整段高精度转写,自动带标点、数字规整
streaming-zipformer-bilingual-zh-enonline边说边出的低延迟草稿

下载端点默认 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:

  1. 零成本磁盘判定:asr-config.json 或 node_modules/sherpa-onnx-node/package.json 任一缺失 → 直接判不可用,不起子进程。
  2. 子进程确认:只有「看起来装过」才 spawn 探测脚本 require 一次,超时 3 秒(RAG 是 60 秒,但这里只 require 一个包)。
  3. 不自动重装:只记录 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_LIMITRSS 保护阈值,默认 3.5GB
AI_HELPER_ASR_OFFLINE_TIMEOUT_MS单次定稿推理超时,默认 180000

结论 ​

模型与依赖管理遵守一条共同原则:失败要可解释、可重试、且不污染主链路。所以每个校验失败都有明确原因码、下载中断可以续传、安装失败不会留下半成品目录、依赖缺失时按钮直接隐藏而不是点了报错。新增模型来源时,应复用这套契约而不是另开一条路径。