- CGEvent cghidEventTap 全局监听 Fn 键 - Apple Speech 流式语音识别(zh-CN 默认) - 实时音频 RMS 电平驱动波形动画 - NSPanel 胶囊浮动窗口 - 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换 - OpenAI 兼容 LLM 纠错(DeepSeek 预设) - LSUIElement 菜单栏运行 - SPM + Makefile 构建
134 lines
4.0 KiB
Swift
134 lines
4.0 KiB
Swift
import Foundation
|
||
import Speech
|
||
import AVFAudio
|
||
|
||
/// 流式语音识别,使用 Apple Speech Recognition 框架
|
||
final class SpeechRecognizer: NSObject, SFSpeechRecognizerDelegate {
|
||
|
||
private var recognizer: SFSpeechRecognizer?
|
||
private var request: SFSpeechAudioBufferRecognitionRequest?
|
||
private var task: SFSpeechRecognitionTask?
|
||
private let engine = AVAudioEngine()
|
||
private var isRunning = false
|
||
|
||
var onResult: ((String) -> Void)?
|
||
var onError: ((Error) -> Void)?
|
||
var onStateChange: ((Bool) -> Void)? // true = 开始识别中
|
||
|
||
override init() {
|
||
super.init()
|
||
}
|
||
|
||
/// 请求权限
|
||
func requestAuthorization() async -> Bool {
|
||
let status = await withCheckedContinuation { (continuation: CheckedContinuation<SFSpeechRecognizerAuthorizationStatus, Never>) in
|
||
SFSpeechRecognizer.requestAuthorization { status in
|
||
continuation.resume(returning: status)
|
||
}
|
||
}
|
||
return status == .authorized
|
||
}
|
||
|
||
/// 使用指定语言启动识别
|
||
func start(language: String) async -> Bool {
|
||
guard !isRunning else { return true }
|
||
|
||
let authorized = await requestAuthorization()
|
||
guard authorized else {
|
||
onError?(NSError(domain: "SpeechRecognizer", code: -1,
|
||
userInfo: [NSLocalizedDescriptionKey: "Speech recognition not authorized"]))
|
||
return false
|
||
}
|
||
|
||
guard let recognizer = SFSpeechRecognizer(locale: Locale(identifier: language)),
|
||
recognizer.isAvailable else {
|
||
let msg = "Speech recognizer unavailable for \(language)"
|
||
onError?(NSError(domain: "SpeechRecognizer", code: -2,
|
||
userInfo: [NSLocalizedDescriptionKey: msg]))
|
||
return false
|
||
}
|
||
|
||
self.recognizer = recognizer
|
||
recognizer.delegate = self
|
||
recognizer.defaultTaskHint = .dictation
|
||
|
||
// 创建新的请求
|
||
request = SFSpeechAudioBufferRecognitionRequest()
|
||
guard let request = request else { return false }
|
||
request.shouldReportPartialResults = true
|
||
request.addsPunctuation = true
|
||
|
||
// 获取音频输入
|
||
let inputNode = engine.inputNode
|
||
let format = inputNode.outputFormat(forBus: 0)
|
||
inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
|
||
self?.request?.append(buffer)
|
||
}
|
||
|
||
do {
|
||
try engine.start()
|
||
} catch {
|
||
onError?(error)
|
||
return false
|
||
}
|
||
|
||
// 开始识别任务
|
||
task = recognizer.recognitionTask(with: request) { [weak self] result, error in
|
||
guard let self = self else { return }
|
||
|
||
if let error = error {
|
||
DispatchQueue.main.async {
|
||
self.onError?(error)
|
||
}
|
||
return
|
||
}
|
||
|
||
if let result = result {
|
||
// 获取当前最佳转录及未完成的备选
|
||
let text = result.bestTranscription.formattedString
|
||
DispatchQueue.main.async {
|
||
self.onResult?(text)
|
||
}
|
||
}
|
||
}
|
||
|
||
isRunning = true
|
||
onStateChange?(true)
|
||
print("[Speech] Started recognition for \(language)")
|
||
return true
|
||
}
|
||
|
||
func stop() -> String? {
|
||
guard isRunning else { return nil }
|
||
isRunning = false
|
||
|
||
engine.inputNode.removeTap(onBus: 0)
|
||
engine.stop()
|
||
|
||
// 结束识别流
|
||
request?.endAudio()
|
||
task?.finish()
|
||
|
||
// 结果已在回调中处理
|
||
request = nil
|
||
task = nil
|
||
recognizer = nil
|
||
|
||
onStateChange?(false)
|
||
print("[Speech] Stopped recognition")
|
||
return nil
|
||
}
|
||
|
||
func cancel() {
|
||
task?.cancel()
|
||
_ = stop()
|
||
}
|
||
|
||
// MARK: - SFSpeechRecognizerDelegate
|
||
|
||
func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer,
|
||
availabilityDidChange available: Bool) {
|
||
print("[Speech] Availability changed: \(available)")
|
||
}
|
||
}
|