diff --git a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift index 9ef7b010fa80f..38ab4861e432e 100644 --- a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift +++ b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift @@ -442,7 +442,7 @@ actor TalkModeRuntime { if let apiKey = input.apiKey, !apiKey.isEmpty, let voiceId = input.voiceId { try await self.playElevenLabs(input: input, apiKey: apiKey, voiceId: voiceId) } else { - try await self.playSystemVoice(input: input) + try await self.playGatewayTTS(input: input) } } catch { self.ttsLogger @@ -643,6 +643,47 @@ actor TalkModeRuntime { return await self.playMP3(stream: stream) } + private func playGatewayTTS(input: TalkPlaybackInput) async throws { + self.ttsLogger.info("talk gateway TTS start chars=\(input.cleanedText.count, privacy: .public)") + + let params: [String: AnyCodable] = ["text": AnyCodable(input.cleanedText)] + let data = try await GatewayConnection.shared.request( + method: "talk.speak", + params: params, + timeoutMs: Double(input.synthTimeoutSeconds * 1000)) + + struct TalkSpeakResponse: Decodable { + let audioBase64: String? + let provider: String? + let mimeType: String? + } + let response = try JSONDecoder().decode(TalkSpeakResponse.self, from: data) + guard let base64 = response.audioBase64, !base64.isEmpty, + let audioData = Data(base64Encoded: base64) + else { + throw NSError( + domain: "TalkGatewayTTS", code: 1, + userInfo: [NSLocalizedDescriptionKey: "gateway returned no audio"]) + } + + let provider = response.provider ?? "edge" + self.ttsLogger.info( + "talk gateway TTS received \(audioData.count, privacy: .public) bytes " + + "provider=\(provider, privacy: .public)") + + guard self.isCurrent(input.generation) else { return } + + if self.interruptOnSpeech { + guard await self.prepareForPlayback(generation: input.generation) else { return } + } + await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } + self.phase = .speaking + + let result = await TalkAudioPlayer.shared.play(data: audioData) + self.ttsLogger.info( + "talk gateway TTS done finished=\(result.finished, privacy: .public)") + } + private func playSystemVoice(input: TalkPlaybackInput) async throws { self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)") if self.interruptOnSpeech { @@ -800,8 +841,8 @@ extension TalkModeRuntime { do { let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded( - method: .talkConfig, - params: ["includeSecrets": AnyCodable(true)], + method: .configGet, + params: nil, timeoutMs: 8000) let talk = snap.config?["talk"]?.dictionaryValue let ui = snap.config?["ui"]?.dictionaryValue