VoiceWake: centralize send chime and guard play

This commit is contained in:
Peter Steinberger
2025-12-08 21:25:30 +01:00
parent 7a82777fc5
commit bb3606b64f
4 changed files with 40 additions and 19 deletions

View File

@@ -349,19 +349,23 @@ enum CommandResolver {
// Run the real clawdis CLI on the remote host; do not fall back to clawdis-mac.
let exportedPath = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm:$PATH"
let cdPrefix = settings.projectRoot.isEmpty ? "" : "cd \(self.shellQuote(settings.projectRoot)) && "
let prjVar = settings.projectRoot.isEmpty ? "" : "PRJ=\(self.shellQuote(settings.projectRoot)); "
let quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ")
let userPRJ = settings.projectRoot
let prjInit = userPRJ.isEmpty ? "" : "PRJ=\(self.shellQuote(userPRJ));"
let scriptBody = """
PATH=\(exportedPath);
CLI="";
\(prjVar)
\(prjInit)
DEFAULT_PRJ="$HOME/Projects/clawdis"
if [ -z "${PRJ:-}" ] && [ -d "$DEFAULT_PRJ" ]; then PRJ="$DEFAULT_PRJ"; fi
if [ -n "${PRJ:-}" ]; then
cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; }
fi
if command -v clawdis >/dev/null 2>&1; then
\(cdPrefix)clawdis \(quotedArgs);
clawdis \(quotedArgs);
elif [ -n "${PRJ:-}" ] && [ -f "$PRJ/bin/clawdis.js" ] && command -v node >/dev/null 2>&1; then
\(cdPrefix)node "$PRJ/bin/clawdis.js" \(quotedArgs);
node "$PRJ/bin/clawdis.js" \(quotedArgs);
elif command -v pnpm >/dev/null 2>&1; then
\(cdPrefix)pnpm --silent clawdis \(quotedArgs);
pnpm --silent clawdis \(quotedArgs);
else
echo "clawdis CLI missing on remote host"; exit 127;
fi
@@ -383,12 +387,16 @@ enum CommandResolver {
args.append(userHost)
let exportedPath = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
let cdPrefix = settings.projectRoot.isEmpty ? "" : "cd \(self.shellQuote(settings.projectRoot)) && "
let userPRJ = settings.projectRoot
let quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ")
let scriptBody = """
PATH=\(exportedPath);
PRJ=\(userPRJ.isEmpty ? "" : self.shellQuote(userPRJ))
DEFAULT_PRJ="$HOME/Projects/clawdis"
if [ -z "${PRJ:-}" ] && [ -d "$DEFAULT_PRJ" ]; then PRJ="$DEFAULT_PRJ"; fi
if [ -n "${PRJ:-}" ]; then cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; }; fi
if ! command -v clawdis-mac >/dev/null 2>&1; then echo "clawdis-mac missing on remote host"; exit 127; fi;
\(cdPrefix)clawdis-mac \(quotedArgs)
clawdis-mac \(quotedArgs)
"""
args.append(contentsOf: ["/bin/sh", "-c", scriptBody])
return ["/usr/bin/ssh"] + args

View File

@@ -79,6 +79,7 @@ actor VoicePushToTalk {
private var volatile: String = ""
private var activeConfig: Config?
private var isCapturing = false
private var triggerChimePlayed = false
private struct Config {
let micID: String?
@@ -99,7 +100,9 @@ actor VoicePushToTalk {
let config = await MainActor.run { self.makeConfig() }
self.activeConfig = config
self.isCapturing = true
self.triggerChimePlayed = false
if config.triggerChime != .none {
self.triggerChimePlayed = true
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
}
await VoiceWakeRuntime.shared.pauseForPushToTalk()
@@ -137,21 +140,21 @@ actor VoicePushToTalk {
forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
}
if !finalText.isEmpty, let chime = self.activeConfig?.sendChime, chime != .none {
await MainActor.run { VoiceWakeChimePlayer.play(chime) }
}
let chime = finalText.isEmpty ? .none : (self.activeConfig?.sendChime ?? .none)
await MainActor.run {
VoiceWakeOverlayController.shared.presentFinal(
transcript: finalText,
forwardConfig: forward,
delay: finalText.isEmpty ? 0.0 : 0.8,
sendChime: chime,
attributed: attributed)
}
self.committed = ""
self.volatile = ""
self.activeConfig = nil
self.triggerChimePlayed = false
// Resume the wake-word runtime after push-to-talk finishes.
_ = await MainActor.run {
@@ -209,8 +212,8 @@ actor VoicePushToTalk {
self.volatile = Self.delta(after: self.committed, current: transcript)
}
let attributed = Self.makeAttributed(committed: self.committed, volatile: self.volatile, isFinal: isFinal)
let snapshot = self.committed + self.volatile
let attributed = Self.makeAttributed(committed: self.committed, volatile: self.volatile, isFinal: isFinal)
await MainActor.run {
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
}

View File

@@ -42,10 +42,15 @@ struct VoiceWakeChimeCatalog {
}
}
@MainActor
enum VoiceWakeChimePlayer {
private static var lastSound: NSSound?
@MainActor
static func play(_ chime: VoiceWakeChime) {
guard let sound = self.sound(for: chime) else { return }
self.lastSound = sound
sound.stop()
sound.play()
}

View File

@@ -22,6 +22,7 @@ actor VoiceWakeRuntime {
private var capturedTranscript: String = ""
private var isCapturing: Bool = false
private var heardBeyondTrigger: Bool = false
private var triggerChimePlayed: Bool = false
private var committedTranscript: String = ""
private var volatileTranscript: String = ""
private var cooldownUntil: Date?
@@ -124,6 +125,7 @@ actor VoiceWakeRuntime {
self.isCapturing = false
self.capturedTranscript = ""
self.captureStartedAt = nil
self.triggerChimePlayed = false
self.recognitionTask?.cancel()
self.recognitionTask = nil
self.recognitionRequest?.endAudio()
@@ -203,9 +205,6 @@ actor VoiceWakeRuntime {
private func beginCapture(transcript: String, config: RuntimeConfig) async {
self.isCapturing = true
if config.triggerChime != .none {
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
}
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
self.capturedTranscript = trimmed
self.committedTranscript = ""
@@ -213,6 +212,12 @@ actor VoiceWakeRuntime {
self.captureStartedAt = Date()
self.cooldownUntil = nil
self.heardBeyondTrigger = !trimmed.isEmpty
self.triggerChimePlayed = false
if config.triggerChime != .none {
self.triggerChimePlayed = true
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
}
let snapshot = self.committedTranscript + self.volatileTranscript
let attributed = Self.makeAttributed(
@@ -264,6 +269,7 @@ actor VoiceWakeRuntime {
self.captureStartedAt = nil
self.lastHeard = nil
self.heardBeyondTrigger = false
self.triggerChimePlayed = false
await MainActor.run { AppStateStore.shared.stopVoiceEars() }
@@ -275,14 +281,13 @@ actor VoiceWakeRuntime {
committed: finalTranscript,
volatile: "",
isFinal: true)
if !finalTranscript.isEmpty, config.sendChime != .none {
await MainActor.run { VoiceWakeChimePlayer.play(config.sendChime) }
}
let sendChime = finalTranscript.isEmpty ? .none : config.sendChime
await MainActor.run {
VoiceWakeOverlayController.shared.presentFinal(
transcript: finalTranscript,
forwardConfig: forwardConfig,
delay: delay,
sendChime: sendChime,
attributed: finalAttributed)
}