fix(ios): harden voice wake callbacks

This commit is contained in:
Peter Steinberger
2025-12-12 21:59:04 +00:00
parent 13b8dc61ba
commit e31383a8f1

View File

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