Skip to content

Commit 4f482d2

Browse files
committed
refactor: share Apple talk config parsing
1 parent eba9dcc commit 4f482d2

File tree

7 files changed

+267
-186
lines changed

7 files changed

+267
-186
lines changed

apps/ios/Sources/Voice/TalkModeManager.swift

Lines changed: 17 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,57 +1970,15 @@ extension TalkModeManager {
19701970
return trimmed
19711971
}
19721972

1973-
struct TalkProviderConfigSelection {
1974-
let provider: String
1975-
let config: [String: Any]
1976-
}
1977-
1978-
private static func normalizedTalkProviderID(_ raw: String?) -> String? {
1979-
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
1980-
return trimmed.isEmpty ? nil : trimmed
1981-
}
1982-
1983-
static func selectTalkProviderConfig(_ talk: [String: Any]?) -> TalkProviderConfigSelection? {
1984-
guard let talk else { return nil }
1985-
let rawProvider = talk["provider"] as? String
1986-
let rawProviders = talk["providers"] as? [String: Any]
1987-
guard rawProvider != nil || rawProviders != nil else { return nil }
1988-
let providers = rawProviders ?? [:]
1989-
let normalizedProviders = providers.reduce(into: [String: [String: Any]]()) { acc, entry in
1990-
guard
1991-
let providerID = Self.normalizedTalkProviderID(entry.key),
1992-
let config = entry.value as? [String: Any]
1993-
else { return }
1994-
acc[providerID] = config
1995-
}
1996-
let providerID =
1997-
Self.normalizedTalkProviderID(rawProvider) ??
1998-
normalizedProviders.keys.min() ??
1999-
Self.defaultTalkProvider
2000-
return TalkProviderConfigSelection(
2001-
provider: providerID,
2002-
config: normalizedProviders[providerID] ?? [:])
2003-
}
2004-
2005-
static func resolvedSilenceTimeoutMs(_ talk: [String: Any]?) -> Int {
2006-
switch talk?["silenceTimeoutMs"] {
2007-
case let timeout as Int where timeout > 0:
2008-
return timeout
2009-
case let timeout as Double
2010-
where timeout > 0 && timeout.rounded(.towardZero) == timeout && timeout <= Double(Int.max):
2011-
return Int(timeout)
2012-
case let timeout as NSNumber:
2013-
if CFGetTypeID(timeout) == CFBooleanGetTypeID() {
2014-
return Self.defaultSilenceTimeoutMs
2015-
}
2016-
let value = timeout.doubleValue
2017-
if value > 0 && value.rounded(.towardZero) == value && value <= Double(Int.max) {
2018-
return Int(value)
2019-
}
2020-
return Self.defaultSilenceTimeoutMs
2021-
default:
2022-
return Self.defaultSilenceTimeoutMs
2023-
}
1973+
static func selectTalkProviderConfig(_ talk: [String: AnyCodable]?) -> TalkProviderConfigSelection? {
1974+
TalkConfigParsing.selectProviderConfig(
1975+
talk,
1976+
defaultProvider: Self.defaultTalkProvider,
1977+
allowLegacyFallback: false)
1978+
}
1979+
1980+
static func resolvedSilenceTimeoutMs(_ talk: [String: AnyCodable]?) -> Int {
1981+
TalkConfigParsing.resolvedSilenceTimeoutMs(talk, fallback: Self.defaultSilenceTimeoutMs)
20241982
}
20251983

20261984
func reloadConfig() async {
@@ -2034,7 +1992,7 @@ extension TalkModeManager {
20341992
)
20351993
guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else { return }
20361994
guard let config = json["config"] as? [String: Any] else { return }
2037-
let talk = config["talk"] as? [String: Any]
1995+
let talk = TalkConfigParsing.bridgeFoundationDictionary(config["talk"] as? [String: Any])
20381996
let selection = Self.selectTalkProviderConfig(talk)
20391997
if talk != nil, selection == nil {
20401998
GatewayDiagnostics.log(
@@ -2043,12 +2001,12 @@ extension TalkModeManager {
20432001
let activeProvider = selection?.provider ?? Self.defaultTalkProvider
20442002
let activeConfig = selection?.config
20452003
let silenceTimeoutMs = Self.resolvedSilenceTimeoutMs(talk)
2046-
self.defaultVoiceId = (activeConfig?["voiceId"] as? String)?
2004+
self.defaultVoiceId = activeConfig?["voiceId"]?.stringValue?
20472005
.trimmingCharacters(in: .whitespacesAndNewlines)
2048-
if let aliases = activeConfig?["voiceAliases"] as? [String: Any] {
2006+
if let aliases = activeConfig?["voiceAliases"]?.dictionaryValue {
20492007
var resolved: [String: String] = [:]
20502008
for (key, value) in aliases {
2051-
guard let id = value as? String else { continue }
2009+
guard let id = value.stringValue else { continue }
20522010
let normalizedKey = key.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
20532011
let trimmedId = id.trimmingCharacters(in: .whitespacesAndNewlines)
20542012
guard !normalizedKey.isEmpty, !trimmedId.isEmpty else { continue }
@@ -2061,14 +2019,14 @@ extension TalkModeManager {
20612019
if !self.voiceOverrideActive {
20622020
self.currentVoiceId = self.defaultVoiceId
20632021
}
2064-
let model = (activeConfig?["modelId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
2022+
let model = activeConfig?["modelId"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
20652023
self.defaultModelId = (model?.isEmpty == false) ? model : Self.defaultModelIdFallback
20662024
if !self.modelOverrideActive {
20672025
self.currentModelId = self.defaultModelId
20682026
}
2069-
self.defaultOutputFormat = (activeConfig?["outputFormat"] as? String)?
2027+
self.defaultOutputFormat = activeConfig?["outputFormat"]?.stringValue?
20702028
.trimmingCharacters(in: .whitespacesAndNewlines)
2071-
let rawConfigApiKey = (activeConfig?["apiKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
2029+
let rawConfigApiKey = activeConfig?["apiKey"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
20722030
let configApiKey = Self.normalizedTalkApiKey(rawConfigApiKey)
20732031
let localApiKey = Self.normalizedTalkApiKey(
20742032
GatewaySettingsStore.loadTalkProviderApiKey(provider: activeProvider))
@@ -2087,7 +2045,7 @@ extension TalkModeManager {
20872045
self.gatewayTalkDefaultModelId = self.defaultModelId
20882046
self.gatewayTalkApiKeyConfigured = (self.apiKey?.isEmpty == false)
20892047
self.gatewayTalkConfigLoaded = true
2090-
if let interrupt = talk?["interruptOnSpeech"] as? Bool {
2048+
if let interrupt = talk?["interruptOnSpeech"]?.boolValue {
20912049
self.interruptOnSpeech = interrupt
20922050
}
20932051
self.silenceWindow = TimeInterval(silenceTimeoutMs) / 1000

apps/ios/Tests/TalkModeConfigParsingTests.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import OpenClawKit
23
import Testing
34
@testable import OpenClaw
45

@@ -15,9 +16,10 @@ import Testing
1516
"voiceId": "voice-legacy",
1617
]
1718

18-
let selection = TalkModeManager.selectTalkProviderConfig(talk)
19+
let selection = TalkModeManager.selectTalkProviderConfig(
20+
TalkConfigParsing.bridgeFoundationDictionary(talk))
1921
#expect(selection?.provider == "elevenlabs")
20-
#expect(selection?.config["voiceId"] as? String == "voice-normalized")
22+
#expect(selection?.config["voiceId"]?.stringValue == "voice-normalized")
2123
}
2224

2325
@Test func ignoresLegacyTalkFieldsWhenNormalizedPayloadMissing() {
@@ -26,7 +28,8 @@ import Testing
2628
"apiKey": "legacy-key", // pragma: allowlist secret
2729
]
2830

29-
let selection = TalkModeManager.selectTalkProviderConfig(talk)
31+
let selection = TalkModeManager.selectTalkProviderConfig(
32+
TalkConfigParsing.bridgeFoundationDictionary(talk))
3033
#expect(selection == nil)
3134
}
3235

@@ -53,7 +56,7 @@ import Testing
5356
"silenceTimeoutMs": 1500,
5457
]
5558

56-
#expect(TalkModeManager.resolvedSilenceTimeoutMs(talk) == 1500)
59+
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == 1500)
5760
}
5861

5962
@Test func defaultsSilenceTimeoutMsWhenMissing() {
@@ -65,14 +68,14 @@ import Testing
6568
"silenceTimeoutMs": 0,
6669
]
6770

68-
#expect(TalkModeManager.resolvedSilenceTimeoutMs(talk) == 900)
71+
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == 900)
6972
}
7073

7174
@Test func defaultsSilenceTimeoutMsWhenBool() {
7275
let talk: [String: Any] = [
7376
"silenceTimeoutMs": true,
7477
]
7578

76-
#expect(TalkModeManager.resolvedSilenceTimeoutMs(talk) == 900)
79+
#expect(TalkModeManager.resolvedSilenceTimeoutMs(TalkConfigParsing.bridgeFoundationDictionary(talk)) == 900)
7780
}
7881
}

apps/macos/Sources/OpenClaw/AnyCodable+Helpers.swift

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,3 @@ import OpenClawKit
44
// Prefer the OpenClawKit wrapper to keep gateway request payloads consistent.
55
typealias AnyCodable = OpenClawKit.AnyCodable
66
typealias InstanceIdentity = OpenClawKit.InstanceIdentity
7-
8-
extension AnyCodable {
9-
var stringValue: String? {
10-
self.value as? String
11-
}
12-
13-
var boolValue: Bool? {
14-
self.value as? Bool
15-
}
16-
17-
var intValue: Int? {
18-
self.value as? Int
19-
}
20-
21-
var doubleValue: Double? {
22-
self.value as? Double
23-
}
24-
25-
var dictionaryValue: [String: AnyCodable]? {
26-
self.value as? [String: AnyCodable]
27-
}
28-
29-
var arrayValue: [AnyCodable]? {
30-
self.value as? [AnyCodable]
31-
}
32-
33-
var foundationValue: Any {
34-
switch self.value {
35-
case let dict as [String: AnyCodable]:
36-
dict.mapValues { $0.foundationValue }
37-
case let array as [AnyCodable]:
38-
array.map(\.foundationValue)
39-
default:
40-
self.value
41-
}
42-
}
43-
}

apps/macos/Sources/OpenClaw/TalkModeRuntime.swift

Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ actor TalkModeRuntime {
6767
private var fallbackVoiceId: String?
6868
private var lastPlaybackWasPCM: Bool = false
6969

70-
private var silenceWindow: TimeInterval = TimeInterval(TalkModeRuntime.defaultSilenceTimeoutMs) / 1000
70+
private var silenceWindow: TimeInterval = .init(TalkModeRuntime.defaultSilenceTimeoutMs) / 1000
7171
private let minSpeechRMS: Double = 1e-3
7272
private let speechBoostFactor: Double = 6.0
7373

@@ -808,95 +808,14 @@ extension TalkModeRuntime {
808808
let apiKey: String?
809809
}
810810

811-
struct TalkProviderConfigSelection {
812-
let provider: String
813-
let config: [String: AnyCodable]
814-
let normalizedPayload: Bool
815-
}
816-
817-
private static func normalizedTalkProviderID(_ raw: String?) -> String? {
818-
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
819-
return trimmed.isEmpty ? nil : trimmed
820-
}
821-
822-
private static func normalizedTalkProviderConfig(_ value: AnyCodable) -> [String: AnyCodable]? {
823-
if let typed = value.value as? [String: AnyCodable] {
824-
return typed
825-
}
826-
if let foundation = value.value as? [String: Any] {
827-
return foundation.mapValues(AnyCodable.init)
828-
}
829-
if let nsDict = value.value as? NSDictionary {
830-
var converted: [String: AnyCodable] = [:]
831-
for case let (key as String, raw) in nsDict {
832-
converted[key] = AnyCodable(raw)
833-
}
834-
return converted
835-
}
836-
return nil
837-
}
838-
839-
private static func normalizedTalkProviders(_ raw: AnyCodable?) -> [String: [String: AnyCodable]] {
840-
guard let raw else { return [:] }
841-
var providerMap: [String: AnyCodable] = [:]
842-
if let typed = raw.value as? [String: AnyCodable] {
843-
providerMap = typed
844-
} else if let foundation = raw.value as? [String: Any] {
845-
providerMap = foundation.mapValues(AnyCodable.init)
846-
} else if let nsDict = raw.value as? NSDictionary {
847-
for case let (key as String, value) in nsDict {
848-
providerMap[key] = AnyCodable(value)
849-
}
850-
} else {
851-
return [:]
852-
}
853-
854-
return providerMap.reduce(into: [String: [String: AnyCodable]]()) { acc, entry in
855-
guard
856-
let providerID = Self.normalizedTalkProviderID(entry.key),
857-
let providerConfig = Self.normalizedTalkProviderConfig(entry.value)
858-
else { return }
859-
acc[providerID] = providerConfig
860-
}
861-
}
862-
863811
static func selectTalkProviderConfig(
864812
_ talk: [String: AnyCodable]?) -> TalkProviderConfigSelection?
865813
{
866-
guard let talk else { return nil }
867-
let rawProvider = talk["provider"]?.stringValue
868-
let rawProviders = talk["providers"]
869-
let hasNormalizedPayload = rawProvider != nil || rawProviders != nil
870-
if hasNormalizedPayload {
871-
let normalizedProviders = Self.normalizedTalkProviders(rawProviders)
872-
let providerID =
873-
Self.normalizedTalkProviderID(rawProvider) ??
874-
normalizedProviders.keys.min() ??
875-
Self.defaultTalkProvider
876-
return TalkProviderConfigSelection(
877-
provider: providerID,
878-
config: normalizedProviders[providerID] ?? [:],
879-
normalizedPayload: true)
880-
}
881-
return TalkProviderConfigSelection(
882-
provider: Self.defaultTalkProvider,
883-
config: talk,
884-
normalizedPayload: false)
814+
TalkConfigParsing.selectProviderConfig(talk, defaultProvider: self.defaultTalkProvider)
885815
}
886816

887817
static func resolvedSilenceTimeoutMs(_ talk: [String: AnyCodable]?) -> Int {
888-
if let timeout = talk?["silenceTimeoutMs"]?.intValue, timeout > 0 {
889-
return timeout
890-
}
891-
if
892-
let timeout = talk?["silenceTimeoutMs"]?.doubleValue,
893-
timeout > 0,
894-
timeout.rounded(.towardZero) == timeout,
895-
timeout <= Double(Int.max)
896-
{
897-
return Int(timeout)
898-
}
899-
return Self.defaultSilenceTimeoutMs
818+
TalkConfigParsing.resolvedSilenceTimeoutMs(talk, fallback: self.defaultSilenceTimeoutMs)
900819
}
901820

902821
private func fetchTalkConfig() async -> TalkRuntimeConfig {

0 commit comments

Comments
 (0)