-
Notifications
You must be signed in to change notification settings - Fork 0
Talk: add gateway TTS fallback when ElevenLabs is not configured #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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)") | ||||||||||||||||||
|
||||||||||||||||||
| "talk gateway TTS done finished=\(result.finished, privacy: .public)") | |
| "talk gateway TTS done finished=\(result.finished, privacy: .public)") | |
| guard result.finished else { | |
| guard self.isCurrent(input.generation) else { return } | |
| throw NSError( | |
| domain: "TalkGatewayTTS", code: 2, | |
| userInfo: [NSLocalizedDescriptionKey: "gateway audio playback did not finish"]) | |
| } |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switching from talk.config (with includeSecrets) to config.get may change authorization/shape/secret-redaction semantics. In particular, talk.config is already used elsewhere to retrieve talk.apiKey when includeSecrets is true (e.g. apps/ios/Sources/Voice/TalkModeManager.swift:1674), but config.get has no includeSecrets parameter in the protocol model. If config.get returns redacted secrets or is restricted in some modes, this can break ElevenLabs configuration loading. Consider keeping talk.config for Talk mode config (and only fetching additional UI keys via config.get if needed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fallback now routes through gateway TTS, but some of the surrounding logging still says it is “falling back to system voice” when the ElevenLabs API key/voiceId is missing. Updating those warnings to mention gateway TTS (with system voice as the final fallback) will make diagnostics match runtime behavior.