VoiceWake: centralize send chime and guard play
This commit is contained in:
@@ -349,19 +349,23 @@ enum CommandResolver {
|
|||||||
|
|
||||||
// Run the real clawdis CLI on the remote host; do not fall back to clawdis-mac.
|
// 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 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 quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ")
|
||||||
|
let userPRJ = settings.projectRoot
|
||||||
|
let prjInit = userPRJ.isEmpty ? "" : "PRJ=\(self.shellQuote(userPRJ));"
|
||||||
let scriptBody = """
|
let scriptBody = """
|
||||||
PATH=\(exportedPath);
|
PATH=\(exportedPath);
|
||||||
CLI="";
|
\(prjInit)
|
||||||
\(prjVar)
|
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
|
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
|
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
|
elif command -v pnpm >/dev/null 2>&1; then
|
||||||
\(cdPrefix)pnpm --silent clawdis \(quotedArgs);
|
pnpm --silent clawdis \(quotedArgs);
|
||||||
else
|
else
|
||||||
echo "clawdis CLI missing on remote host"; exit 127;
|
echo "clawdis CLI missing on remote host"; exit 127;
|
||||||
fi
|
fi
|
||||||
@@ -383,12 +387,16 @@ enum CommandResolver {
|
|||||||
args.append(userHost)
|
args.append(userHost)
|
||||||
|
|
||||||
let exportedPath = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
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 quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ")
|
||||||
let scriptBody = """
|
let scriptBody = """
|
||||||
PATH=\(exportedPath);
|
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;
|
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])
|
args.append(contentsOf: ["/bin/sh", "-c", scriptBody])
|
||||||
return ["/usr/bin/ssh"] + args
|
return ["/usr/bin/ssh"] + args
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ actor VoicePushToTalk {
|
|||||||
private var volatile: String = ""
|
private var volatile: String = ""
|
||||||
private var activeConfig: Config?
|
private var activeConfig: Config?
|
||||||
private var isCapturing = false
|
private var isCapturing = false
|
||||||
|
private var triggerChimePlayed = false
|
||||||
|
|
||||||
private struct Config {
|
private struct Config {
|
||||||
let micID: String?
|
let micID: String?
|
||||||
@@ -99,7 +100,9 @@ actor VoicePushToTalk {
|
|||||||
let config = await MainActor.run { self.makeConfig() }
|
let config = await MainActor.run { self.makeConfig() }
|
||||||
self.activeConfig = config
|
self.activeConfig = config
|
||||||
self.isCapturing = true
|
self.isCapturing = true
|
||||||
|
self.triggerChimePlayed = false
|
||||||
if config.triggerChime != .none {
|
if config.triggerChime != .none {
|
||||||
|
self.triggerChimePlayed = true
|
||||||
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
||||||
}
|
}
|
||||||
await VoiceWakeRuntime.shared.pauseForPushToTalk()
|
await VoiceWakeRuntime.shared.pauseForPushToTalk()
|
||||||
@@ -137,21 +140,21 @@ actor VoicePushToTalk {
|
|||||||
forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
|
forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
|
||||||
}
|
}
|
||||||
|
|
||||||
if !finalText.isEmpty, let chime = self.activeConfig?.sendChime, chime != .none {
|
let chime = finalText.isEmpty ? .none : (self.activeConfig?.sendChime ?? .none)
|
||||||
await MainActor.run { VoiceWakeChimePlayer.play(chime) }
|
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.presentFinal(
|
VoiceWakeOverlayController.shared.presentFinal(
|
||||||
transcript: finalText,
|
transcript: finalText,
|
||||||
forwardConfig: forward,
|
forwardConfig: forward,
|
||||||
delay: finalText.isEmpty ? 0.0 : 0.8,
|
delay: finalText.isEmpty ? 0.0 : 0.8,
|
||||||
|
sendChime: chime,
|
||||||
attributed: attributed)
|
attributed: attributed)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.committed = ""
|
self.committed = ""
|
||||||
self.volatile = ""
|
self.volatile = ""
|
||||||
self.activeConfig = nil
|
self.activeConfig = nil
|
||||||
|
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 MainActor.run {
|
||||||
@@ -209,8 +212,8 @@ actor VoicePushToTalk {
|
|||||||
self.volatile = Self.delta(after: self.committed, current: transcript)
|
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 snapshot = self.committed + self.volatile
|
||||||
|
let attributed = Self.makeAttributed(committed: self.committed, volatile: self.volatile, isFinal: isFinal)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
|
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,15 @@ struct VoiceWakeChimeCatalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
enum VoiceWakeChimePlayer {
|
enum VoiceWakeChimePlayer {
|
||||||
|
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.lastSound = sound
|
||||||
|
sound.stop()
|
||||||
sound.play()
|
sound.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ actor VoiceWakeRuntime {
|
|||||||
private var capturedTranscript: String = ""
|
private var capturedTranscript: String = ""
|
||||||
private var isCapturing: Bool = false
|
private var isCapturing: Bool = false
|
||||||
private var heardBeyondTrigger: Bool = false
|
private var heardBeyondTrigger: Bool = false
|
||||||
|
private var triggerChimePlayed: Bool = false
|
||||||
private var committedTranscript: String = ""
|
private var committedTranscript: String = ""
|
||||||
private var volatileTranscript: String = ""
|
private var volatileTranscript: String = ""
|
||||||
private var cooldownUntil: Date?
|
private var cooldownUntil: Date?
|
||||||
@@ -124,6 +125,7 @@ actor VoiceWakeRuntime {
|
|||||||
self.isCapturing = false
|
self.isCapturing = false
|
||||||
self.capturedTranscript = ""
|
self.capturedTranscript = ""
|
||||||
self.captureStartedAt = nil
|
self.captureStartedAt = nil
|
||||||
|
self.triggerChimePlayed = false
|
||||||
self.recognitionTask?.cancel()
|
self.recognitionTask?.cancel()
|
||||||
self.recognitionTask = nil
|
self.recognitionTask = nil
|
||||||
self.recognitionRequest?.endAudio()
|
self.recognitionRequest?.endAudio()
|
||||||
@@ -203,9 +205,6 @@ actor VoiceWakeRuntime {
|
|||||||
|
|
||||||
private func beginCapture(transcript: String, config: RuntimeConfig) async {
|
private func beginCapture(transcript: String, config: RuntimeConfig) async {
|
||||||
self.isCapturing = true
|
self.isCapturing = true
|
||||||
if config.triggerChime != .none {
|
|
||||||
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
|
||||||
}
|
|
||||||
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
|
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
|
||||||
self.capturedTranscript = trimmed
|
self.capturedTranscript = trimmed
|
||||||
self.committedTranscript = ""
|
self.committedTranscript = ""
|
||||||
@@ -213,6 +212,12 @@ actor VoiceWakeRuntime {
|
|||||||
self.captureStartedAt = Date()
|
self.captureStartedAt = Date()
|
||||||
self.cooldownUntil = nil
|
self.cooldownUntil = nil
|
||||||
self.heardBeyondTrigger = !trimmed.isEmpty
|
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 snapshot = self.committedTranscript + self.volatileTranscript
|
||||||
let attributed = Self.makeAttributed(
|
let attributed = Self.makeAttributed(
|
||||||
@@ -264,6 +269,7 @@ actor VoiceWakeRuntime {
|
|||||||
self.captureStartedAt = nil
|
self.captureStartedAt = nil
|
||||||
self.lastHeard = nil
|
self.lastHeard = nil
|
||||||
self.heardBeyondTrigger = false
|
self.heardBeyondTrigger = false
|
||||||
|
self.triggerChimePlayed = false
|
||||||
|
|
||||||
await MainActor.run { AppStateStore.shared.stopVoiceEars() }
|
await MainActor.run { AppStateStore.shared.stopVoiceEars() }
|
||||||
|
|
||||||
@@ -275,14 +281,13 @@ actor VoiceWakeRuntime {
|
|||||||
committed: finalTranscript,
|
committed: finalTranscript,
|
||||||
volatile: "",
|
volatile: "",
|
||||||
isFinal: true)
|
isFinal: true)
|
||||||
if !finalTranscript.isEmpty, config.sendChime != .none {
|
let sendChime = finalTranscript.isEmpty ? .none : config.sendChime
|
||||||
await MainActor.run { VoiceWakeChimePlayer.play(config.sendChime) }
|
|
||||||
}
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.presentFinal(
|
VoiceWakeOverlayController.shared.presentFinal(
|
||||||
transcript: finalTranscript,
|
transcript: finalTranscript,
|
||||||
forwardConfig: forwardConfig,
|
forwardConfig: forwardConfig,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
|
sendChime: sendChime,
|
||||||
attributed: finalAttributed)
|
attributed: finalAttributed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user