# OSGKeyboard 本地 ASR 技术架构 > **文档状态**:1.7.0 代码事实 + 后续评测方向 > **适用范围**:macOS 15+ 本地听写;iOS 26+ 使用 Apple SpeechAnalyzer。 > **当前结论**:Mac 默认安装目录模型为 > `qwen3-mlx-0.6b-4bit`,通过 `mlx-audio-swift` 做 Qwen3 MLX 真流式识别; > 模型不可用时走 Apple Speech fallback。Sherpa 已不在当前 catalog 或运行路径中。 ## 1. 架构摘要 本地 ASR 的专有名词质量由三层共同完成: 1. **ASR bias**:`PersonalDictionary` 与内置技术词经 `LocalASRBiasAdapter` 生成 Qwen3 `promptBias`。 2. **确定性纠错**:识别后按个人词库 `aliases → term` 做边界受控替换。 3. **Polish 保真**:把内置词参考作为补充上下文交给润色层;无 API Key 或润色失败时 仍返回本地识别与纠错结果。 这三层已经接入 Mac 的 live 与 batch 路径。“Mac 本地路径不消费词库”不再是当前事实。 ## 2. 当前端到端数据流 ```mermaid flowchart LR Audio["MacAudioRecorder · 16 kHz samples"] --> Pipeline["MacDictationPipeline"] Dict["PersonalDictionary"] --> Bias["LocalASRBiasAdapter"] Lexicon["phrases.tsv · BuiltinLexiconIndex"] --> Bias App["Front app + locale"] --> Bias Bias --> Prompt["promptBias"] Bias --> Pairs["correctionPairs"] Bias --> PolishTerms["polishFragment"] Pipeline -->|local + installed MLX| Live["MacMLXLiveCapture"] Pipeline -->|batch/recovery| Local["MacLocalASRService"] Live --> Qwen["MacMLXStreamingASRProvider"] Local --> Qwen Local -->|model unavailable / Apple selected| Apple["MacSpeechLocalASR"] Prompt --> Qwen Qwen --> Raw["Raw transcript"] Apple --> Raw Raw --> Correct["LocalASRTranscriptCorrector"] Pairs --> Correct Correct --> Polish["PolishingService"] PolishTerms --> Polish Polish --> Insert["MacTextInsertionService"] ``` ### 2.1 引擎选择与回退 | 项目 | 当前事实 | |---|---| | 默认模型 ID | `qwen3-mlx-0.6b-4bit` | | 可选 MLX 模型 | 0.6B 4-bit、1.7B 4-bit | | Catalog | `OSGKeyboard/Resources/LocalASR/local-asr-catalog.json` | | 下载源 | `hf-mirror.com` 与 Hugging Face repository files | | 推理 | `MLXAudioSTT.Qwen3ASRModel` | | Live partial | 100 ms 音频 feed;流式 session 定期 decode | | Batch | 同一 MLX 模型的 `generate(audio:context:language:)` | | Fallback | Apple Speech 的本地 `SFSpeechURLRecognitionRequest` | 旧 Sherpa model ID 只在偏好迁移逻辑中映射到当前 MLX 默认值,不代表 Sherpa backend 仍可运行。当前 catalog 的 `runtimes` 为空。 ### 2.2 词库接线 `MacDictationPipeline.resolveLocalBias` 与 `MacMLXLiveCapture.resolveBias` 都读取 `store.personalDictionary`,再调用: ```text LocalASRBiasAdapter.adapt( dictionary + locale + frontAppBundleId + backend capabilities ) ``` 适配器当前输出: | 输出 | 当前消费者 | |---|---| | `promptBias` | Qwen3 MLX streaming `StreamingConfig.context` 与 batch `generate(context:)` | | `correctionPairs` | `LocalASRTranscriptCorrector`,在 polish 前修正 aliases | | `polishFragment` | `PolishingService` 的 `dictionarySupplement` | | `diagnostics` | `LocalASRBiasDiagnosticsStore` | | `hardHotwords` | 为具备 hard-hotword capability 的 backend 保留;当前 Qwen3 MLX 不使用 | 个人词优先;`BuiltinLexiconIndex` 从 `phrases.tsv` 选择 `weight >= 4` 的 Top-N, 代码编辑器/终端前台场景优先项目维护的 `ai_tech_seed`。默认最多考虑 300 个内置 ASR 词, Qwen3 soft prompt 最长 800 字符,润色补充最多 40 个内置词。 ### 2.3 Apple Speech fallback Apple fallback 强制 `requiresOnDeviceRecognition = true`,系统缺少对应语言模型时会 明确失败,不会静默切云。中文路径会准备打包的 Apple Custom Language Model; 个人词库仍参与识别后的 alias 纠错与 polish 保真。 当前 `LocalASRCapabilities.appleSpeech` 声明 `hotwordMode = none`,因此不要把 `contextualStrings` 描述为已经由个人词库动态注入。相关 API 虽有适配入口,但当前 capability 不生成 hard hotwords。 ## 3. 代码索引 | 主题 | 当前路径 | |---|---| | Mac 听写编排 | `OSGKeyboardMac/MacDictationPipeline.swift` | | 本地引擎选择 / fallback | `OSGKeyboardMac/MacLocalASRService.swift` | | MLX live capture | `OSGKeyboardMac/MacMLXLiveCapture.swift` | | MLX provider | `OSGKeyboardMac/MacMLXStreamingASRProvider.swift` | | MLX streaming session | `OSGKeyboardMac/MacMLXStreamingSession.swift` | | Apple Speech fallback | `OSGKeyboardMac/MacSpeechLocalASR.swift` | | Bias payload / capability | `OSGKeyboardShared/Models/LocalASRBiasPayload.swift`, `LocalASRCapabilities.swift` | | Bias 构建 | `OSGKeyboardShared/Services/LocalASRBiasAdapter.swift` | | 内置词索引 | `OSGKeyboardShared/Services/BuiltinLexiconIndex.swift` | | 用户词库 | `OSGKeyboardShared/Models/PersonalDictionary.swift` | | 云 ASR bias | `OSGKeyboardShared/Models/PersonalDictionary+ASRBias.swift` | | Apple CLM | `OSGKeyboardHostSupport/Services/CustomLanguageModelManager.swift` | | 内置 TSV | `OSGKeyboard/Resources/CustomLanguageModel/v1/phrases.tsv` | ## 4. 设计目标与边界 ### 4.1 目标 - Mac 本地听写离线可用,音频默认不离开设备。 - iOS 与 Mac 复用 `PersonalDictionary` 和 `phrases.tsv` 的源数据。 - 每个 backend 如实声明 soft prompt、hard hotword、streaming 与 reload 成本。 - 模型下载、校验、安装和选择显式可管理。 - 质量、延迟、内存与误触发均可量化评测。 - 本地失败不静默回退云端。 ### 4.2 非目标 - 不把一万词全量塞进 ASR prompt。 - 不把 polish 当作唯一专名纠错层。 - 不把尚未进入 catalog/runtime 的 Sherpa 或 SenseVoice 描述成当前能力。 - 不承诺未经同一测试集验证的模型质量。 - 不把 iOS 生成的 CLM `.bin` 直接喂给 MLX 或其他 backend。 ## 5. 能力模型 `LocalASRCapabilities` 区分: | 字段 | 含义 | |---|---| | `hotwordMode` | `none` / `promptOnly` / `perRequest` / `recognizerScoped` / `cloudVocabulary` | | `maxPromptCharacters` | soft prompt 上限 | | `maxHotwordCount` | hard hotword 上限 | | `supportsStreaming` | 是否提供真流式 partial | | `hotwordReloadCost` | `none` / `recognizerReload` / `modelReload` | 当前实际运行矩阵: | Backend | 当前角色 | Bias | Streaming | |---|---|---|---| | Qwen3 MLX | Mac 默认 | `promptOnly`,800 字符 | 是 | | Apple Speech | Mac fallback / 显式选择 | ASR 层 `none`;后处理与 polish 仍接词库 | 否 | | iOS SpeechAnalyzer | iOS 主路径 | Apple CLM + 本地纠错 | 渐进结果 | | Cloud ASR | 用户显式选择 | 按 provider 使用个人词库 | 按 provider | ## 6. 竞品研究中仍有效的结论 基于 2026-03 的公开源码快照,保留以下架构结论,不把它们当作 OSG 当前实现: | 项目 | 观察 | 对 OSG 的启示 | |---|---|---| | OpenLess | 多 provider;本地 Qwen3 词典接线有限 | README 不能把统一词库接口等同于 backend 已消费 | | Typeflux | 词库限额、动态排序、项目词学习 | Top-N 与上下文排序有价值;自动学习必须可确认 | | SayIt | Sherpa Qwen3 recognizer-scoped hotwords | 若未来重做 Sherpa POC,词库变化需计入 recognizer 重建成本 | | VoiceSnap | SenseVoice 离线与静音处理 | 可作为速度/资源基线,不代表具备个性词库 | | OpenBroca | 显式 model selection 与 manifest | 禁止扫描目录后任取第一个模型 | 这些研究只支撑未来实验。当前发布架构仍是 Qwen3 MLX + Apple Speech fallback。 ## 7. 模型管理 当前安装由 catalog 描述 repository files、模型布局与体积,安装状态由 manifest 和必要文件校验决定。设计原则保持: - Catalog 与 runtime 分离。 - `selectedModelId` 显式保存;旧 ID 有确定迁移规则。 - 下载进入 staging,经校验后原子发布。 - 无效或未安装模型不能假装可用。 - 中国大陆镜像与官方 Hugging Face 可按偏好选择。 ModelScope、自定义企业镜像与额外 backend 可以作为后续 catalog 扩展,但不是 1.7.0 当前下载路径。 ## 8. 评测方法 ### 8.1 测试集 | 类别 | 内容 | 目的 | |---|---|---| | A 普通中文 | 日常口语 50 句 | CER / 误触发基线 | | B 技术术语 | SwiftUI、Cursor、Qwen3-ASR 等 50 句 | 专名召回 | | C 用户词典 | 20 个个人词,每词多句 | bias 与 alias 纠错 | | D 长句 | 30 秒以上口语 | streaming 稳定性与 finalize | | E 噪声 / 短句 | 低 SNR、少于 2 秒 | 幻觉与热词污染 | | F 中英混合 | 技术会议和代码口述 | 语言提示与专名保真 | ### 8.2 当前对照矩阵 | 配置 | 说明 | |---|---| | Baseline | Qwen3 MLX 0.6B,不传 bias | | B1 | 0.6B + `promptBias` | | B2 | B1 + alias correction + polish supplement | | Quality | Qwen3 MLX 1.7B + B2 | | Fallback | Apple Speech + correction / polish | | Reference | 用户所选 Cloud ASR + `PersonalDictionary` | 未来 Sherpa/SenseVoice 只能作为新增 POC 行,不能替换当前 baseline 名称。 ### 8.3 指标 - Raw CER/WER。 - 用户词 raw/final hotword recall。 - False hotword rate。 - 录音结束到最终插入的延迟,以及 live partial 首次可见延迟。 - 8 GB Apple-silicon Mac 上的峰值内存、CPU 与模型加载时间。 - 无网络完成率、模型安装成功率、fallback 成功率。 若未来评估 hard-hotword backend,建议门槛: - 用户词召回相对当前 Qwen3 MLX + B2 提升至少 20%。 - False hotword rate 不高于 2%。 - 30 秒音频端到端延迟不超过当前基线 1.5 倍。 - 分发体积、签名、公证与 8 GB 设备内存均可接受。 ## 9. 回退与风险 | 条件 | 当前/要求行为 | |---|---| | MLX 模型未安装 | 使用 Apple Speech fallback | | MLX live 失败 | 返回 batch recovery 信号 | | Apple 本地语言模型缺失 | 明确报错并提示系统下载,不切云 | | Polish 缺 Key、超时或失败 | 返回本地识别/纠错文本 | | 用户禁用云 | 不静默切换云 ASR | | 热词 dump / 近静音幻觉 | 丢弃可疑 live 结果并尝试 batch | 主要后续风险: 1. 800 字符 soft prompt 对低频专名的提升有限。 2. 内置 Top-N 过多会污染短句或近静音输入。 3. alias 替换必须保持整词/高置信边界。 4. 1.7B 模型在低配 Mac 上的内存与首载延迟需持续量化。 5. 新 backend 必须先证明收益,再承担下载、签名和维护成本。 ## 10. 后续方向 1. 用固定测试集持续比较 0.6B / 1.7B、无 bias / 分层 bias。 2. 将 diagnostics 与实际命中/截断数据用于调节 Top-N 和 prompt 上限。 3. 验证 Apple Speech fallback 的个人词动态提示能力后,再决定是否修改 capability。 4. 只有 hard-hotword 收益达到阈值时,才恢复 Sherpa Qwen3 POC。 5. 自动词库学习若实验,必须默认关闭、本地处理、用户确认后写入,并可审计/清空。 ## 11. 修订记录 | 日期 | 说明 | |---|---| | 2026-08-11 | 按 1.7.0 代码重写:Qwen3 MLX streaming 默认、Apple Speech fallback、LocalASRBiasAdapter/PersonalDictionary 已接线;移除 Sherpa 当前路径叙述 | | 2026-03-31 | 初版竞品研究与评测框架 |