|
1 | 1 | # Typoless |
2 | 2 |
|
3 | | -Typoless 是一个面向 macOS 的语音 + AI 输入助手项目。 |
| 3 | + |
4 | 4 |
|
5 | | -首版产品形态不是系统级输入法,而是 `菜单栏常驻应用`。用户通过全局快捷键触发录音(按一次开始,再按一次结束),录音结束后自动完成: |
| 5 | +Typoless 是一个面向 macOS 的菜单栏语音 + AI 输入助手。 |
6 | 6 |
|
7 | | -`录音 -> RNNoise 本地降噪 -> FunASR 离线识别 -> OpenAI 兼容 LLM 润色 -> 写回当前焦点应用` |
| 7 | +当前主链路: |
8 | 8 |
|
9 | | -## 项目目标 |
| 9 | +`录音 -> RNNoise 本地降噪 -> FunASR / 腾讯云 ASR -> OpenAI 兼容 LLM 润色 -> 写回当前焦点应用` |
10 | 10 |
|
11 | | -- 在 macOS 上提供全局可用的中文语音输入能力 |
12 | | -- 使用本地 `FunASR` 离线 ASR 完成短语音识别,无需配置云端 ASR 服务 |
13 | | -- 使用 RNNoise 本地降噪和个人词典提升中文短语音识别质量 |
14 | | -- 支持用户接入自有 `OpenAI 兼容` 大模型服务 |
15 | | -- 将口语输入整理为更适合直接发送或写入的文本 |
16 | | -- 在常见桌面应用中稳定注入文本 |
| 11 | +## 当前范围 |
17 | 12 |
|
18 | | -## 首版范围 |
19 | | - |
20 | | -### 已实现 |
21 | | - |
22 | | -- 菜单栏常驻应用(MenuBarExtra) |
23 | | -- 设置页(LLM / 通用 / 权限 / 诊断) |
24 | | -- 全局快捷键(Carbon Event API) |
25 | | -- 按一次开始录音,再按一次结束录音 |
26 | | -- FunASR 本地离线 ASR(默认链路,通过内置 Python sidecar 运行 paraformer-zh + fsmn-vad) |
27 | | -- RNNoise 本地降噪(录音后、ASR 前自动执行) |
28 | | -- OpenAI 兼容 LLM 润色(增强版固定 Prompt,纠错 + 同音词 + 去赘词 + 轻度书面化 + 补标点 + 专有名词保护) |
29 | | -- 个人词典(ASR hotwords + LLM 术语参考,`~/.typoless/dictionary.json`) |
30 | | -- 文本注入(AX API 主策略 + 键盘事件回退) |
| 13 | +- 菜单栏常驻应用,不是系统级输入法 |
| 14 | +- 单一全局快捷键,按一次开始录音,再按一次结束录音 |
| 15 | +- 本地 `FunASR` 离线识别,模型外置到 `~/.typoless/models/funasr/` |
| 16 | +- 腾讯云一句话识别,用户手动切换 |
| 17 | +- `OpenAI Chat Completions` 兼容接口 |
| 18 | +- 默认输出 LLM 润色结果 |
| 19 | +- 文本注入优先走 `Accessibility API`,失败后回退键盘事件 |
31 | 20 | - 麦克风与辅助功能权限引导 |
32 | | -- 注入失败文本临时复制入口(菜单栏内显示截断预览,点击复制到剪贴板,仅当前运行期有效) |
33 | | -- 状态机驱动菜单栏反馈(空闲 / 录音中 / 识别中 / 润色中 / 注入中 / 完成 / 失败 / 已取消) |
34 | | -- 用户可理解的错误分类与展示 |
35 | | -- 处理中可取消(识别中 / 润色中) |
36 | | -- 诊断页(最近错误摘要 + 会话状态 + 版本信息) |
37 | | -- 诊断日志(per-session 耗时、Debug ASR/LLM 明文对照、Release 脱敏) |
38 | | -- 60 秒录音上限 + 500ms 短录音静默取消 |
39 | | -- 本地 FunASR 模型外置到用户目录,设置页通过模型状态区引导下载与展示就绪状态 |
40 | | -- 腾讯云一句话识别(用户手动选择,直接调用 Cloud API) |
41 | | -- 构建前与录音前资源校验 |
42 | | - |
43 | | -### 不包含 |
44 | | - |
45 | | -- macOS 系统级输入法 |
46 | | -- 多种 LLM 协议 |
47 | | -- 自定义 Prompt |
48 | | -- 风格模式切换 |
49 | | -- temperature / max tokens 等高级参数 |
50 | | -- ASR 高级运行参数 |
51 | | -- 音频历史保存 |
52 | | -- Agent 工作流 |
| 21 | +- 注入失败文本仅保留在当前运行期,可从菜单栏复制 |
53 | 22 |
|
54 | | -## 产品行为 |
| 23 | +## 技术栈 |
55 | 24 |
|
56 | | -- 应用形态:菜单栏常驻应用 |
57 | | -- 交互方式:单一全局快捷键,按一次开始录音,再按一次结束录音 |
58 | | -- 单次录音上限:`60 秒` |
59 | | -- 低于 `500ms` 的录音视为误触,静默取消,不进入识别和 LLM |
60 | | -- ASR 平台:用户手动选择 `本地 FunASR` 或 `腾讯云一句话识别` |
61 | | -- 本地模型:存储于 `~/.typoless/models/funasr/`,设置页仅展示模型状态,不暴露路径或版本 |
62 | | -- 音频预处理:默认 RNNoise 本地降噪 |
63 | | -- 默认输出:`LLM 润色版` |
64 | | -- LLM 启用条件:`Base URL`、`API Key`、`Model` 三项完整 |
65 | | -- LLM 失败处理:直接报错,不注入任何文本 |
66 | | -- 注入失败策略:不自动写剪贴板,菜单栏显示失败文本截断预览,点击可复制到剪贴板,仅当前运行期有效 |
| 25 | +- `Swift 6` |
| 26 | +- `SwiftUI + AppKit` |
| 27 | +- `MVVM + Service Layer` |
| 28 | +- `RNNoise` |
| 29 | +- `FunASR` Python sidecar |
| 30 | +- `OpenAI Chat Completions` 兼容接口 |
67 | 31 |
|
68 | | -## 技术架构 |
| 32 | +## 仓库结构 |
69 | 33 |
|
70 | | -### 技术栈 |
71 | | - |
72 | | -- 客户端:`Swift 6.0 + SwiftUI + AppKit` |
73 | | -- 架构:`MVVM + Service Layer` |
74 | | -- 语音识别:`FunASR` 本地离线 ASR / `腾讯云一句话识别`(用户手动切换) |
75 | | -- 音频降噪:`RNNoise` |
76 | | -- 大模型接入:`OpenAI Chat Completions` 兼容接口 |
77 | | -- 音频格式:`PCM/WAV 16k mono` |
78 | | -- 文本注入:优先 `Accessibility API`,失败后回退键盘事件输入 |
79 | | -- 配置存储:`~/.typoless/config.json`(UTF-8 JSON) |
80 | | - |
81 | | -### 分层结构 |
82 | | - |
83 | | -``` |
84 | | -app/Typoless/ |
85 | | -├── App/ # 应用入口(TypolessApp) |
86 | | -├── Domain/ |
87 | | -│ ├── Coordinators/ # AppCoordinator, SessionCoordinator |
88 | | -│ ├── Models/ # SessionState, TypolessError, AppConfig 等 |
89 | | -│ └── Services/ # DiagnosticsLogger, ResourceValidator |
90 | | -├── Persistence/ # ConfigStore, KeychainHelper, PersonalDictionaryStore |
91 | | -├── Platform/ # AudioRecorder, AudioPreprocessor, HotkeyManager, PermissionsManager, TextInjector |
92 | | -├── Providers/ # ASRProvider, FunASRProvider, ASRRuntimeManager, StreamingASRProvider, WhisperProvider, LLMProvider |
93 | | -├── Resources/ # 资源文件(正式包默认包含 rnnoise, funasr) |
94 | | -└── UI/ |
95 | | - ├── MenuBar/ # MenuBarView |
96 | | - └── Settings/ # 设置页各 Tab 视图 |
| 34 | +```text |
| 35 | +app/ macOS 客户端与 XcodeGen 工程定义 |
| 36 | +docs/ PRD、TDD、验证与设计文档 |
| 37 | +scripts/ 资源准备、构建、签名相关脚本 |
97 | 38 | ``` |
98 | 39 |
|
99 | | -### 核心对象 |
100 | | - |
101 | | -| 对象 | 职责 | |
102 | | -| --- | --- | |
103 | | -| `AppCoordinator` | 应用生命周期、菜单栏入口、设置页导航 | |
104 | | -| `SessionCoordinator` | 主链路状态机编排(录音→识别→润色→注入) | |
105 | | -| `AudioRecorder` | 音频采集与 PCM/WAV 标准化 | |
106 | | -| `AudioPreprocessor` | RNNoise 本地降噪 | |
107 | | -| `ASRProvider` | 统一 ASR 识别协议 | |
108 | | -| `FunASRProvider` | FunASR 离线识别(默认链路),通过 sidecar 通信 | |
109 | | -| `ASRRuntimeManager` | Python sidecar 生命周期管理、warmup、健康检查 | |
110 | | -| `StreamingASRProvider` | sherpa-onnx 流式识别(旧链路) | |
111 | | -| `WhisperProvider` | 本地 Whisper 子进程调用(旧链路) | |
112 | | -| `LLMProvider` | OpenAI Chat Completions 调用 | |
113 | | -| `TextInjector` | AX API 文本注入 + 键盘事件回退 | |
114 | | -| `PermissionsManager` | 麦克风与辅助功能权限管理 | |
115 | | -| `HotkeyManager` | Carbon Event 全局快捷键 | |
116 | | -| `ConfigStore` | `~/.typoless/config.json` 配置读写 | |
117 | | -| `PersonalDictionaryStore` | 个人词典管理(`~/.typoless/dictionary.json`) | |
118 | | -| `DiagnosticsLogger` | 会话耗时与 ASR/LLM 对照日志 | |
119 | | -| `ResourceValidator` | 运行时资源完整性校验 | |
120 | | - |
121 | | -## 核心流程 |
122 | | - |
123 | | -### 首次配置 |
124 | | - |
125 | | -1. 启动应用 |
126 | | -2. 应用内置降噪、FunASR runtime 与 worker;若使用本地 FunASR,先在设置页下载模型 |
127 | | -3. 配置 LLM `Base URL / API Key / Model` |
128 | | -4. 设置全局快捷键 |
129 | | -5. 授予麦克风权限 |
130 | | -6. 授予辅助功能权限 |
131 | | - |
132 | | -### 日常使用 |
133 | | - |
134 | | -1. 在任意应用中聚焦输入区域 |
135 | | -2. 按下快捷键开始录音 |
136 | | -3. 再次按下快捷键结束录音(或达到 60 秒自动结束) |
137 | | -4. 对音频进行本地降噪 |
138 | | -5. 使用 FunASR 进行本地离线识别,获取转写文本 |
139 | | -6. 调用 LLM 做纠错与轻度书面化 |
140 | | -7. 将最终文本一次性注入当前焦点应用 |
141 | | - |
142 | | -## 状态机 |
143 | | - |
144 | | -主状态流转: |
145 | | - |
146 | | -`idle -> recording -> transcribing -> polishing -> injecting -> done` |
147 | | - |
148 | | -异常状态: |
149 | | - |
150 | | -- `error` |
151 | | -- `cancelled` |
152 | | - |
153 | | -约束: |
154 | | - |
155 | | -- 同一时间只允许一个 session |
156 | | -- 菜单栏允许取消处理中任务(识别中 / 润色中) |
157 | | -- 取消后不得继续注入文本 |
158 | | -- 状态变化实时反映到菜单栏图标 |
159 | | - |
160 | | -## 错误处理 |
161 | | - |
162 | | -| 错误类型 | 用户提示 | |
163 | | -| --- | --- | |
164 | | -| 麦克风权限缺失 | 麦克风权限未开启,无法录音 | |
165 | | -| 辅助功能权限缺失 | 辅助功能权限未开启,无法注入文本 | |
166 | | -| 录音数据为空 | 录音数据为空,请重试 | |
167 | | -| 本地识别引擎未就绪 | 本地识别引擎未就绪,请重新安装应用 | |
168 | | -| LLM 配置无效 | LLM 配置无效:具体原因 | |
169 | | -| LLM 网络失败 | LLM 网络连接失败,请检查网络 | |
170 | | -| LLM 空结果 | LLM 返回空结果,请检查模型或网关配置 | |
171 | | -| 文本注入失败 | 文本注入失败:具体原因 | |
172 | | - |
173 | | -## LLM 处理边界 |
174 | | - |
175 | | -首版 LLM 只做以下事情: |
176 | | - |
177 | | -- 修正 ASR 识别错误 |
178 | | -- 修正常见同音词和错别字 |
179 | | -- 去掉明显口语赘词 |
180 | | -- 轻度书面化表达 |
181 | | -- 自动补自然中文标点 |
182 | | -- 保留个人词典中的专有名词 |
183 | | - |
184 | | -首版 LLM 不做以下事情: |
185 | | - |
186 | | -- 大幅改写句子结构 |
187 | | -- 扩写用户没说出的内容 |
188 | | -- 擅自改变语义、语气、事实 |
189 | | - |
190 | | -## 配置项 |
| 40 | +## 本地开发 |
191 | 41 |
|
192 | | -| 配置字段 | 存储位置 | |
193 | | -| --- | --- | |
194 | | -| `openai_base_url` | `~/.typoless/config.json` | |
195 | | -| `openai_model` | `~/.typoless/config.json` | |
196 | | -| `global_hotkey` | `~/.typoless/config.json` | |
197 | | -| `pasteboard_injection_bundle_ids` | `~/.typoless/config.json` | |
198 | | -| `openai_api_key` | `~/.typoless/config.json` | |
199 | | -| 个人词典 | `~/.typoless/dictionary.json` | |
| 42 | +依赖: |
200 | 43 |
|
201 | | -## 测试策略 |
| 44 | +- macOS 14+ |
| 45 | +- Xcode 16+ |
| 46 | +- `xcodegen` |
202 | 47 |
|
203 | | -- 单元测试重点覆盖 `Provider`(FunASR/sherpa/Whisper)、`ASRRuntimeManager`、`AudioPreprocessor`、`PersonalDictionaryStore` 和 `Session Coordinator` |
204 | | -- 端到端以手工验收主链路为主 |
205 | | -- 重点验证权限缺失、配置错误、LLM 回退、注入失败 |
206 | | - |
207 | | -### 手工验收清单 |
208 | | - |
209 | | -- [ ] 首次启动自动打开设置页 |
210 | | -- [ ] 配置 LLM 并保存成功 |
211 | | -- [ ] 设置全局快捷键并生效 |
212 | | -- [ ] 麦克风权限授权后可录音 |
213 | | -- [ ] 低于 500ms 的短录音静默取消,不触发 ASR / LLM / 注入 |
214 | | -- [ ] 辅助功能权限授权后可注入文本 |
215 | | -- [ ] 完整链路:录音 → 降噪 → FunASR → LLM → 注入(浏览器输入框、备忘录、聊天应用) |
216 | | -- [ ] RNNoise 降噪链路正常 |
217 | | -- [ ] FunASR 离线识别链路正常 |
218 | | -- [ ] FunASR sidecar 首次惰性启动成功 |
219 | | -- [ ] Debug 日志可查看耗时和 ASR/LLM 明文对照 |
220 | | -- [ ] Release 日志不泄露 ASR/LLM 明文 |
221 | | -- [ ] 个人词典可改善专有名词识别与润色 |
222 | | -- [ ] LLM 配置不完整时,录音流程直接报错且不注入文本 |
223 | | -- [ ] LLM 失败或空结果时直接报错且不注入文本 |
224 | | -- [ ] 注入失败后菜单栏显示失败文本预览,点击可复制 |
225 | | -- [ ] 成功注入后失败文本预览消失 |
226 | | -- [ ] 菜单栏状态随主链路正确刷新 |
227 | | -- [ ] 识别中 / 润色中可从菜单取消 |
228 | | -- [ ] 诊断页展示最近错误摘要 |
229 | | -- [ ] 权限缺失场景提示清晰 |
230 | | -- [ ] 本地识别失败场景提示清晰 |
231 | | -- [ ] 应用重启后配置正常恢复,无历史文本残留 |
232 | | - |
233 | | -## 目录说明 |
234 | | - |
235 | | -- [PRD.md](./docs/PRD.md): 已更新的产品需求文档 |
236 | | -- [TDD.md](./docs/TDD.md): 已更新的技术设计文档 |
237 | | -- [EPICS_AND_STORIES.md](./docs/EPICS_AND_STORIES.md): Epic 和 Story 拆分 |
238 | | -- `app/`: macOS 客户端代码(Swift + SwiftUI + AppKit) |
239 | | -- `app/project.yml`: XcodeGen 项目配置 |
240 | | - |
241 | | -## 开发环境 |
242 | | - |
243 | | -### 依赖 |
244 | | - |
245 | | -- macOS 14.0+ |
246 | | -- Xcode 16.0+ |
247 | | -- [XcodeGen](https://github.com/yonaskolb/XcodeGen) |
248 | | - |
249 | | -### 准备本地语音资源 |
250 | | - |
251 | | -正式包默认仅内置 `FunASR + RNNoise` 资源。开发环境至少需要准备 RNNoise;旧链路脚本只保留给历史调试用途: |
| 48 | +准备资源: |
252 | 49 |
|
253 | 50 | ```bash |
254 | | -# 准备 RNNoise 降噪库 |
255 | 51 | ./scripts/setup-rnnoise.sh |
256 | | - |
257 | | -# 可选:准备 sherpa-onnx runtime 与中文 streaming 模型(旧链路调试) |
258 | | -# ./scripts/setup-sherpa.sh |
259 | | - |
260 | | -# 可选:准备 Whisper 资源(旧链路调试) |
261 | | -# ./scripts/setup-whisper.sh |
262 | 52 | ``` |
263 | 53 |
|
264 | | -脚本支持通过环境变量指定资源路径,详见各脚本顶部注释。 |
265 | | - |
266 | | -### 生成 Xcode 工程 |
| 54 | +如需本地 FunASR runtime 打包或签名,使用: |
267 | 55 |
|
268 | 56 | ```bash |
269 | | -cd app |
270 | | -xcodegen generate |
271 | | -open Typoless.xcodeproj |
| 57 | +./scripts/bundle-funasr-runtime.sh |
| 58 | +./scripts/sign-funasr-runtime.sh |
272 | 59 | ``` |
273 | 60 |
|
274 | | -### 构建与运行 |
| 61 | +生成工程并构建: |
275 | 62 |
|
276 | 63 | ```bash |
277 | 64 | cd app |
278 | 65 | xcodegen generate |
279 | 66 | xcodebuild build -project Typoless.xcodeproj -scheme Typoless -destination 'platform=macOS' |
280 | 67 | ``` |
281 | 68 |
|
282 | | -或在 Xcode 中打开 `Typoless.xcodeproj` 后按 `⌘R` 运行。 |
283 | | - |
284 | | -## CI / CD |
285 | | - |
286 | | -项目使用 GitHub Actions 进行持续集成和构建产物生成。 |
287 | | - |
288 | | -### CI(`.github/workflows/ci.yml`) |
289 | | - |
290 | | -- **触发条件**:`main` 分支 push、PR 提交 |
291 | | -- **执行内容**:`xcodegen generate` → `xcodebuild build` → `xcodebuild test` |
292 | | -- **失败诊断**:自动上传 `xcresult` 和构建日志 |
293 | | - |
294 | | -### Release Build(`.github/workflows/release-build.yml`) |
295 | | - |
296 | | -- **触发条件**:手动触发(`workflow_dispatch`)、`v*` tag push |
297 | | -- **执行内容**:`xcodegen generate` → `xcodebuild archive`(未签名)→ 打包 `.app` zip |
298 | | -- **产物**:可从 Actions 下载 `Typoless-unsigned.zip` 和 `.xcarchive` |
299 | | -- **不依赖**:Developer ID 签名、Apple notarization、GitHub Secrets |
300 | | - |
301 | | -### 构建准备脚本 |
302 | | - |
303 | | -```bash |
304 | | -# CI 和本地均可使用 |
305 | | -./scripts/ci/prepare-macos-build.sh |
306 | | -``` |
307 | | - |
308 | | -脚本自动完成:安装 `xcodegen`、准备 RNNoise 库、生成 Xcode 工程。 |
309 | | - |
310 | | -## 当前状态 |
311 | | - |
312 | | -- `PRD`: 已更新至 v1.2(FunASR 收敛) |
313 | | -- `TDD`: 已更新至 v1.2(FunASR sidecar 架构) |
314 | | -- `代码实现`: FunASR 默认链路实现中(DiagnosticsLogger、RNNoise、个人词典、Prompt 优化、资源校验已完成;FunASR Provider 与 sidecar 集成进行中) |
315 | | - |
316 | | -## 参考 |
| 69 | +## 文档入口 |
317 | 70 |
|
318 | | -- 产品需求文档:[PRD.md](./docs/PRD.md) |
319 | | -- Epic 和 Story 拆分:[EPICS_AND_STORIES.md](./docs/EPICS_AND_STORIES.md) |
320 | | -- 技术设计文档:[TDD.md](./docs/TDD.md) |
| 71 | +- [PRD](./docs/PRD.md) |
| 72 | +- [TDD](./docs/TDD.md) |
| 73 | +- [EPICS_AND_STORIES](./docs/EPICS_AND_STORIES.md) |
0 commit comments