- updateText 移除文本宽度硬编码下限 160,真正根据内容自适应 - showRefining/showThinking 简化为调用 updateText,复用自适应逻辑 - 面板最小宽度由 minPanelWidth(180pt) 兜底 - 打包 VoiceInput-20260425.dmg
6.7 KiB
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— 允许加载系统插件
注意事项
-
ARC 陷阱:
NSApplication.delegate是 weak 引用,main.swift中的AppDelegate会被 ARC 释放。解决方案:AppDelegate内部使用static retainedInstance自持 +main.swift使用withExtendedLifetime双重保险。 -
HID Event Tap 回退:
KeyMonitor优先创建.cghidEventTap,失败时回退到.cgSessionEventTap。前者需要输入监控权限,后者权限要求较低但可能丢失部分事件。 -
语音识别间歇性报错:
SFSpeechRecognizer在环境安静时可能报kAFAssistantErrorDomain 1110(无语音输入),AppDelegate中已忽略此错误。 -
输入法切换:注入文本前
TextInjector会通过InputSourceManager检测并切换到合适的 CJK 输入法,确保粘贴中文时不出现乱码。 -
DMG 构建:
make dmg依赖make sign→make release,需要hdiutil(系统自带)。