# Mac 本地 ASR 迁移计划:Whisperer 流式 + 词库分层 > **文档状态**:实施计划(**已评审,决策已冻结**) > **适用范围**:macOS 本地听写(`OSGKeyboardMac`) > **关联文档**:[`local-asr-architecture.md`](./local-asr-architecture.md) > **创建日期**:2026-07-14 --- ## 1. Executive Summary ### 1.1 目标 将 macOS 本地听写从 **Sherpa offline CLI 分块架构** 迁移到 **Qwen3-ASR + mlx-audio-swift 真流式引擎**(Whisperer 路线),并完整支持: 1. **iOS 同源用户自定义词库**(`PersonalDictionary`:term / aliases / iCloud) 2. **系统预设增强词库**(`phrases.tsv` → `BuiltinLexiconIndex`) 3. **分层 bias**:ASR soft prompt → 别名纠错 → Polish 保真 4. **按住说话实时出字**(边说边 partial,松开后短 tail + final) ### 1.2 核心结论 | 决策 | 选择 | |------|------| | **引擎** | `Blaizzy/mlx-audio-swift`(`MLXAudioSTT` + `StreamingInferenceSession`) | | **模型** | Qwen3-ASR 0.6B 4-bit(默认)/ 1.7B 4-bit(高质量档) | | **热词模式** | `promptOnly`(`StreamingConfig.context`),**禁用** Sherpa hard hotwords | | **词库适配** | 复用现有 `LocalASRBiasAdapter`,不新建平行词库系统 | | **废弃** | Sherpa Qwen3 offline 子进程 + `ChunkedUtterancePipeline` 本地主线(**catalog 直接移除**,不保留 advanced) | | **partial UI** | **仅 overlay 预览**;松开后润色完成再注入前台 App | | **默认模型** | 0.6B 4-bit 默认;1.7B 可选下载(设置 UI 已有) | ### 1.3 非目标(本期不做) - 不替换 iOS 键盘扩展的本地/云 ASR 路径 - 不把 1 万 builtin 词全量塞进 ASR prompt - 不恢复 Sherpa `--qwen3-asr-hotwords` 作为默认 - 不引入 Python sidecar(`mlx-qwen3-asr` HTTP worker)作为默认 - 不强制实现 Typeflux 式自动词库学习(仅预留接口) - 不在 Linux CI 上跑 MLX 集成测试(见 `AGENTS.md`) --- ## 2. 背景与现状差距 ### 2.1 当前 Mac 本地路径(问题) ```text Option 按下 → MacAudioRecorder → liveCaptureTask + ChunkedUtterancePipeline → MacSherpaONNXRunner(每 chunk 起子进程) → --qwen3-asr-hotwords(hard hotwords,易静音幻觉) → 首 partial ~2.5s,松开后队列拖很久 ``` | 问题 | 根因 | |------|------| | 松开后仍录/识别很久 | 无 tail drain;chunk 队列串行跑完 | | 静音喷热词 | hard hotwords + 无 VAD 门控 | | 实时预览慢 | `firstChunkDurationSeconds=2.5` + offline 子进程 | | 词库未完整消费 | `LocalASRBiasAdapter` 已生成 payload,Sherpa 只用 `hardHotwords`,忽略 `promptBias` / correction / polish | ### 2.2 目标 Mac 本地路径 ```text Option 按下 → resolveLocalBias()(PersonalDictionary + BuiltinLexiconIndex) → Qwen3ASRModel.fromPretrained()(常驻) → StreamingInferenceSession(context: promptBias) → 100ms feedAudio → displayUpdate → UI partial +(可选)增量插入 Option 松开 → FlowCaptureTailDrain(250–500ms) → session.stop() → final text → LocalASRTranscriptCorrector(aliases) → PolishingService(polishFragment) → 注入前台 App ``` ### 2.3 与 iOS 词库的关系 | 数据源 | Mac 读取方式 | 用途 | |--------|--------------|------| | `AppGroupStore.personalDictionary` | iCloud KVS + 本地 | 用户 term / aliases | | `phrases.tsv`(bundle 资源) | `BuiltinLexiconIndex.shared` | 系统增强词库 Top-N | | `PersonalDictionary+ASRBias` | 经 `LocalASRBiasAdapter` | prompt / correction / polish | **Mac 与 iOS 共用同一套词库数据与 adapter 逻辑**,不 fork 词库模型。 --- ## 3. 目标架构 ### 3.1 分层图 ```mermaid flowchart TB subgraph Input["输入层"] Mic["MacAudioRecorder 16kHz mono"] Dict["PersonalDictionary"] Lex["BuiltinLexiconIndex"] AppCtx["MacAppContextService.frontAppBundleId"] end subgraph Bias["词库适配层(Shared)"] Adapter["LocalASRBiasAdapter"] Payload["LocalASRBiasPayload"] end subgraph Engine["引擎层(新增)"] Model["Qwen3ASRModel 常驻"] Stream["StreamingInferenceSession"] Config["StreamingConfig.context + language"] end subgraph Post["后处理层(Shared)"] Hallu["stripHallucinations"] Corrector["LocalASRTranscriptCorrector"] Polish["PolishingService"] end subgraph UI["产品层(Mac)"] VM["MacDictationViewModel"] Pipe["MacDictationPipeline"] Insert["MacTextInserter"] end Mic --> Stream Dict --> Adapter Lex --> Adapter AppCtx --> Adapter Adapter --> Payload Payload -->|promptBias| Config Config --> Stream Model --> Stream Stream -->|partial/final| VM Stream --> Hallu --> Corrector --> Polish --> Insert Payload -->|correctionPairs| Corrector Payload -->|polishFragment| Polish ``` ### 3.2 Provider 抽象(建议) 新增 `MacMLXStreamingASRProvider`,实现窄接口: ```swift protocol MacStreamingASRSession: Sendable { func feed(samples: [Float]) func stop() async -> String func cancel() var events: AsyncStream { get } } protocol MacStreamingASRProviding { func prepare() async throws func makeSession(bias: LocalASRBiasPayload, locale: Locale) throws -> any MacStreamingASRSession } ``` Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。 ### 3.3 词库 payload 消费矩阵 | `LocalASRBiasPayload` 字段 | Whisperer/MLX 路线 | 说明 | |---------------------------|-------------------|------| | `promptBias` | `StreamingConfig.context` | 用户词 + builtin Top-N(≤800 字符) | | `hardHotwords` | **丢弃** | 禁用,防幻觉 | | `correctionPairs` | `LocalASRTranscriptCorrector` | aliases 确定性替换 | | `polishFragment` | `PolishingService` supplement | builtin 补充 + 专名保真 | | `diagnostics` | `LocalASRBiasDiagnosticsStore` | 设置页展示 | --- ## 4. 分阶段实施计划 ### Phase 0 — 决策冻结与文档(0.5 周当量) **产出** - [ ] 评审并批准本文档 - [ ] 更新 [`local-asr-architecture.md`](./local-asr-architecture.md) 核心结论:默认引擎改为 mlx-audio-swift,Sherpa hard hotwords 降级为 advanced/deprecated - [ ] 确认 `LocalASRCapabilities.qwen3MLX.supportsStreaming = true`(类型层) **验收** - 团队对引擎选型、词库分层、非目标无歧义 --- ### Phase 1 — 引擎依赖与 mlx-audio-swift patch(P0) **目标**:Mac target 可链接 MLX,streaming 支持 `context`。 **任务** 1. **SPM 依赖**(`project.yml`) - 添加 `Blaizzy/mlx-audio-swift`(`MLXAudioSTT`, `MLXAudioCore`) - 确认 Metal Toolchain / `mlx.metallib` 打包流程(参考 Whisperer `make bundle`) - 更新开源许可 catalog 2. **Fork 或 upstream PR:`StreamingConfig.context`** - 在 `StreamingConfig` 增加 `public var context: String?` - `QwenStreamingInferenceSessionCore` 两处 `buildPrompt` 传入 `context` - 若短期无法 upstream:vendored fork pin 到 OSG 分支 3. **模型 catalog 更新**(`local-asr-catalog.json`) - 新增条目:`qwen3-mlx-0.6b-4bit`、`qwen3-mlx-1.7b-4bit` - `backend: mlx`,`hotwordMode: promptOnly` - 默认模型 ID 从 `sherpa-qwen3-0.6b-int8` 改为 `qwen3-mlx-0.6b-4bit` - **移除**所有 Sherpa 模型与 runtime 条目(不保留 advanced / fallback) 4. **新文件骨架** - `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` - `OSGKeyboardMac/MacMLXStreamingSession.swift` - `OSGKeyboardMac/MacHallucinationFilter.swift`(移植 Whisperer 逻辑) **验收** - macOS 上 `Qwen3ASRModel.fromPretrained` 加载成功 - 固定 WAV 文件 batch transcribe 有输出 - 传入 `context: "Cursor, SwiftUI"` 时专名召回优于无 context(人工 smoke) **风险** - `project.yml` 当前零 SPM;首次引入 MLX 需验证 XcodeGen + 签名 + metallib - GitHub Actions macOS runner Xcode 版本(见 `AGENTS.md` CI 说明) --- ### Phase 2 — 流式会话 + 词库接入(P0) **目标**:按住 Option 时真流式 partial;词库经 `LocalASRBiasAdapter` 注入。 **任务** 1. **`MacMLXStreamingASRProvider`** - 模型常驻 singleton(类似 VoxFlow `SpeechSwiftQwen3ModelCache`) - Metal warmup:1s 静音 pre-transcribe(学 MacWispr) - `makeSession(bias:locale:)` → `StreamingConfig(context: bias.promptBias, language: ...)` 2. **替换 `MacDictationViewModel` 录音循环** - 删除/绕开 `liveCaptureTask` + `ChunkedUtterancePipeline` 本地路径 - 学 Whisperer `AppState`: - 按下:创建 session + 100ms `feedAudio` timer - 监听 `session.events` → 更新 overlay `transcript` / `isStreamingPartial`(**仅预览,不插入前台 App**) - 松开:进入 Phase 3 tail drain → 润色 → 再注入 3. **`MacDictationPipeline.resolveLocalBias`** - 已有实现保留;capabilities 改为 `.qwen3MLX` - 确保 `hardHotwords` 不被下游使用 4. **语言映射** - 复用 `MacQwen3LanguageHint.from(locale:)` 或 mlx-audio 语言别名表 **验收** - 按住说话 **< 1s** 出现 partial(M 系列,0.6B 模型) - 设置页 diagnostics 显示 `promptBiasLength > 0`、用户词/builtin 计数 - 用户词典中的 term 在 prompt 中可见(debug log) --- ### Phase 3 — 松开截断 + 后处理链(P0) **目标**:解决「松开后仍录很久」;完整词库后处理。 **任务** 1. **接入 `FlowCaptureTailDrain`**(Shared,iOS 已有) - Mac `MacAudioRecorder` 松开后启用 tail drain - 参数:`silenceDurationSeconds: 0.25–0.5`,`maxDrainSeconds: 0.5–1.0` - drain 期间继续 `feedAudio`;超时硬截断 2. **Session 结束** - `session.stop()` → 等待 `.ended(fullText:)` - `MacHallucinationFilter.strip`(CJK filler、空白标记) 3. **后处理链**(对齐 iOS 云路径语义) ```text raw → stripHallucinations → LocalASRTranscriptCorrector.apply(pairs: bias.correctionPairs) → PolishingService(mergedDictionaryBlock + polishFragment) → final transcript ``` 4. **取消路径** - Option 二次按下 / Esc:`session.cancel()` + 清空 partial **验收** - 松开后 **≤ 500ms** 进入 finalizing(正常环境) - aliases 纠错:`k8s` → `Kubernetes`(单元 + 人工) - 静音段不喷词库词(hard hotwords 已禁用) --- ### Phase 4 — VAD / 静音门控(P1) **目标**:减少静音 chunk 送模型、降低幻觉与算力浪费。 **任务** 1. RMS 门控(轻量,先上) - feed 前检测 chunk RMS < threshold 则跳过(学 VoxFlow `silencePeakThreshold: 0.0005`) 2. 可选:Silero VAD(speech-swift 或自研) - 仅在 Phase 4 评估后引入;避免双引擎依赖膨胀 3. **热词 dump 过滤器**(Shared utility) - 若 final 文本主要由词库词构成且音频能量低 → 丢弃 **验收** - 静音按住 3s 无输出 / 不插入 - 嘈杂环境 tail drain 不无限拖 --- ### Phase 5 — 设置、迁移与 fallback(P1) **任务** 1. **`MacLocalASRModelSettingsView`** - 模型档:0.6B(默认)/ 1.7B - 流式窗口:`streamingWindow` 0.5–3s(默认 1–2s) - Bias diagnostics:userTermCount / builtinTermCount / truncated / promptLength 2. **用户迁移** - 已选 `sherpa-qwen3-*` 的用户自动映射到 `qwen3-mlx-0.6b-4bit` - 首次启动提示:本地引擎升级,需下载 MLX 权重 3. **Fallback 链** ```text qwen3-mlx 失败 → Apple Speech(现有 MacSpeechLocalASR) 模型缺失 → 引导下载 / 云模式 ``` (无 Sherpa 回退) 4. **Sherpa 代码清理** - 删除 `MacSherpaONNXRunner` / `MacSherpaLocalASR` / Sherpa runtime 下载逻辑 - `MacLocalASRChunkAdapter` 仅保留 cloud chunked 路径(若仍需要) **验收** - 老用户升级后默认本地模式仍可用 - 设置页可看到词库注入诊断 --- ### Phase 6 — 评测、灰度与文档(P2) **任务** 1. **评测集**(见 §6) 2. **Benchmark 脚本**(可选,学 MacWispr `bench.sh`) - latency RTF、首 partial 延迟、专名召回 3. **CHANGELOG** 双语条目 4. **用户文档**:本地模式说明、词库如何生效、与云模式差异 **灰度** - 内部 dogfood → 默认开启 MLX 本地 → 移除 Sherpa 默认(保留 advanced) --- ## 5. 文件级变更清单 ### 5.1 新增 | 文件 | 职责 | |------|------| | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | 模型加载、session 工厂、bias→config | | `OSGKeyboardMac/MacMLXStreamingSession.swift` | 封装 `StreamingInferenceSession` + event 转发 | | `OSGKeyboardMac/MacHallucinationFilter.swift` | 静音幻觉过滤 | | `OSGKeyboardMac/MacStreamingAudioFeeder.swift` | 100ms 定时 feed + RMS 门控 | | `OSGKeyboardTests/MacMLXStreamingASRTests.swift` | mock / 可选 macOS CI smoke | ### 5.2 修改 | 文件 | 变更 | |------|------| | `project.yml` | SPM:`mlx-audio-swift`;Copy Bundle Resources 不变 | | `OSGKeyboardShared/Resources/LocalASR/local-asr-catalog.json` | MLX 模型条目 + 默认 ID | | `OSGKeyboardShared/Models/LocalASRCapabilities.swift` | `qwen3MLX.supportsStreaming = true` | | `OSGKeyboardMac/MacDictationViewModel.swift` | 流式 session 生命周期 | | `OSGKeyboardMac/MacDictationPipeline.swift` | 本地路径走 MLX;后处理链接 | | `OSGKeyboardMac/MacLocalASRService.swift` | 新 backend `mlxQwen3` | | `OSGKeyboardMac/MacAudioRecorder.swift` | tail drain 钩子 | | `OSGKeyboardMac/MacLocalASRModelSettingsView.swift` | 模型档 + diagnostics | | `docs/local-asr-architecture.md` | 与本文对齐 | ### 5.3 删除(Sherpa 路径) | 文件 | 说明 | |------|------| | `OSGKeyboardMac/MacSherpaONNXRunner.swift` | 移除 | | `OSGKeyboardMac/MacSherpaLocalASR.swift` | 移除 | | `local-asr-catalog.json` 中 Sherpa runtime / 模型条目 | 移除 | | `MacLocalASRChunkAdapter.swift`(Sherpa 专用部分) | 移除或仅留 cloud | | Mac 本地 `ChunkedUtterancePipeline` 调用 | 移除 | --- ## 6. 词库策略(详细) ### 6.1 输入与优先级 与 [`local-asr-architecture.md` §7.3](./local-asr-architecture.md) 一致: ```text 用户高频 / usageCount > PersonalDictionary.effectiveEntries(含 systemEntries) > 当前 App 相关 builtin(computer_terms 子集) > BuiltinLexiconIndex.topTerms(weight≥4, limit=300) > 其余 builtin(仅 polish,不进 ASR) ``` ### 6.2 参数默认值 | 参数 | 值 | 来源 | |------|-----|------| | `hotwordMode` | `.promptOnly` | `LocalASRCapabilities.qwen3MLX` | | `maxPromptCharacters` | **800** | 现有 capabilities;POC 可调 500–800 | | `builtinASRLimit` | **300** | `LocalASRBiasRequest` 默认 | | `builtin in prompt block` | **80** | `buildPromptBias` 现有逻辑 | | `builtinPolishLimit` | **40** | polish supplement | | `hardHotwords` | **0** | 显式禁用 | ### 6.3 用户词 vs 系统词库 | 类型 | ASR context | correction | polish | |------|-------------|------------|--------| | 用户 `term` | ✅ 优先 | — | ✅ | | 用户 `aliases` | ✅ 误识别提示 | ✅ 硬替换 | — | | builtin Top-N | ✅ 部分(≤80) | — | ✅(≤40) | | 全量 1 万 builtin | ❌ | ❌ | 间接 | ### 6.4 Session 级 snapshot - 每次 **Option 按下** 时调用一次 `LocalASRBiasAdapter.adapt` - 整段 utterance 使用同一 `promptBias`(不在段内改 context) - 前台 App 变化下一段生效(`MacAppContextService.frontmostBundleIdentifier()`) --- ## 7. 评测计划 ### 7.1 场景集 | ID | 场景 | 词库侧重 | 通过标准 | |----|------|----------|----------| | A | 中文日常 20 句 | 无 | WER 可接受 | | B | 中英混合技术句 50 句 | builtin | 专名 ≥ 基线 | | C | 用户词典 20 词 × 多句 | PersonalDictionary | 召回 ≥ 90% | | D | aliases(k8s→Kubernetes) | correction | 100% 替换 | | E | 静音 5s 按住 | 词库 | 无输出 / 无词库幻觉 | | F | 松开截断 | — | finalize ≤ 500ms | ### 7.2 对比基线 | 基线 | 说明 | |------|------| | B0 | 当前 Sherpa offline + hard hotwords | | B1 | MLX streaming,无 bias | | B2 | MLX streaming + promptBias | | B3 | B2 + correction + polish | | Ref | 云 ASR + PersonalDictionary | **采纳门槛**:B3 专名场景 ≥ B0,且 E/F 显著优于 B0。 ### 7.3 性能指标 | 指标 | 目标(0.6B,M 系列) | |------|---------------------| | 首 partial 延迟 | **< 1s**(streamingWindow 1–2s) | | RTF | **< 0.1**(10s 音频) | | 模型常驻内存 | **~1–1.5 GB** | | 松开后 finalize | **≤ 500ms**(含 tail drain) | --- ## 8. 风险与缓解 | 风险 | 影响 | 缓解 | |------|------|------| | mlx-audio-swift streaming 无 context | 热词无法进 ASR | Phase 1 patch;fallback 仅后处理 | | SPM + metallib 打包失败 | 无法发版 | 参考 Whisperer Makefile;CI macOS 验证 | | soft prompt 弱于云 hotwords | 专名召回下降 | correction + polish;POC 调 prompt 模板 | | 流式 partial 抖动 | UI 体验差 | agreement passes 默认;UI 只显示 confirmed | | 用户磁盘 / 下载 | 首次体验差 | 0.6B 优先;进度 UI;可选云 fallback | | Linux VM 无法测 MLX | CI 缺口 | 单元测 adapter/后处理;macOS 手动 + CI smoke | --- ## 9. 回滚策略 1. **Feature flag**:`mac.localASR.backend = mlxQwen3 | appleSpeech | cloud` 2. **云模式**:本地 MLX / Apple Speech 失败时提示切换云 ASR 3. **词库不受影响**:adapter 层与引擎解耦 4. **无 Sherpa 回滚**:已决策直接移除 --- ## 10. 成功标准(Definition of Done) - [ ] 默认本地引擎为 Qwen3 MLX streaming,Sherpa 非默认 - [ ] 按住 Option **< 1s** 出现 partial - [ ] 松开后 **≤ 500ms** 进入 final(正常环境) - [ ] 用户 `PersonalDictionary` + `phrases.tsv` 经 `LocalASRBiasAdapter` 注入 - [ ] `hardHotwords` 在 MLX 路径为零 - [ ] aliases 经 `LocalASRTranscriptCorrector` 生效 - [ ] 静音不喷词库词 - [ ] 设置页展示 bias diagnostics - [ ] CHANGELOG + 架构文档更新 - [ ] macOS Xcode 16+ 构建通过 --- ## 11. 里程碑总览 ```text Phase 0 决策冻结 ─────────────────────────────► 文档批准 Phase 1 引擎依赖 + context patch ─────────────► MLX 可加载、batch smoke Phase 2 流式 + 词库接入 ───────────────────────► 按住 partial + promptBias Phase 3 tail drain + 后处理 ───────────────────► 松开快、aliases 生效 Phase 4 VAD / 静音门控 ────────────────────────► 无静音幻觉 Phase 5 设置 / 迁移 / fallback ───────────────► 用户可升级 Phase 6 评测 / 灰度 / 发布 ────────────────────► 默认 MLX,Sherpa deprecated ``` --- ## 12. 参考实现 | 项目 | 学什么 | 链接 | |------|--------|------| | Whisperer | 100ms feed、delta insert、幻觉过滤 | [mosquito/whisperer](https://github.com/mosquito/whisperer) | | mlx-audio-swift | `StreamingInferenceSession`、Qwen3 `context` batch API | [Blaizzy/mlx-audio-swift](https://github.com/Blaizzy/mlx-audio-swift) | | VoxFlow | 词库矩阵、`LocalASRBiasAdapter` 式 prompt | [xingbofeng/VoxFlow](https://github.com/xingbofeng/VoxFlow) | | MacWispr | context 词库、Metal warmup | [vasanthsreeram/macwispr](https://github.com/vasanthsreeram/macwispr) | | OSG Shared | tail drain、adapter、corrector | `FlowCaptureTailDrain.swift`、`LocalASRBiasAdapter.swift` | --- ## 13. 已冻结决策(2026-07-14 评审) | # | 问题 | 决策 | |---|------|------| | 1 | mlx-audio-swift 引入方式 | **Mac target 通过 SPM 直接引入** `Blaizzy/mlx-audio-swift`;iOS 仍零 SPM。若 upstream streaming 缺 `context`,在 OSG fork 打小 patch 后 pin revision(见 §13.1) | | 2 | partial UI | **仅 overlay 预览**;松开后经润色链再注入(与云路径一致) | | 3 | 默认模型 | **0.6B 默认**;1.7B 可选下载;现有设置 UI 复用 | | 4 | Sherpa | **直接移除**(catalog + 代码 + runtime 下载),不保留 advanced | | 5 | 静音检测 | **Phase 4 先上轻量 RMS 门控**;Silero VAD 仅当 RMS 实测不够再评估(见 §13.2) | ### 13.1 依赖引入说明(给开发) 「可以直接引入」= 在 `project.yml` 为 **仅 `OSGKeyboardMac` target** 添加 Swift Package: ```yaml packages: MLXAudio: url: https://github.com/Blaizzy/mlx-audio-swift from: "0.1.0" # 或 pin 到具体 revision / OSG fork targets: OSGKeyboardMac: dependencies: - package: MLXAudio product: MLXAudioSTT ``` - **不需要**把整个仓库 copy 进 OSGKeyboard 源码树(除非 fork patch 暂无法 upstream)。 - **需要** macOS + Xcode 16+ 本机构建;`xcodegen generate` 后 Xcode 会拉取并编译 MLX。 - **模型权重**仍走现有 `LocalASRModelManager` 下载管线,不随 SPM 打包进 app。 - **唯一前置条件**:确认 streaming API 支持 `context`(词库 prompt);若无,fork 加 2 处 `buildPrompt` 改动即可。 ### 13.2 静音检测说明(给产品) 把「用户是否在说话」想象成两道筛子: | 方案 | 产品语言 | 优点 | 缺点 | |------|----------|------|------| | **RMS 门控**(先做) | 听音量大小:太安静就不送给识别引擎 | 实现简单、几乎不增加包体、不拖慢首字 | 嘈杂环境可能把背景声当「在说话」 | | **Silero VAD**(备选) | 专门训练过的「人声探测器」,区分人声 vs 键盘/风扇声 | 静音误触发更少 | 多一个模型依赖、开发和评测成本更高 | **决策**:先用 RMS(成本低、能解决「按住不说话却喷词」的主痛点);若内测发现办公室/咖啡厅误触发仍多,再加 Silero。 --- *文档维护:实施过程中若引擎 API 或 catalog 结构变化,请同步更新本节与 `local-asr-architecture.md`。*