fix(ios): harden voice wake callbacks

main
Peter Steinberger 2025-12-12 21:59:04 +00:00
parent 13b8dc61ba
commit e31383a8f1
1 changed files with 44 additions and 27 deletions

View File

@ -106,10 +106,19 @@ final class VoiceWakeManager: NSObject, ObservableObject {
self.recognitionTask = self.speechRecognizer?.recognitionTask(with: request) { [weak self] result, error in
guard let self else { return }
Task { @MainActor in
self.handleRecognitionCallback(result: result, error: error)
}
}
}
private func handleRecognitionCallback(result: SFSpeechRecognitionResult?, error: Error?) {
if let error {
self.statusText = "Recognizer error: \(error.localizedDescription)"
self.isListening = false
if self.isEnabled {
let shouldRestart = self.isEnabled
if shouldRestart {
Task {
try? await Task.sleep(nanoseconds: 700_000_000)
await self.start()
@ -117,24 +126,28 @@ final class VoiceWakeManager: NSObject, ObservableObject {
}
return
}
guard let result else { return }
guard let result else { return }
let transcript = result.bestTranscription.formattedString
if let cmd = self.extractCommand(from: transcript) {
if cmd != self.lastDispatched {
guard let cmd = self.extractCommand(from: transcript) else { return }
if cmd == self.lastDispatched { return }
self.lastDispatched = cmd
self.statusText = "Triggered"
Task { [weak self] in
guard let self else { return }
await self.onCommand?(cmd)
if self.isEnabled {
await self.startIfEnabled()
}
}
private func startIfEnabled() async {
let shouldRestart = self.isEnabled
if shouldRestart {
await self.start()
}
}
}
}
}
}
private func extractCommand(from transcript: String) -> String? {
let lower = transcript.lowercased()
@ -150,7 +163,7 @@ final class VoiceWakeManager: NSObject, ObservableObject {
try session.setCategory(.playAndRecord, mode: .measurement, options: [
.duckOthers,
.mixWithOthers,
.allowBluetooth,
.allowBluetoothHFP,
.defaultToSpeaker,
])
try session.setActive(true, options: [])
@ -158,17 +171,21 @@ final class VoiceWakeManager: NSObject, ObservableObject {
private static func requestMicrophonePermission() async -> Bool {
await withCheckedContinuation { cont in
AVAudioSession.sharedInstance().requestRecordPermission { ok in
AVAudioApplication.requestRecordPermission { ok in
Task { @MainActor in
cont.resume(returning: ok)
}
}
}
}
private static func requestSpeechPermission() async -> Bool {
await withCheckedContinuation { cont in
SFSpeechRecognizer.requestAuthorization { status in
Task { @MainActor in
cont.resume(returning: status == .authorized)
}
}
}
}
}