Voice wake: log overlay lifecycle and enforce PTT cooldown

This commit is contained in:
Peter Steinberger
2025-12-09 03:20:52 +01:00
parent 912a53318e
commit 3a42979e53
4 changed files with 20 additions and 16 deletions

View File

@@ -156,11 +156,8 @@ actor VoicePushToTalk {
self.triggerChimePlayed = false self.triggerChimePlayed = false
// Resume the wake-word runtime after push-to-talk finishes. // Resume the wake-word runtime after push-to-talk finishes.
_ = await MainActor.run { await VoiceWakeRuntime.shared.applyPushToTalkCooldown()
Task { _ = await MainActor.run { Task { await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared) } }
await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared)
}
}
} }
// MARK: - Private // MARK: - Private

View File

@@ -1,5 +1,6 @@
import AppKit import AppKit
import Foundation import Foundation
import OSLog
enum VoiceWakeChime: Codable, Equatable, Sendable { enum VoiceWakeChime: Codable, Equatable, Sendable {
case none case none
@@ -44,11 +45,13 @@ struct VoiceWakeChimeCatalog {
@MainActor @MainActor
enum VoiceWakeChimePlayer { enum VoiceWakeChimePlayer {
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "voicewake.chime")
private static var lastSound: NSSound? private static var lastSound: NSSound?
@MainActor @MainActor
static func play(_ chime: VoiceWakeChime) { static func play(_ chime: VoiceWakeChime) {
guard let sound = self.sound(for: chime) else { return } guard let sound = self.sound(for: chime) else { return }
self.logger.log(level: .info, "chime play type=\(String(describing: chime), privacy: .public) name=\(sound.name ?? "", privacy: .public)")
self.lastSound = sound self.lastSound = sound
sound.stop() sound.stop()
sound.play() sound.play()

View File

@@ -39,7 +39,7 @@ final class VoiceWakeOverlayController: ObservableObject {
let closeOverflow: CGFloat = 10 let closeOverflow: CGFloat = 10
func showPartial(transcript: String, attributed: NSAttributedString? = nil) { func showPartial(transcript: String, attributed: NSAttributedString? = nil) {
self.logger.debug("overlay showPartial len=\(transcript.count, privacy: .public) visible=\(self.model.isVisible, privacy: .public) isFinal=false") self.logger.log(level: .info, "overlay showPartial len=\(transcript.count, privacy: .public) visible=\(self.model.isVisible, privacy: .public) isFinal=false")
self.autoSendTask?.cancel() self.autoSendTask?.cancel()
self.forwardConfig = nil self.forwardConfig = nil
self.model.text = transcript self.model.text = transcript
@@ -60,7 +60,7 @@ final class VoiceWakeOverlayController: ObservableObject {
sendChime: VoiceWakeChime = .none, sendChime: VoiceWakeChime = .none,
attributed: NSAttributedString? = nil) attributed: NSAttributedString? = nil)
{ {
self.logger.debug("overlay presentFinal len=\(transcript.count, privacy: .public) autoSendAfter=\(delay ?? -1, privacy: .public) forwardEnabled=\(forwardConfig.enabled, privacy: .public)") self.logger.log(level: .info, "overlay presentFinal len=\(transcript.count, privacy: .public) autoSendAfter=\(delay ?? -1, privacy: .public) forwardEnabled=\(forwardConfig.enabled, privacy: .public)")
self.autoSendTask?.cancel() self.autoSendTask?.cancel()
self.forwardConfig = forwardConfig self.forwardConfig = forwardConfig
self.model.text = transcript self.model.text = transcript
@@ -101,31 +101,31 @@ final class VoiceWakeOverlayController: ObservableObject {
} }
func sendNow(sendChime: VoiceWakeChime = .none) { func sendNow(sendChime: VoiceWakeChime = .none) {
self.logger.debug("overlay sendNow called isSending=\(self.model.isSending, privacy: .public) forwardEnabled=\(self.model.forwardEnabled, privacy: .public) textLen=\(self.model.text.count, privacy: .public)") self.logger.log(level: .info, "overlay sendNow called isSending=\(self.model.isSending, privacy: .public) forwardEnabled=\(self.model.forwardEnabled, privacy: .public) textLen=\(self.model.text.count, privacy: .public)")
self.autoSendTask?.cancel() self.autoSendTask?.cancel()
self.autoSendTask = nil self.autoSendTask = nil
if self.model.isSending { return } if self.model.isSending { return }
self.model.isEditing = false self.model.isEditing = false
guard let forwardConfig, forwardConfig.enabled else { guard let forwardConfig, forwardConfig.enabled else {
self.logger.debug("overlay sendNow disabled -> dismiss") self.logger.log(level: .info, "overlay sendNow disabled -> dismiss")
self.dismiss(reason: .explicit) self.dismiss(reason: .explicit)
return return
} }
let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines) let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { guard !text.isEmpty else {
self.logger.debug("overlay sendNow empty -> dismiss") self.logger.log(level: .info, "overlay sendNow empty -> dismiss")
self.dismiss(reason: .empty) self.dismiss(reason: .empty)
return return
} }
if sendChime != .none { if sendChime != .none {
self.logger.debug("overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)") self.logger.log(level: .info, "overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)")
VoiceWakeChimePlayer.play(sendChime) VoiceWakeChimePlayer.play(sendChime)
} }
self.model.isSending = true self.model.isSending = true
let payload = VoiceWakeForwarder.prefixedTranscript(text) let payload = VoiceWakeForwarder.prefixedTranscript(text)
self.logger.debug("overlay sendNow forwarding len=\(payload.count, privacy: .public) target=\(forwardConfig.target, privacy: .public)") self.logger.log(level: .info, "overlay sendNow forwarding len=\(payload.count, privacy: .public) target=\(forwardConfig.target, privacy: .public)")
Task.detached { Task.detached {
await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig) await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig)
} }
@@ -135,7 +135,7 @@ final class VoiceWakeOverlayController: ObservableObject {
} }
func dismiss(reason: DismissReason = .explicit, outcome: SendOutcome = .empty) { func dismiss(reason: DismissReason = .explicit, outcome: SendOutcome = .empty) {
self.logger.debug("overlay dismiss reason=\(String(describing: reason), privacy: .public) outcome=\(String(describing: outcome), privacy: .public) visible=\(self.model.isVisible, privacy: .public) sending=\(self.model.isSending, privacy: .public)") self.logger.log(level: .info, "overlay dismiss reason=\(String(describing: reason), privacy: .public) outcome=\(String(describing: outcome), privacy: .public) visible=\(self.model.isVisible, privacy: .public) sending=\(self.model.isSending, privacy: .public)")
self.autoSendTask?.cancel() self.autoSendTask?.cancel()
self.model.isSending = false self.model.isSending = false
self.model.isEditing = false self.model.isEditing = false
@@ -180,7 +180,7 @@ final class VoiceWakeOverlayController: ObservableObject {
guard let window else { return } guard let window else { return }
if !self.model.isVisible { if !self.model.isVisible {
self.model.isVisible = true self.model.isVisible = true
self.logger.debug("overlay present windowShown textLen=\(self.model.text.count, privacy: .public)") self.logger.log(level: .info, "overlay present windowShown textLen=\(self.model.text.count, privacy: .public)")
// Keep the status item in listening mode until we explicitly dismiss the overlay. // Keep the status item in listening mode until we explicitly dismiss the overlay.
AppStateStore.shared.triggerVoiceEars(ttl: nil) AppStateStore.shared.triggerVoiceEars(ttl: nil)
let start = target.offsetBy(dx: 0, dy: -6) let start = target.offsetBy(dx: 0, dy: -6)
@@ -293,7 +293,7 @@ final class VoiceWakeOverlayController: ObservableObject {
} }
private func scheduleAutoSend(after delay: TimeInterval, sendChime: VoiceWakeChime) { private func scheduleAutoSend(after delay: TimeInterval, sendChime: VoiceWakeChime) {
self.logger.debug("overlay scheduleAutoSend after=\(delay, privacy: .public) sendChime=\(String(describing: sendChime), privacy: .public)") self.logger.log(level: .info, "overlay scheduleAutoSend after=\(delay, privacy: .public) sendChime=\(String(describing: sendChime), privacy: .public)")
self.autoSendTask?.cancel() self.autoSendTask?.cancel()
self.autoSendTask = Task<Void, Never> { [weak self, sendChime] in self.autoSendTask = Task<Void, Never> { [weak self, sendChime] in
let nanos = UInt64(max(0, delay) * 1_000_000_000) let nanos = UInt64(max(0, delay) * 1_000_000_000)
@@ -301,7 +301,7 @@ final class VoiceWakeOverlayController: ObservableObject {
guard !Task.isCancelled else { return } guard !Task.isCancelled else { return }
await MainActor.run { await MainActor.run {
guard let self else { return } guard let self else { return }
self.logger.debug("overlay autoSend firing") self.logger.log(level: .info, "overlay autoSend firing")
self.sendNow(sendChime: sendChime) self.sendNow(sendChime: sendChime)
self.autoSendTask = nil self.autoSendTask = nil
} }

View File

@@ -375,6 +375,10 @@ actor VoiceWakeRuntime {
} }
} }
func applyPushToTalkCooldown() {
self.cooldownUntil = Date().addingTimeInterval(self.debounceAfterSend)
}
func pauseForPushToTalk() { func pauseForPushToTalk() {
self.listeningState = .pushToTalk self.listeningState = .pushToTalk
self.stop(dismissOverlay: false) self.stop(dismissOverlay: false)