diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2a534ff8e..78a7a0be832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/sessions: move hot transcript reads and mirror appends onto async bounded IO with serialized parent-linked writes, keeping large session histories from stalling Gateway requests and channel replies. Fixes #75656. Thanks @DerFlash. +- macOS/Voice Wake: accept trigger-only phrases in the built-in Voice Wake test, matching the settings UI and runtime trigger-only path instead of requiring extra command text after the wake word. Fixes #64986. Thanks @zoiks65. - Cron/TTS: run cron announce payloads through the normal TTS directive transform before outbound delivery, so scheduled `[[tts]]` replies generate voice payloads instead of leaking raw tags. Fixes #52125. Thanks @kenchen3000. - WhatsApp: save downloadable quoted image media from reply context as inbound media, so agents can inspect an image that a user replied to instead of only seeing ``. Fixes #59174. Thanks @gaffner. - Doctor/WhatsApp: warn when Linux crontabs still run the legacy `ensure-whatsapp.sh` health check, which can misreport `Gateway inactive` when cron lacks the systemd user-bus environment. Fixes #60204. Thanks @mySebbe. diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeRecognitionDebugSupport.swift b/apps/macos/Sources/OpenClaw/VoiceWakeRecognitionDebugSupport.swift index ab436c0b1c5..ac93283d6b5 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeRecognitionDebugSupport.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeRecognitionDebugSupport.swift @@ -48,6 +48,23 @@ enum VoiceWakeRecognitionDebugSupport { trigger: VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers)) } + static func triggerOnlyFallbackMatch( + transcript: String, + triggers: [String], + trimWake: (String, [String]) -> String) -> WakeWordGateMatch? + { + guard VoiceWakeTextUtils.isTriggerOnly( + transcript: transcript, + triggers: triggers, + trimWake: trimWake) + else { return nil } + return WakeWordGateMatch( + triggerEndTime: 0, + postGap: 0, + command: "", + trigger: VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers)) + } + static func transcriptSummary( transcript: String, triggers: [String], diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift index 3d8c93abbd0..3db6a92146f 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeRuntime.swift @@ -517,12 +517,10 @@ actor VoiceWakeRuntime { } private static func isTriggerOnlyText(transcript: String, triggers: [String]) -> Bool { - guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return false } - guard - VoiceWakeTextUtils.startsWithTrigger(transcript: transcript, triggers: triggers) - || VoiceWakeTextUtils.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers) - else { return false } - return self.trimmedAfterTrigger(transcript, triggers: triggers).isEmpty + VoiceWakeTextUtils.isTriggerOnly( + transcript: transcript, + triggers: triggers, + trimWake: self.trimmedAfterTrigger) } private static func matchedTriggerWordText(transcript: String, triggers: [String]) -> String? { diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeTester.swift b/apps/macos/Sources/OpenClaw/VoiceWakeTester.swift index 906f4a1c8b7..2eff26b7765 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeTester.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeTester.swift @@ -230,15 +230,23 @@ final class VoiceWakeTester { if self.holdingAfterDetect { return } - if let match, !match.command.isEmpty { + let triggerOnlyMatch = match == nil + ? VoiceWakeRecognitionDebugSupport.triggerOnlyFallbackMatch( + transcript: text, + triggers: self.currentTriggers, + trimWake: WakeWordGate.stripWake) + : nil + let acceptedMatch = match.flatMap { $0.command.isEmpty ? nil : $0 } ?? triggerOnlyMatch + if let match = acceptedMatch { self.holdingAfterDetect = true - self.detectedText = match.command - self.logger.info("voice wake detected (test) (len=\(match.command.count))") + let detectedText = match.command.isEmpty ? (match.trigger ?? text) : match.command + self.detectedText = detectedText + self.logger.info("voice wake detected (test) (len=\(detectedText.count))") await MainActor.run { AppStateStore.shared.triggerVoiceEars(ttl: nil) } self.stop() await MainActor.run { AppStateStore.shared.stopVoiceEars() - onUpdate(.detected(match.command)) + onUpdate(.detected(detectedText)) } return } @@ -399,20 +407,26 @@ final class VoiceWakeTester { guard !self.isStopping, !self.holdingAfterDetect else { return } guard let lastSeenAt, let lastText else { return } guard self.lastTranscriptAt == lastSeenAt, self.lastTranscript == lastText else { return } - guard let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch( + let gateConfig = WakeWordGateConfig(triggers: triggers) + let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch( transcript: lastText, triggers: triggers, - config: WakeWordGateConfig(triggers: triggers), + config: gateConfig, trimWake: WakeWordGate.stripWake) - else { return } + ?? VoiceWakeRecognitionDebugSupport.triggerOnlyFallbackMatch( + transcript: lastText, + triggers: triggers, + trimWake: WakeWordGate.stripWake) + guard let match else { return } self.holdingAfterDetect = true - self.detectedText = match.command - self.logger.info("voice wake detected (test, silence) (len=\(match.command.count))") + let detectedText = match.command.isEmpty ? (match.trigger ?? lastText) : match.command + self.detectedText = detectedText + self.logger.info("voice wake detected (test, silence) (len=\(detectedText.count))") await MainActor.run { AppStateStore.shared.triggerVoiceEars(ttl: nil) } self.stop() await MainActor.run { AppStateStore.shared.stopVoiceEars() - onUpdate(.detected(match.command)) + onUpdate(.detected(detectedText)) } } } diff --git a/apps/macos/Sources/OpenClaw/VoiceWakeTextUtils.swift b/apps/macos/Sources/OpenClaw/VoiceWakeTextUtils.swift index 3caa6c1a804..d2f2aaf017b 100644 --- a/apps/macos/Sources/OpenClaw/VoiceWakeTextUtils.swift +++ b/apps/macos/Sources/OpenClaw/VoiceWakeTextUtils.swift @@ -149,6 +149,19 @@ enum VoiceWakeTextUtils { return trimmed } + static func isTriggerOnly( + transcript: String, + triggers: [String], + trimWake: TrimWake) -> Bool + { + guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return false } + guard + self.startsWithTrigger(transcript: transcript, triggers: triggers) + || self.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers) + else { return false } + return trimWake(transcript, triggers).isEmpty + } + static func hasOnlyFillerBeforeTrigger(transcript: String, triggers: [String]) -> Bool { guard let match = self.bestRawTriggerMatch(transcript: transcript, triggers: triggers) else { return false } let prefixTokens = transcript[..