Voice wake: log overlay lifecycle and enforce PTT cooldown
parent
912a53318e
commit
3a42979e53
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue