From 414889e03bfa2da954e50786b2fd56bc84d60e74 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Dec 2025 16:33:49 +0100 Subject: [PATCH] feat: add adaptive voice wake delays --- .../Sources/Clawdis/VoiceWakeOverlay.swift | 9 ++++---- .../Sources/Clawdis/VoiceWakeRuntime.swift | 22 ++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index fb8a3c819..cbe9629da 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -35,7 +35,7 @@ final class VoiceWakeOverlayController: ObservableObject { self.present() } - func presentFinal(transcript: String, forwardConfig: VoiceWakeForwardConfig) { + func presentFinal(transcript: String, forwardConfig: VoiceWakeForwardConfig, delay: TimeInterval) { self.autoSendTask?.cancel() self.forwardConfig = forwardConfig self.model.text = transcript @@ -43,7 +43,7 @@ final class VoiceWakeOverlayController: ObservableObject { self.model.forwardEnabled = forwardConfig.enabled self.model.isSending = false self.present() - self.scheduleAutoSend() + self.scheduleAutoSend(after: delay) } func userBeganEditing() { @@ -178,10 +178,11 @@ final class VoiceWakeOverlayController: ObservableObject { } } - private func scheduleAutoSend() { + private func scheduleAutoSend(after delay: TimeInterval) { guard let forwardConfig, forwardConfig.enabled else { return } self.autoSendTask = Task { [weak self] in - try? await Task.sleep(nanoseconds: 250_000_000) + let nanos = UInt64(delay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanos) self?.sendNow() } } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index e07210cb7..d49153fcb 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -18,6 +18,7 @@ actor VoiceWakeRuntime { private var captureTask: Task? private var capturedTranscript: String = "" private var isCapturing: Bool = false + private var heardBeyondTrigger: Bool = false private var cooldownUntil: Date? private var currentConfig: RuntimeConfig? @@ -144,6 +145,7 @@ actor VoiceWakeRuntime { self.lastHeard = now if self.isCapturing { self.capturedTranscript = transcript + self.updateHeardBeyondTrigger(with: transcript) await MainActor.run { VoiceWakeOverlayController.shared.showPartial(transcript: transcript) } @@ -176,6 +178,7 @@ actor VoiceWakeRuntime { self.capturedTranscript = transcript self.captureStartedAt = Date() self.cooldownUntil = nil + self.heardBeyondTrigger = self.textHasBeyondTriggerContent(transcript) await MainActor.run { VoiceWakeOverlayController.shared.showPartial(transcript: transcript) @@ -226,6 +229,8 @@ actor VoiceWakeRuntime { self.capturedTranscript = "" self.captureStartedAt = nil self.lastHeard = nil + let heardBeyondTrigger = self.heardBeyondTrigger + self.heardBeyondTrigger = false await MainActor.run { AppStateStore.shared.stopVoiceEars() } @@ -237,8 +242,12 @@ actor VoiceWakeRuntime { } let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } + let delay: TimeInterval = heardBeyondTrigger ? 1.0 : 3.0 await MainActor.run { - VoiceWakeOverlayController.shared.presentFinal(transcript: finalTranscript, forwardConfig: forwardConfig) + VoiceWakeOverlayController.shared.presentFinal( + transcript: finalTranscript, + forwardConfig: forwardConfig, + delay: delay) } self.cooldownUntil = Date().addingTimeInterval(self.debounceAfterSend) @@ -254,6 +263,17 @@ actor VoiceWakeRuntime { } } + private func textHasBeyondTriggerContent(_ text: String) -> Bool { + let words = text.split(whereSeparator: { $0.isWhitespace }) + return words.count > 1 + } + + private func updateHeardBeyondTrigger(with transcript: String) { + if !self.heardBeyondTrigger, self.textHasBeyondTriggerContent(transcript) { + self.heardBeyondTrigger = true + } + } + #if DEBUG static func _testMatches(text: String, triggers: [String]) -> Bool { self.matches(text: text, triggers: triggers)