主题
语音输入(本地 STT)
本文引用的源码文件
本文引用的文件
引言
语音输入(本地 STT)让用户用说的代替打字:在侧边栏点 🎤 说话,识别结果只写进输入框草稿,不会自动发送,也不触发任何工具调用。识别完全在本地 Agent 进程内跑 ONNX 模型,音频不出本机、不写盘。
它与 RAG、长期记忆一样属于可选组件:默认关闭,未配置时扩展端不会尝试加载任何语音相关资源,未安装依赖的用户不会为 ONNX 运行时多付一次常驻内存。
目录
设计目标与边界
| 目标 | 落地方式 |
|---|---|
| 音频不出本机 | 采集、编码、推理全在本机;不上传、不写临时文件 |
| 装了不该自动生效 | DEFAULT_ASR_CONFIG.enabled = false,且 UI 门控要求三项同时就绪 |
| 不拖累主链路 | 定稿推理在独立 worker 线程,Agent 事件循环不被阻塞 |
| 出错不装死 | 依赖缺失 → 按钮直接隐藏;推理失败 → 退回流式文本 |
| 内存可控 | 会话空闲回收、录音时长上限、RSS 保护、引擎空闲卸载 |
明确不做的事:
- 不做自动重装:
sherpa-onnx-node包体约 300MB+,静默重装会在用户无感知时拉满带宽,只记录everSucceeded并提示用户点击「重新安装」。 - 不做侧边栏麦克风授权弹窗:Chromium 在侧边栏 / 弹窗形态下不允许弹授权框,授权入口放在选项页的标签页上下文里。
- 不接管发送:识别结果只作为草稿插入,是否发送由用户决定。
三段式门控
🎤 按钮是否出现,由三个彼此独立的信号共同决定,任一为假即隐藏(避免「点了才报错」):
deps.ready(依赖就绪) × modelsReady(模型就绪) × enabled(用户开关)| 信号 | 事实来源 | 说明 |
|---|---|---|
deps.ready | AI_HELPER_ASR_DEPS 未置 0 且磁盘存在 node_modules/sherpa-onnx-node/package.json,并通过子进程探测 | 零成本磁盘判定在前,子进程 require 确认在后 |
modelsReady | /api/asr/status 返回的能力汇总 capability.offline || capability.online | 至少有一个 offline(定稿)或 online(流式)模型包通过校验 |
enabled | asr-config.json 的 enabled 字段 | 默认 false,只能由用户显式打开 |
三者由代理端在 /api/asr/status 里汇总成 available 一次,扩展端直接采信;只缺一环时 reason 会区分 deps_missing / models_missing / disabled,配置页据此提示补哪一项。
门控状态在扩展端由 refreshGating() 拉取,点 🎤 前会再复查一次,避免状态陈旧。
启用流程
- 安装依赖:选项页「语音」标签页显示检测结果,缺失时点「一键安装」;等价命令行是
aha asr install,或在安装 Agent 时加--with-asr。 - 准备模型:把模型包放进
~/.ai-helper-agent/asr/models/<id>/(每包一个目录,内含manifest.json),可点「扫描」重扫,或用「从文件夹导入」;也可点「下载」从内置目录拉取。 - 打开开关:在「语音」标签页打开总开关,按需选识别语言(
auto/ 中文 / 英文 / 日文 / 韩文 / 粤语)。 - 授权麦克风:点「去授权」,在打开的标签页里允许一次;未授权时录音条里也会出现「去授权」按钮。
三项就绪后,侧边栏输入框右侧出现 🎤。
使用体验
| 动作 | 行为 |
|---|---|
点 🎤 / 按 Alt+V | 开始录音,录音条展开(音量柱 + 计时 + 状态文本 + 取消 / 完成) |
| 说话过程中 | 流式草稿实时刷新(服务端节流 150ms),已定稿片段不再回改 |
按 Alt+V / 点「完成」 | 停止采集并发起定稿,用整段高精度模型重转一遍 |
按 Esc / 点「取消」 | 放弃本次录音,草稿不落输入框 |
| 静音超时 | 5 分钟上限自动结束并定稿 |
录音条的键盘可达性有两处刻意处理:
- 隐藏前先把焦点交还输入框(
releaseBarFocus())。点「取消 / 完成」时焦点留在按钮上,若紧接着给整条设aria-hidden="true",Chrome 122+ 会拦截并告警Blocked aria-hidden on an element because its descendant retained focus,键盘与读屏用户的焦点还会被悬空。 role="status"/aria-live="polite"只挂在状态文本#voiceStatusText上,而不是整条录音条。否则每 200ms 刷新的计时器会被读屏反复朗读,形成刷屏。
配置项
用户配置落在 ~/.ai-helper-agent/asr/asr-config.json(缺失即视为从未配置,一律按未启用处理):
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled | false | 总开关 |
language | auto | 取值 auto / zh / en / ja / ko / yue |
streamingModelId | null | 流式草稿模型包 id;null 表示按能力自动选第一个可用包 |
offlineModelId | null | 定稿模型包 id;同上 |
idleUnloadMs | 600000 | 引擎空闲卸载毫秒数,0 表示不卸载 |
写入走「临时文件 + rename」,避免读到写一半的 JSON;读取时对类型不符的字段一律回落默认值(sanitizeAsrConfig)。
接口一览
HTTP 端点(均需 Bearer Token):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/asr/status | 依赖 / 模型 / 开关 / 已加载引擎状态 |
| POST | /api/asr/config | 修改开关、语言、模型 id、空闲卸载 |
| GET | /api/asr/models | 已扫描模型包列表(非法包附原因) |
| POST | /api/asr/models/scan | 重新扫描模型目录 |
| POST | /api/asr/models/pick | 从文件夹导入模型包 |
| DELETE | /api/asr/models/:id | 删除模型包 |
| POST | /api/asr/models/download | 按内置目录下载模型 |
| GET | /api/asr/models/download/status | 下载进度 |
| POST | /api/asr/models/download/cancel | 取消下载 |
| GET | /api/asr/models/catalog | 内置推荐模型清单 |
| POST | /api/asr/transcribe | 整段音频一次性转写 |
| POST | /api/asr/warmup | 预热引擎 |
| POST | /api/asr/deps/install | 一键安装依赖 |
| GET | /api/asr/deps/install/status | 安装进度 |
| POST | /api/asr/deps/detect | 重新检测依赖 |
WebSocket:ws://127.0.0.1:18910/ws/asr/:sessionId,鉴权与 /ws/exec 一致(Header 优先,回退 ?token=),依赖不可用时直接 503 拒连。帧协议与定稿流程见 流式会话与定稿引擎。
数据与隐私
~/.ai-helper-agent/asr/
├── asr-config.json # 用户配置
├── hash-cache.json # 模型文件 sha256 缓存(按 path+size+mtime 失效)
└── models/<id>/ # 模型包(manifest.json + ONNX 文件)音频只在内存中流转:采集为 16kHz PCM,切片后经 WebSocket 上送,既不落盘也不出网。识别文本只写进侧边栏输入框,随会话内容进入正常的对话存储路径。
资源控制
| 机制 | 约束 |
|---|---|
| 单次录音上限 | MAX_RECORDING_MS,默认 5 分钟 |
| 会话空闲回收 | SESSION_IDLE_MS,默认 30 秒无帧即释放 |
| 并发会话 | MAX_SESSIONS,默认 1(第二个请求直接回 busy) |
| RSS 保护 | 超过 RSS_LIMIT_BYTES(默认 3.5GB)跳过 offline 定稿,改用流式文本(fallback: true) |
| 引擎空闲卸载 | idleUnloadMs 默认 10 分钟 |
| 依赖探测开销 | 零成本磁盘判定优先;需子进程确认时超时 3 秒 |
故障排查
🎤 按钮不出现:三项门控里至少有一项为假。打开选项页「语音」Tab,按提示补齐依赖、模型或开关。
提示没有麦克风权限:侧边栏形态下无法弹授权框,去选项页点「去授权」,在标签页里允许一次。
模型包显示「非法」:manifest.json 校验失败,常见原因是 schema 不是 1、engine 不是 sherpa-onnx、kind 不在 online / offline / vad 内、files 列出的文件缺失或为空、sha256 与文件实际摘要不符。错误原因随列表一并返回。
连接被拒(503):依赖不可用。此时扩展端不会开流式通道,需先完成依赖安装。
定稿文本明显弱于预期:可能命中 RSS 保护退化为流式文本。查看 /api/asr/status 的已加载引擎与最近一次结果标记。
结论
语音输入的工程重心不在「接一个 ASR 模型」,而在可选性与不打扰:默认关闭、三段式门控、隐藏而非报错、音频不出本机、推理不占主线程、内存有明确上限。按此边界,它可以作为一个可插拔的输入方式长期存在,而不成为扩展主链路的负担。