diff --git a/apps/macos/Sources/Clawdis/AppMain.swift b/apps/macos/Sources/Clawdis/AppMain.swift index e2ce59daa..51cb6de1a 100644 --- a/apps/macos/Sources/Clawdis/AppMain.swift +++ b/apps/macos/Sources/Clawdis/AppMain.swift @@ -1319,6 +1319,12 @@ enum VoiceWakeTestState: Equatable { case failed(String) } +private struct AudioInputDevice: Identifiable, Equatable { + let uid: String + let name: String + var id: String { uid } +} + actor VoiceWakeTester { private let recognizer: SFSpeechRecognizer? private let audioEngine = AVAudioEngine() @@ -1329,7 +1335,7 @@ actor VoiceWakeTester { self.recognizer = SFSpeechRecognizer(locale: locale) } - func start(triggers: [String], onUpdate: @MainActor @escaping @Sendable (VoiceWakeTestState) -> Void) async throws { + func start(triggers: [String], micID: String?, onUpdate: @MainActor @escaping @Sendable (VoiceWakeTestState) -> Void) async throws { guard recognitionTask == nil else { return } guard let recognizer, recognizer.isAvailable else { throw NSError(domain: "VoiceWakeTester", code: 1, userInfo: [NSLocalizedDescriptionKey: "Speech recognition unavailable"]) @@ -1344,6 +1350,8 @@ actor VoiceWakeTester { throw NSError(domain: "VoiceWakeTester", code: 2, userInfo: [NSLocalizedDescriptionKey: "Microphone or speech permission denied"]) } + configureSession(preferredMicID: micID) + recognitionRequest = SFSpeechAudioBufferRecognitionRequest() recognitionRequest?.shouldReportPartialResults = true @@ -1390,16 +1398,22 @@ actor VoiceWakeTester { onUpdate: @MainActor @escaping @Sendable (VoiceWakeTestState) -> Void ) async { if matched, let text { - await stop() + stop() await MainActor.run { onUpdate(.detected(text)) } return } if let errorMessage { - await stop() + stop() await MainActor.run { onUpdate(.failed(errorMessage)) } } } + private func configureSession(preferredMicID: String?) { + // macOS uses the system default input for AVAudioEngine. Selection is stored for future + // pipeline wiring; test currently relies on the system default device. + _ = preferredMicID + } + private static func matches(text: String, triggers: [String]) -> Bool { let lowered = text.lowercased() return triggers.contains { lowered.contains($0.lowercased()) } @@ -1758,6 +1772,7 @@ struct VoiceWakeSettings: View { do { try await tester.start( triggers: triggers, + micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID, onUpdate: { newState in self.testState = newState if case .detected = newState { self.isTesting = false }