feat(mac): add Qwen3 MLX streaming dictation

Replace the Sherpa offline pipeline with native MLX streaming, resilient model downloads, live transcript previews, and supporting tests and documentation.
This commit is contained in:
Rocky
2026-07-23 14:34:56 +08:00
parent f1a811fbf0
commit c0c9dad149
35 changed files with 1373 additions and 764 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
> **文档状态**:架构规划(非实现规格)
> **适用范围**:macOS 本地听写;与 iOS 键盘扩展、云 ASR 路径的关系见各节说明。
> **核心结论**短期不换主模型,优先打通 **词库感知管道**;中期用 POC 验证 **Sherpa Qwen3 hard hotwords** 是否值得成为热词主线
> **核心结论**macOS 本地听写默认 **Qwen3 MLX 真流式**mlx-audio-swift);词库经 `LocalASRBiasAdapter` 以 soft prompt + 后处理注入。Sherpa offline 已移除
---
+62 -22
View File
@@ -1,6 +1,6 @@
# Mac 本地 ASR 迁移计划:Whisperer 流式 + 词库分层
> **文档状态**:实施计划(待评审)
> **文档状态**:实施计划(**已评审,决策已冻结**
> **适用范围**macOS 本地听写(`OSGKeyboardMac`
> **关联文档**[`local-asr-architecture.md`](./local-asr-architecture.md)
> **创建日期**2026-07-14
@@ -26,7 +26,9 @@
| **模型** | Qwen3-ASR 0.6B 4-bit(默认)/ 1.7B 4-bit(高质量档) |
| **热词模式** | `promptOnly``StreamingConfig.context`),**禁用** Sherpa hard hotwords |
| **词库适配** | 复用现有 `LocalASRBiasAdapter`,不新建平行词库系统 |
| **废弃** | Sherpa Qwen3 offline 子进程 + `ChunkedUtterancePipeline` 本地主线 |
| **废弃** | Sherpa Qwen3 offline 子进程 + `ChunkedUtterancePipeline` 本地主线**catalog 直接移除**,不保留 advanced |
| **partial UI** | **仅 overlay 预览**;松开后润色完成再注入前台 App |
| **默认模型** | 0.6B 4-bit 默认;1.7B 可选下载(设置 UI 已有) |
### 1.3 非目标(本期不做)
@@ -204,7 +206,7 @@ Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。
- 新增条目:`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 条目但标记 `deprecated` / 高级选项
- **移除**所有 Sherpa 模型与 runtime 条目(不保留 advanced / fallback
4. **新文件骨架**
- `OSGKeyboardMac/MacMLXStreamingASRProvider.swift`
@@ -239,8 +241,8 @@ Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。
- 删除/绕开 `liveCaptureTask` + `ChunkedUtterancePipeline` 本地路径
- 学 Whisperer `AppState`
- 按下:创建 session + 100ms `feedAudio` timer
- 监听 `session.events` → 更新 `transcript` / `isStreamingPartial`
- 松开:进入 Phase 3 tail drain
- 监听 `session.events` → 更新 overlay `transcript` / `isStreamingPartial`**仅预览,不插入前台 App**
- 松开:进入 Phase 3 tail drain → 润色 → 再注入
3. **`MacDictationPipeline.resolveLocalBias`**
- 已有实现保留;capabilities 改为 `.qwen3MLX`
@@ -331,10 +333,11 @@ Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。
qwen3-mlx 失败 → Apple Speech(现有 MacSpeechLocalASR
模型缺失 → 引导下载 / 云模式
```
(无 Sherpa 回退)
4. **废弃路径标记**
- `MacSherpaONNXRunner` / `MacSherpaLocalASR` 保留但默认隐藏
- `MacLocalASRChunkAdapter` 仅 cloud chunked 或 legacy flag 使用
4. **Sherpa 代码清理**
- 删除 `MacSherpaONNXRunner` / `MacSherpaLocalASR` / Sherpa runtime 下载逻辑
- `MacLocalASRChunkAdapter` 仅保留 cloud chunked 路径(若仍需要)
**验收**
@@ -385,13 +388,15 @@ Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。
| `OSGKeyboardMac/MacLocalASRModelSettingsView.swift` | 模型档 + diagnostics |
| `docs/local-asr-architecture.md` | 与本文对齐 |
### 5.3 废弃(保留代码,默认不启用
### 5.3 删除(Sherpa 路径
| 文件 | 说明 |
|------|------|
| `OSGKeyboardMac/MacSherpaONNXRunner.swift` | hard hotwords 路径 |
| `OSGKeyboardMac/MacLocalASRChunkAdapter.swift` | Sherpa chunked |
| `OSGKeyboardShared/.../ChunkedUtterancePipeline.swift` | Mac 本地不再使用 |
| `OSGKeyboardMac/MacSherpaONNXRunner.swift` | 移除 |
| `OSGKeyboardMac/MacSherpaLocalASR.swift` | 移除 |
| `local-asr-catalog.json` 中 Sherpa runtime / 模型条目 | 移除 |
| `MacLocalASRChunkAdapter.swift`(Sherpa 专用部分) | 移除或仅留 cloud |
| Mac 本地 `ChunkedUtterancePipeline` 调用 | 移除 |
---
@@ -488,10 +493,10 @@ Sherpa / Apple Speech 保留为 **fallback provider**,不作为默认。
## 9. 回滚策略
1. **Feature flag**`mac.localASR.backend = mlxQwen3 | sherpaQwen3 | appleSpeech`
2. **模型级回滚**catalog 默认指回 Sherpa(不推荐长期使用)
3. **云模式**:本地失败自动提示切换云 ASR(现有路径)
4. **词库不受影响**:adapter 层与引擎解耦,回滚不改词库
1. **Feature flag**`mac.localASR.backend = mlxQwen3 | appleSpeech | cloud`
2. **云模式**:本地 MLX / Apple Speech 失败时提示切换云 ASR
3. **词库不受影响**:adapter 层与引擎解耦
4. **无 Sherpa 回滚**:已决策直接移除
---
@@ -536,13 +541,48 @@ Phase 6 评测 / 灰度 / 发布 ───────────────
---
## 13. 开放问题(评审时确认
## 13. 已冻结决策(2026-07-14 评审
1. **mlx-audio-swift 依赖方式**:直接 pin `main` vs fork 带 `context` patch
2. **partial UI vs 增量插入**Mac 默认仅 overlay 预览,还是像 Whisperer 边说边插入?
3. **0.6B vs 1.7B 默认**:质量优先还是延迟优先?
4. **Sherpa 条目何时从 catalog 移除**:灰度后一个版本 vs 长期保留 advanced
5. **是否 Phase 4 引入 Silero VAD**:或 RMS 门控足够?
| # | 问题 | 决策 |
|---|------|------|
| 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。
---