VoiceInput/Sources/App/AppDelegate.swift
JOJO 1d9c9d5fa0 feat: macOS 菜单栏语音输入应用初始版本
- CGEvent cghidEventTap 全局监听 Fn 键
- Apple Speech 流式语音识别(zh-CN 默认)
- 实时音频 RMS 电平驱动波形动画
- NSPanel 胶囊浮动窗口
- 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换
- OpenAI 兼容 LLM 纠错(DeepSeek 预设)
- LSUIElement 菜单栏运行
- SPM + Makefile 构建
2026-04-24 16:17:57 +08:00

160 lines
5.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Cocoa
/// LLM
final class AppDelegate: NSObject, NSApplicationDelegate {
private let keyMonitor = KeyMonitor()
private let speechRecognizer = SpeechRecognizer()
private let audioMonitor = AudioLevelMonitor()
private let recordingPanel = RecordingPanel()
private let menuBarController = MenuBarController()
private let llmRefiner = LLMRefiner()
private var isRecording = false
private var finalTranscription: String = ""
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
//
let started = keyMonitor.start()
if !started {
showPermissionAlert()
}
keyMonitor.onFnDown = { [weak self] in
self?.startRecording()
}
keyMonitor.onFnUp = { [weak self] in
self?.stopRecording()
}
print("[App] Ready")
}
// MARK: - Recording
private func startRecording() {
guard !isRecording else { return }
isRecording = true
finalTranscription = ""
recordingPanel.showAnimated()
recordingPanel.updateText("Listening...")
let language = Config.shared.language.rawValue
Task { @MainActor in
//
let audioOK = audioMonitor.start()
if audioOK {
audioMonitor.onLevelUpdate = { [weak self] level in
self?.recordingPanel.waveformView.updateLevel(level)
}
}
//
let speechOK = await speechRecognizer.start(language: language)
if !speechOK {
recordingPanel.updateText("Recognition error")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
self?.cancelRecording()
}
return
}
speechRecognizer.onResult = { [weak self] text in
guard let self = self, self.isRecording else { return }
self.finalTranscription = text
self.recordingPanel.updateText(text)
}
speechRecognizer.onError = { [weak self] error in
print("[App] Speech error: \(error)")
self?.recordingPanel.updateText("Error: \(error.localizedDescription)")
}
speechRecognizer.onStateChange = { [weak self] isActive in
if !isActive, self?.isRecording == true {
//
}
}
}
}
private func stopRecording() {
guard isRecording else { return }
isRecording = false
//
audioMonitor.stop()
_ = speechRecognizer.stop()
let text = finalTranscription.trimmingCharacters(in: .whitespacesAndNewlines)
// LLM
if Config.shared.llmEnabled && Config.shared.isLLMConfigured && !text.isEmpty {
recordingPanel.showRefining()
Task { @MainActor in
do {
let result = try await llmRefiner.refine(text: text, config: Config.shared)
recordingPanel.hideAnimated {
TextInjector.inject(result.refinedText)
}
} catch {
print("[App] LLM error: \(error)")
// LLM 退
recordingPanel.hideAnimated {
TextInjector.inject(text)
}
}
}
} else if !text.isEmpty {
recordingPanel.hideAnimated {
TextInjector.inject(text)
}
} else {
recordingPanel.hideAnimated()
}
finalTranscription = ""
}
private func cancelRecording() {
audioMonitor.stop()
speechRecognizer.cancel()
recordingPanel.hideAnimated()
isRecording = false
finalTranscription = ""
}
// MARK: - Permissions
private func showPermissionAlert() {
let alert = NSAlert()
alert.messageText = "Accessibility Permission Required"
alert.informativeText = """
Voice Input needs Accessibility permission to detect the Fn key globally.
Please go to System Settings > Privacy & Security > Accessibility
and enable Voice Input.
"""
alert.alertStyle = .warning
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Later")
if alert.runModal() == .alertFirstButtonReturn {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!)
}
}
func applicationWillTerminate(_ notification: Notification) {
keyMonitor.stop()
audioMonitor.stop()
speechRecognizer.cancel()
recordingPanel.close()
}
}