VoiceInput/Sources/Core/SpeechRecognizer.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

134 lines
4.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 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)")
}
}