164 lines
5.2 KiB
Swift
164 lines
5.2 KiB
Swift
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 = { error in
|
||
let nsError = error as NSError
|
||
// kAFAssistantErrorDomain code 1110 = 未检测到语音,属正常情况,不报错
|
||
if nsError.domain == "kAFAssistantErrorDomain" && nsError.code == 1110 {
|
||
return
|
||
}
|
||
print("[App] Speech error: \(error)")
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|