# VoiceInput — AI Agent 项目指南 ## 项目概述 VoiceInput 是一款 **macOS 菜单栏语音输入应用**。按住 Fn / Globe 键录音,松开后通过剪贴板 + Cmd+V 自动将识别文本注入光标位置。支持 Apple 原生语音识别实时转录、LLM(DeepSeek)纠错优化、多语言切换,以及双击 Control 切换 Input / Assist 模式。 - **最低系统**:macOS 14+ - **语言**:Swift 5.9 - **构建系统**:Swift Package Manager(`Package.swift`)+ Makefile ## 常用命令 所有命令在项目根目录执行。 | 命令 | 说明 | |------|------| | `make build` / `make debug` | Debug 构建并生成 `.app` bundle | | `make release` | Release 构建(优化) | | `make run` | Debug 构建 + 启动应用 | | `make clean` | 清理构建产物 | | `make install` | Release 构建 + 安装到 `/Applications` | | `make sign` | Ad-hoc 签名(Release 后用) | | `make dmg` | 签名 + 创建 DMG 分发包 | 构建产物路径:`.build/release/VoiceInput.app`(Release)或 `.build/debug/VoiceInput.app`(Debug)。 `.app` bundle 由 `scripts/create_app_bundle.sh` 生成,负责将二进制 + 资源封装为标准 macOS 应用包。 ## 项目结构 ``` Sources/ ├── main.swift # 入口:创建 NSApplication(.accessory) + AppDelegate ├── App/ │ ├── AppDelegate.swift # 主控制器:权限引导、录音/停止、模式切换、LLM 调度 │ └── MenuBarController.swift # 菜单栏图标与菜单(语言、LLM 配置、模式切换) ├── Core/ │ ├── KeyMonitor.swift # CGEvent 全局热键:Fn 键长按录音、Control 键双击切换模式 │ ├── SpeechRecognizer.swift # SFSpeechRecognizer 封装:流式识别、语言切换 │ ├── AudioLevelMonitor.swift # AVAudioEngine 音频电平:RMS 音量驱动波形 │ ├── TextInjector.swift # 剪贴板 + Cmd+V 注入;自动切换 CJK 输入法 │ ├── InputSourceManager.swift # 输入法检测与切换(中文/英文等) │ └── LLMRefiner.swift # DeepSeek API 调用:语音纠错(refine) / AI 对话(chat) ├── UI/ │ ├── RecordingPanel.swift # 胶囊浮动窗:无边框、底部居中、弹簧动画 │ ├── WaveformView.swift # 5 条球形波动画:RMS 驱动实时跳动 │ └── SettingsWindow.swift # LLM 设置面板:API Key / Base URL / Model / Test ├── Utils/ │ └── Config.swift # UserDefaults 持久化:语言、LLM 配置、触发模式 └── Resources/ ├── AppIcon.icns # 应用图标 ├── AppIcon-macOS.png # 图标源文件 └── Info.plist # Bundle 配置 ``` ## 架构模式 ### 应用生命周期 `main.swift` 创建 `NSApplication` 并设置为 `.accessory`(无 Dock 图标,仅菜单栏)。`AppDelegate` 通过 `static retainedInstance` 保持强引用(因为 `NSApplication.delegate` 是 weak)。 ### 权限引导流程(顺序执行) ``` 辅助功能权限 → 输入监控权限 → 启动 KeyMonitor ↓ 运行时按需请求: 麦克风权限 + 语音识别权限 ``` `beginPermissionFlow()` 使用 `CGPreflightListenEventAccess()` 和 `CGEvent.tapCreate` 做权限预检。未授权时弹出对话框 + 轮询 Timer。 ### 录音流程 ``` Fn Down(长按 > 200ms) → AppDelegate.startRecording() → AudioLevelMonitor.start() // 音频电平 → SpeechRecognizer.start() // 语音识别 → RecordingPanel.showAnimated() // 显示胶囊窗 Fn Up → AppDelegate.stopRecording() → 如果启用 LLM 纠错 → LLMRefiner.refine() → TextInjector.inject() // 注入文本 → RecordingPanel.hideAnimated() ``` ### Fn 键交互(长按录音) `KeyMonitor` 通过 `CGEvent.tapCreate(.cghidEventTap)` 监听 `flagsChanged` 事件。 - Fn 键 mask 为 `0x00800000`,**长按 > 200ms**:触发录音(`onFnDown` → `onFnUp`) - Control 键 mask 为 `0x00040000`,**双击(两次按下间隔 < 400ms)**:切换模式(`onControlDoubleClick`) - Fn 短按(< 200ms):无操作(留作扩展) ### LLM 集成 `LLMRefiner` 通过 HTTP 调用 DeepSeek Chat Completions API。两种模式: - **Input 模式**(`refine`):纠错优化,修正同音字/拼写/术语 - **Assist 模式**(`chat`):AI 对话,结果注入光标 配置通过 `Config.shared.inputLLM` / `Config.shared.assistLLM` 独立管理。 ## 代码约定 | 约定 | 说明 | |------|------| | `final class` | 所有业务类默认使用,除非设计上需要继承 | | `weak self` | 闭包/异步回调中必须使用,防止循环引用 | | `@MainActor` | 所有 UI 操作在主线程,异步任务用 `Task { @MainActor in }` | | `// MARK: -` | 按功能分区,保持 `AppDelegate` 等大文件可读性 | | Print 日志 | 使用 `print("[模块名] 内容")` 格式,如 `[KeyMonitor]`、`[App]` | | 权限处理 | 所有权限失败必须给用户明确提示(NSAlert)并引导至系统设置 | | 异步 | 语音识别和 LLM 调用使用 Swift Concurrency(`async/await`) | ## 关键依赖 所有依赖通过系统框架链接,无第三方包: ``` Speech → 语音识别 AVFAudio → 音频采集与电平 Carbon → 全局热键(已逐步迁移至 CoreGraphics) CoreGraphics → CGEvent API(热键监听、文本注入) AppKit → UI 组件 Foundation → 基础类型与网络 ``` Entitlements 文件 `Sources/Resources/VoiceInput.entitlements` 包含: - `com.apple.security.device.audio-input` — 麦克风 - `com.apple.security.cs.disable-library-validation` — 允许加载系统插件 ## 注意事项 1. **ARC 陷阱**:`NSApplication.delegate` 是 weak 引用,`main.swift` 中的 `AppDelegate` 会被 ARC 释放。解决方案:`AppDelegate` 内部使用 `static retainedInstance` 自持 + `main.swift` 使用 `withExtendedLifetime` 双重保险。 2. **HID Event Tap 回退**:`KeyMonitor` 优先创建 `.cghidEventTap`,失败时回退到 `.cgSessionEventTap`。前者需要输入监控权限,后者权限要求较低但可能丢失部分事件。 3. **语音识别间歇性报错**:`SFSpeechRecognizer` 在环境安静时可能报 `kAFAssistantErrorDomain 1110`(无语音输入),`AppDelegate` 中已忽略此错误。 4. **输入法切换**:注入文本前 `TextInjector` 会通过 `InputSourceManager` 检测并切换到合适的 CJK 输入法,确保粘贴中文时不出现乱码。 5. **DMG 构建**:`make dmg` 依赖 `make sign` → `make release`,需要 `hdiutil`(系统自带)。