mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(macos): require local realtime relay opt-in
Co-authored-by: Zhilong Zheng <zhengzhilong1115@gmail.com> Punchcard-Session: golden-meadow-cedar-dv
This commit is contained in:
@@ -30059,7 +30059,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call-multiline",
|
||||
"line": 399,
|
||||
"line": 403,
|
||||
"path": "apps/macos/Sources/OpenClaw/AppState.swift",
|
||||
"source": "These settings changed outside the app while you were editing: \\(fieldList). Choose which version to keep.",
|
||||
"surface": "apple",
|
||||
@@ -30067,7 +30067,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1017,
|
||||
"line": 1022,
|
||||
"path": "apps/macos/Sources/OpenClaw/AppState.swift",
|
||||
"source": "\\(user)@\\(host)",
|
||||
"surface": "apple",
|
||||
@@ -30075,7 +30075,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1017,
|
||||
"line": 1022,
|
||||
"path": "apps/macos/Sources/OpenClaw/AppState.swift",
|
||||
"source": "\\(user)@\\(host):\\(port)",
|
||||
"surface": "apple",
|
||||
@@ -39139,7 +39139,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 187,
|
||||
"line": 189,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Push-to-talk paused",
|
||||
"surface": "apple",
|
||||
@@ -39147,7 +39147,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 188,
|
||||
"line": 190,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Push-to-Talk resumes when Talk Mode is turned off.",
|
||||
"surface": "apple",
|
||||
@@ -39155,7 +39155,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 196,
|
||||
"line": 198,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Play phase-transition sounds",
|
||||
"surface": "apple",
|
||||
@@ -39163,7 +39163,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument-multiline",
|
||||
"line": 197,
|
||||
"line": 199,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Play short sounds when Talk Mode switches between listening, thinking, and speaking.",
|
||||
"surface": "apple",
|
||||
@@ -39171,7 +39171,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 203,
|
||||
"line": 205,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Right Option stops speech",
|
||||
"surface": "apple",
|
||||
@@ -39179,7 +39179,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 204,
|
||||
"line": 206,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Tap Right Option to interrupt speech and return to listening.",
|
||||
"surface": "apple",
|
||||
@@ -39187,7 +39187,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 209,
|
||||
"line": 211,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Recognition",
|
||||
"surface": "apple",
|
||||
@@ -39195,7 +39195,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 215,
|
||||
"line": 217,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Test",
|
||||
"surface": "apple",
|
||||
@@ -39203,7 +39203,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 315,
|
||||
"line": 317,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Trigger Words",
|
||||
"surface": "apple",
|
||||
@@ -39211,7 +39211,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 317,
|
||||
"line": 319,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Wake phrases",
|
||||
"surface": "apple",
|
||||
@@ -39219,7 +39219,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 318,
|
||||
"line": 320,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Short phrases that start voice wake detection.",
|
||||
"surface": "apple",
|
||||
@@ -39227,7 +39227,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 324,
|
||||
"line": 326,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Add word",
|
||||
"surface": "apple",
|
||||
@@ -39235,7 +39235,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 329,
|
||||
"line": 331,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Reset",
|
||||
"surface": "apple",
|
||||
@@ -39243,7 +39243,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 351,
|
||||
"line": 353,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "No wake phrases configured",
|
||||
"surface": "apple",
|
||||
@@ -39251,7 +39251,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 381,
|
||||
"line": 383,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Sounds",
|
||||
"surface": "apple",
|
||||
@@ -39259,7 +39259,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 383,
|
||||
"line": 385,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Trigger sound",
|
||||
"surface": "apple",
|
||||
@@ -39267,7 +39267,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 387,
|
||||
"line": 389,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Send sound",
|
||||
"surface": "apple",
|
||||
@@ -39275,7 +39275,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 474,
|
||||
"line": 476,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "No Sound",
|
||||
"surface": "apple",
|
||||
@@ -39283,7 +39283,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 482,
|
||||
"line": 484,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Choose file…",
|
||||
"surface": "apple",
|
||||
@@ -39291,7 +39291,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 502,
|
||||
"line": 504,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Play",
|
||||
"surface": "apple",
|
||||
@@ -39299,7 +39299,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 550,
|
||||
"line": 552,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Microphone",
|
||||
"surface": "apple",
|
||||
@@ -39307,7 +39307,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 552,
|
||||
"line": 554,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "System default",
|
||||
"surface": "apple",
|
||||
@@ -39315,7 +39315,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 554,
|
||||
"line": 556,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Unavailable",
|
||||
"surface": "apple",
|
||||
@@ -39323,7 +39323,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 565,
|
||||
"line": 567,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Disconnected (using System default)",
|
||||
"surface": "apple",
|
||||
@@ -39331,7 +39331,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 584,
|
||||
"line": 586,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Recognition language",
|
||||
"surface": "apple",
|
||||
@@ -39339,7 +39339,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 585,
|
||||
"line": 587,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Languages are tried in order. Models may need a first-use download on macOS 26.",
|
||||
"surface": "apple",
|
||||
@@ -39347,7 +39347,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 587,
|
||||
"line": 589,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Language",
|
||||
"surface": "apple",
|
||||
@@ -39355,7 +39355,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 589,
|
||||
"line": 591,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "\\(self.friendlyName(for: current)) (System)",
|
||||
"surface": "apple",
|
||||
@@ -39363,7 +39363,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 601,
|
||||
"line": 603,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Voice Wake needs on-device recognition for its selected language. Push-to-talk and Talk Mode remain available.",
|
||||
"surface": "apple",
|
||||
@@ -39371,7 +39371,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 613,
|
||||
"line": 615,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Additional languages",
|
||||
"surface": "apple",
|
||||
@@ -39379,7 +39379,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 656,
|
||||
"line": 658,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Add another language",
|
||||
"surface": "apple",
|
||||
@@ -39387,7 +39387,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 660,
|
||||
"line": 662,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Add",
|
||||
"surface": "apple",
|
||||
@@ -39395,7 +39395,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 766,
|
||||
"line": 768,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Live level",
|
||||
"surface": "apple",
|
||||
@@ -39403,7 +39403,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 840,
|
||||
"line": 842,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Wake phrase",
|
||||
"surface": "apple",
|
||||
@@ -39411,7 +39411,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 857,
|
||||
"line": 859,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Remove trigger word",
|
||||
"surface": "apple",
|
||||
@@ -39419,7 +39419,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 881,
|
||||
"line": 883,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Language %lld",
|
||||
"surface": "apple",
|
||||
@@ -39427,7 +39427,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 883,
|
||||
"line": 885,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Fallback recognition language.",
|
||||
"surface": "apple",
|
||||
@@ -39435,7 +39435,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 887,
|
||||
"line": 889,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Language \\(self.index + 2)",
|
||||
"surface": "apple",
|
||||
@@ -39443,7 +39443,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 904,
|
||||
"line": 906,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Remove language",
|
||||
"surface": "apple",
|
||||
@@ -39451,12 +39451,28 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-call-concatenated",
|
||||
"line": 919,
|
||||
"line": 921,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "OpenClaw reacts when any trigger appears in a transcription. Keep phrases short to avoid false positives.",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.237e9371a693b0ff"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 938,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Use realtime Gateway relay",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.31708bc96d7b1f5f"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument-multiline",
|
||||
"line": 939,
|
||||
"path": "apps/macos/Sources/OpenClaw/VoiceWakeSettings.swift",
|
||||
"source": "Use the Gateway's configured realtime voice session on this Mac. Requires realtime, gateway-relay, and agent-consult in Talk settings.",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.9ef48cad7ccbf2c2"
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 11,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
extension AppState {
|
||||
func persistTalkRealtimeRelayPreference(previousValue: Bool) {
|
||||
guard !self.isPreview else { return }
|
||||
AppDefaults.standard.set(self.talkRealtimeRelayEnabled, forKey: talkRealtimeRelayEnabledKey)
|
||||
guard self.talkEnabled, self.talkRealtimeRelayEnabled != previousValue else { return }
|
||||
Task { await TalkModeRuntime.shared.realtimeRelayPreferenceDidChange() }
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ final class AppState {
|
||||
private static let logger = Logger(subsystem: "ai.openclaw", category: "app-state")
|
||||
private static let execApprovalsReadRetryAttempts = 5
|
||||
|
||||
private let isPreview: Bool
|
||||
let isPreview: Bool
|
||||
@ObservationIgnored private let execApprovalsDefaultsAsyncResolver:
|
||||
@MainActor () async -> Result<ExecApprovalsResolvedDefaults, ExecApprovalsReadError>
|
||||
@ObservationIgnored private let execApprovalsReadRetryDelay: Duration
|
||||
@@ -258,6 +258,10 @@ final class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
var talkRealtimeRelayEnabled: Bool {
|
||||
didSet { self.persistTalkRealtimeRelayPreference(previousValue: oldValue) }
|
||||
}
|
||||
|
||||
var talkPhaseSoundsEnabled: Bool {
|
||||
didSet {
|
||||
self.ifNotPreview {
|
||||
@@ -485,6 +489,7 @@ final class AppState {
|
||||
self.voiceWakeTriggersTalkMode = AppDefaults.standard
|
||||
.object(forKey: voiceWakeTriggersTalkModeKey) as? Bool ?? false
|
||||
self.talkEnabled = AppDefaults.standard.bool(forKey: talkEnabledKey)
|
||||
self.talkRealtimeRelayEnabled = isTalkRealtimeRelayEnabled()
|
||||
if let storedPhaseSounds = AppDefaults.standard.object(forKey: talkPhaseSoundsEnabledKey) as? Bool {
|
||||
self.talkPhaseSoundsEnabled = storedPhaseSounds
|
||||
} else {
|
||||
@@ -1540,6 +1545,7 @@ extension AppState {
|
||||
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
|
||||
state.voicePushToTalkEnabled = false
|
||||
state.talkEnabled = false
|
||||
state.talkRealtimeRelayEnabled = false
|
||||
state.talkPhaseSoundsEnabled = true
|
||||
state.talkShiftToStopEnabled = true
|
||||
state.iconOverride = .system
|
||||
|
||||
@@ -31,6 +31,7 @@ let voiceWakeAdditionalLocalesKey = "openclaw.voiceWakeAdditionalLocaleIDs"
|
||||
let voicePushToTalkEnabledKey = "openclaw.voicePushToTalkEnabled"
|
||||
let voiceWakeTriggersTalkModeKey = "openclaw.voiceWakeTriggersTalkMode"
|
||||
let talkEnabledKey = "openclaw.talkEnabled"
|
||||
let talkRealtimeRelayEnabledKey = "openclaw.talkRealtimeRelayEnabled"
|
||||
let talkPhaseSoundsEnabledKey = "openclaw.talkPhaseSoundsEnabled"
|
||||
let talkShiftToStopEnabledKey = "openclaw.talkShiftToStopEnabled"
|
||||
let iconOverrideKey = "openclaw.iconOverride"
|
||||
@@ -44,6 +45,10 @@ let quickChatEnabledKey = "openclaw.quickChatEnabled"
|
||||
let cameraEnabledKey = "openclaw.cameraEnabled"
|
||||
let computerControlEnabledKey = "openclaw.computerControlEnabled"
|
||||
|
||||
func isTalkRealtimeRelayEnabled(defaults: UserDefaults = AppDefaults.standard) -> Bool {
|
||||
defaults.object(forKey: talkRealtimeRelayEnabledKey) as? Bool ?? false
|
||||
}
|
||||
|
||||
func isComputerControlEnabled(defaults: UserDefaults = AppDefaults.standard) -> Bool {
|
||||
// object(forKey:) preserves an explicit false; bool(forKey:) would conflate it with an unset default.
|
||||
defaults.object(forKey: computerControlEnabledKey) as? Bool ?? true
|
||||
|
||||
@@ -23,7 +23,7 @@ struct TalkModeGatewayConfigState {
|
||||
let realtimeTransport: String?
|
||||
let realtimeBrain: String?
|
||||
|
||||
var enablesMacOSRealtimeRelay: Bool {
|
||||
var hasGatewayRealtimeRelayTuple: Bool {
|
||||
self.realtimeMode == "realtime" &&
|
||||
self.realtimeTransport == "gateway-relay" &&
|
||||
self.realtimeBrain == "agent-consult"
|
||||
|
||||
@@ -85,7 +85,8 @@ actor TalkModeRuntime {
|
||||
private var realtimeMode: String?
|
||||
private var realtimeTransport: String?
|
||||
private var realtimeBrain: String?
|
||||
private var realtimeRelayEnabled = false
|
||||
private var hasGatewayRealtimeRelayTuple = false
|
||||
private var macOSRealtimeRelayOptIn = false
|
||||
var realtimeSession: RealtimeTalkRelaySession?
|
||||
var realtimeSessionReadyAt: Date?
|
||||
var rapidRealtimeRestartCount = 0
|
||||
@@ -182,6 +183,13 @@ actor TalkModeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
func realtimeRelayPreferenceDidChange() async {
|
||||
guard self.isEnabled else { return }
|
||||
self.lifecycleGeneration &+= 1
|
||||
await self.stop()
|
||||
await self.start()
|
||||
}
|
||||
|
||||
func isCurrent(_ generation: Int) -> Bool {
|
||||
generation == self.lifecycleGeneration && self.isEnabled
|
||||
}
|
||||
@@ -314,15 +322,28 @@ actor TalkModeRuntime {
|
||||
}
|
||||
|
||||
private func shouldAttemptRealtimeRelay() -> Bool {
|
||||
guard self.realtimeMode == "realtime" else { return false }
|
||||
guard self.realtimeRelayEnabled else {
|
||||
guard self.macOSRealtimeRelayOptIn else {
|
||||
self.logger.debug("talk macOS realtime relay disabled locally; using native speech")
|
||||
return false
|
||||
}
|
||||
guard self.hasGatewayRealtimeRelayTuple else {
|
||||
self.logger.warning(
|
||||
"talk macOS realtime relay not explicitly enabled: " +
|
||||
"talk macOS realtime relay opted in but Gateway tuple is incompatible: " +
|
||||
"mode=\(self.realtimeMode ?? "missing", privacy: .public) " +
|
||||
"transport=\(self.realtimeTransport ?? "missing", privacy: .public) " +
|
||||
"brain=\(self.realtimeBrain ?? "missing", privacy: .public); using native fallback")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return Self.shouldUseRealtimeRelay(
|
||||
localOptIn: self.macOSRealtimeRelayOptIn,
|
||||
hasGatewayRealtimeRelayTuple: self.hasGatewayRealtimeRelayTuple)
|
||||
}
|
||||
|
||||
static func shouldUseRealtimeRelay(
|
||||
localOptIn: Bool,
|
||||
hasGatewayRealtimeRelayTuple: Bool) -> Bool
|
||||
{
|
||||
localOptIn && hasGatewayRealtimeRelayTuple
|
||||
}
|
||||
|
||||
// MARK: - Speech recognition
|
||||
@@ -1452,7 +1473,10 @@ extension TalkModeRuntime {
|
||||
self.realtimeMode = cfg.realtimeMode
|
||||
self.realtimeTransport = cfg.realtimeTransport
|
||||
self.realtimeBrain = cfg.realtimeBrain
|
||||
self.realtimeRelayEnabled = cfg.enablesMacOSRealtimeRelay
|
||||
self.hasGatewayRealtimeRelayTuple = cfg.hasGatewayRealtimeRelayTuple
|
||||
self.macOSRealtimeRelayOptIn = await MainActor.run {
|
||||
AppStateStore.shared.talkRealtimeRelayEnabled
|
||||
}
|
||||
let configuredSilenceMs = cfg.silenceTimeoutMs
|
||||
let locale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
|
||||
let isCJKLocale = locale.hasPrefix("ko") || locale.hasPrefix("ja") || locale.hasPrefix("zh")
|
||||
@@ -1483,7 +1507,8 @@ extension TalkModeRuntime {
|
||||
"speechLocale=\(cfg.speechLocaleID ?? "device", privacy: .public) " +
|
||||
"realtimeMode=\(cfg.realtimeMode ?? "off", privacy: .public) " +
|
||||
"realtimeTransport=\(cfg.realtimeTransport ?? "default", privacy: .public) " +
|
||||
"realtimeBrain=\(cfg.realtimeBrain ?? "default", privacy: .public)")
|
||||
"realtimeBrain=\(cfg.realtimeBrain ?? "default", privacy: .public) " +
|
||||
"macOSRealtimeOptIn=\(self.macOSRealtimeRelayOptIn, privacy: .public)")
|
||||
}
|
||||
|
||||
static func selectTalkProviderConfig(
|
||||
|
||||
@@ -182,6 +182,8 @@ struct VoiceWakeSettings: View {
|
||||
subtitle: "Start listening while you hold the key and show the preview overlay.",
|
||||
binding: self.$state.voicePushToTalkEnabled)
|
||||
|
||||
self.realtimeRelayToggle
|
||||
|
||||
if self.state.voicePushToTalkEnabled, self.state.talkEnabled {
|
||||
SettingsCardRow(
|
||||
title: "Push-to-talk paused",
|
||||
@@ -930,6 +932,18 @@ private struct TriggerPhraseHelpRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
extension VoiceWakeSettings {
|
||||
private var realtimeRelayToggle: some View {
|
||||
SettingsCardToggleRow(
|
||||
title: "Use realtime Gateway relay",
|
||||
subtitle: """
|
||||
Use the Gateway's configured realtime voice session on this Mac. \
|
||||
Requires realtime, gateway-relay, and agent-consult in Talk settings.
|
||||
""",
|
||||
binding: self.$state.talkRealtimeRelayEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct VoiceWakeSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
@@ -80,7 +80,7 @@ struct TalkModeGatewayConfigTests {
|
||||
#expect(parsed.realtimeMode == "realtime")
|
||||
#expect(parsed.realtimeTransport == "gateway-relay")
|
||||
#expect(parsed.realtimeBrain == "agent-consult")
|
||||
#expect(parsed.enablesMacOSRealtimeRelay)
|
||||
#expect(parsed.hasGatewayRealtimeRelayTuple)
|
||||
}
|
||||
|
||||
@Test func `realtime config infers its sole provider and reads provider defaults`() {
|
||||
@@ -104,7 +104,7 @@ struct TalkModeGatewayConfigTests {
|
||||
#expect(parsed.realtimeMode == "realtime")
|
||||
#expect(parsed.realtimeTransport == nil)
|
||||
#expect(parsed.realtimeBrain == nil)
|
||||
#expect(!parsed.enablesMacOSRealtimeRelay)
|
||||
#expect(!parsed.hasGatewayRealtimeRelayTuple)
|
||||
}
|
||||
|
||||
@Test func `realtime provider config lookup is case insensitive`() {
|
||||
|
||||
@@ -88,6 +88,30 @@ private final class RuntimeTestPCMPlayer: PCMStreamingAudioPlaying {
|
||||
}
|
||||
|
||||
struct TalkModeRuntimeSpeechTests {
|
||||
@Test func `macOS realtime relay requires local opt in and exact Gateway tuple`() {
|
||||
#expect(!TalkModeRuntime.shouldUseRealtimeRelay(
|
||||
localOptIn: false,
|
||||
hasGatewayRealtimeRelayTuple: false))
|
||||
#expect(!TalkModeRuntime.shouldUseRealtimeRelay(
|
||||
localOptIn: false,
|
||||
hasGatewayRealtimeRelayTuple: true))
|
||||
#expect(!TalkModeRuntime.shouldUseRealtimeRelay(
|
||||
localOptIn: true,
|
||||
hasGatewayRealtimeRelayTuple: false))
|
||||
#expect(TalkModeRuntime.shouldUseRealtimeRelay(
|
||||
localOptIn: true,
|
||||
hasGatewayRealtimeRelayTuple: true))
|
||||
}
|
||||
|
||||
@Test @MainActor func `macOS realtime relay preference defaults off and reads explicit opt in`() async {
|
||||
await TestIsolation.withUserDefaultsValues([talkRealtimeRelayEnabledKey: nil]) {
|
||||
#expect(!AppState(preview: true).talkRealtimeRelayEnabled)
|
||||
}
|
||||
await TestIsolation.withUserDefaultsValues([talkRealtimeRelayEnabledKey: true]) {
|
||||
#expect(AppState(preview: true).talkRealtimeRelayEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `speech request uses dictation defaults`() {
|
||||
let request = SFSpeechAudioBufferRecognitionRequest()
|
||||
|
||||
|
||||
@@ -72,6 +72,10 @@ of these together:
|
||||
|
||||
Any other combination — including a partially set one — keeps the native path.
|
||||
|
||||
The Mac must also opt in locally with **Settings > Voice Wake > Use realtime Gateway relay**.
|
||||
This preference defaults off and stays on that Mac; Gateway config alone never activates the
|
||||
streamed path.
|
||||
|
||||
The Gateway must also advertise `gateway-relay` and `agent-consult` for the selected provider in
|
||||
`talk.catalog`. Realtime requires macOS 26 or newer, matching Voice Wake; on older versions the
|
||||
Talk runtime returns before startup and the native path is the only one available.
|
||||
|
||||
Reference in New Issue
Block a user