Skip to content

Commit 4cb1bee

Browse files
authored
Force Sloane for floating bar voice answers (#6267)
## Summary - force floating bar replies onto the Sloane ElevenLabs voice instead of honoring stale voice-id overrides - fetch ElevenLabs from the desktop backend config key path so production can use ElevenLabs without relying on a locally seeded dev key - clear deprecated saved voice-id overrides from settings sync and remove the user-facing voice-id field ## Testing - swift build -c debug --package-path desktop/Desktop - cargo check
2 parents caec0d2 + 2c3d937 commit 4cb1bee

9 files changed

Lines changed: 51 additions & 55 deletions

File tree

desktop/Backend-Rust/charts/desktop-backend/dev_values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ env:
3232
secretKeyRef:
3333
name: dev-omi-backend-secrets
3434
key: GEMINI_API_KEY
35+
- name: ELEVENLABS_API_KEY
36+
valueFrom:
37+
secretKeyRef:
38+
name: dev-omi-backend-secrets
39+
key: ELEVENLABS_API_KEY
3540
- name: RESEND_API_KEY
3641
valueFrom:
3742
secretKeyRef:

desktop/Backend-Rust/charts/desktop-backend/prod_values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ env:
3434
secretKeyRef:
3535
name: prod-omi-backend-secrets
3636
key: DEEPGRAM_API_KEY
37+
- name: ELEVENLABS_API_KEY
38+
valueFrom:
39+
secretKeyRef:
40+
name: prod-omi-backend-secrets
41+
key: ELEVENLABS_API_KEY
3742
- name: ENCRYPTION_SECRET
3843
valueFrom:
3944
secretKeyRef:

desktop/Backend-Rust/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ pub struct Config {
6969
pub deepgram_api_key: Option<String>,
7070
/// Anthropic API key for chat (served to desktop clients)
7171
pub anthropic_api_key: Option<String>,
72+
/// ElevenLabs API key for floating bar voice answers (served to desktop clients)
73+
pub elevenlabs_api_key: Option<String>,
7274
/// Google Calendar API key (served to desktop clients)
7375
pub google_calendar_api_key: Option<String>,
7476
}
@@ -130,6 +132,7 @@ impl Config {
130132
agent_gcs_bucket: env::var("AGENT_GCS_BUCKET").ok(),
131133
deepgram_api_key: env::var("DEEPGRAM_API_KEY").ok(),
132134
anthropic_api_key: env::var("ANTHROPIC_API_KEY").ok(),
135+
elevenlabs_api_key: env::var("ELEVENLABS_API_KEY").ok(),
133136
google_calendar_api_key: env::var("GOOGLE_CALENDAR_API_KEY").ok(),
134137
}
135138
}

desktop/Backend-Rust/src/routes/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ struct ApiKeysResponse {
1515
#[serde(skip_serializing_if = "Option::is_none")]
1616
anthropic_api_key: Option<String>,
1717
#[serde(skip_serializing_if = "Option::is_none")]
18+
elevenlabs_api_key: Option<String>,
19+
#[serde(skip_serializing_if = "Option::is_none")]
1820
firebase_api_key: Option<String>,
1921
#[serde(skip_serializing_if = "Option::is_none")]
2022
google_calendar_api_key: Option<String>,
@@ -25,6 +27,7 @@ struct ApiKeysResponse {
2527
async fn get_api_keys(State(state): State<AppState>, _user: AuthUser) -> Json<ApiKeysResponse> {
2628
Json(ApiKeysResponse {
2729
anthropic_api_key: state.config.anthropic_api_key.clone(),
30+
elevenlabs_api_key: state.config.elevenlabs_api_key.clone(),
2831
firebase_api_key: state.config.firebase_api_key.clone(),
2932
google_calendar_api_key: state.config.google_calendar_api_key.clone(),
3033
})

desktop/Desktop/Sources/APIClient.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4629,13 +4629,15 @@ extension APIClient {
46294629
let deepgramApiKey: String?
46304630
let geminiApiKey: String?
46314631
let anthropicApiKey: String?
4632+
let elevenLabsApiKey: String?
46324633
let firebaseApiKey: String?
46334634
let googleCalendarApiKey: String?
46344635

46354636
enum CodingKeys: String, CodingKey {
46364637
case deepgramApiKey = "deepgram_api_key"
46374638
case geminiApiKey = "gemini_api_key"
46384639
case anthropicApiKey = "anthropic_api_key"
4640+
case elevenLabsApiKey = "elevenlabs_api_key"
46394641
case firebaseApiKey = "firebase_api_key"
46404642
case googleCalendarApiKey = "google_calendar_api_key"
46414643
}

desktop/Desktop/Sources/APIKeyService.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import Foundation
44
/// Developer overrides (set in Settings) take precedence over backend-provided keys.
55
///
66
/// NOTE: Deepgram and Gemini keys are NO LONGER fetched from the backend —
7-
/// they are proxied server-side (issue #5861). Only Anthropic, Firebase, and
8-
/// Calendar keys are still served via /v1/config/api-keys.
7+
/// they are proxied server-side (issue #5861). Anthropic, ElevenLabs, Firebase,
8+
/// and Calendar keys are still served via /v1/config/api-keys.
99
@MainActor
1010
final class APIKeyService: ObservableObject {
1111
static let shared = APIKeyService()
@@ -14,6 +14,7 @@ final class APIKeyService: ObservableObject {
1414
@Published private(set) var deepgramApiKey: String?
1515
@Published private(set) var geminiApiKey: String?
1616
@Published private(set) var anthropicApiKey: String?
17+
@Published private(set) var elevenLabsApiKey: String?
1718
@Published private(set) var firebaseApiKey: String?
1819
@Published private(set) var googleCalendarApiKey: String?
1920
@Published private(set) var isLoaded: Bool = false
@@ -51,6 +52,10 @@ final class APIKeyService: ObservableObject {
5152
nonEmpty(UserDefaults.standard.string(forKey: "dev_anthropic_api_key")) ?? anthropicApiKey
5253
}
5354

55+
var effectiveElevenLabsKey: String? {
56+
nonEmpty(UserDefaults.standard.string(forKey: "dev_elevenlabs_api_key")) ?? elevenLabsApiKey
57+
}
58+
5459
var effectiveFirebaseApiKey: String? {
5560
firebaseApiKey
5661
}
@@ -70,14 +75,15 @@ final class APIKeyService: ObservableObject {
7075
self.deepgramApiKey = keys.deepgramApiKey
7176
self.geminiApiKey = keys.geminiApiKey
7277
self.anthropicApiKey = keys.anthropicApiKey
78+
self.elevenLabsApiKey = keys.elevenLabsApiKey
7379
self.firebaseApiKey = keys.firebaseApiKey
7480
self.googleCalendarApiKey = keys.googleCalendarApiKey
7581
self.isLoaded = true
7682

7783
// Set env vars so existing getenv() consumers keep working during transition
7884
applyToEnvironment()
7985

80-
log("APIKeyService: Fetched keys from backend (deepgram=\(keys.deepgramApiKey != nil), gemini=\(keys.geminiApiKey != nil), anthropic=\(keys.anthropicApiKey != nil), firebase=\(keys.firebaseApiKey != nil), calendar=\(keys.googleCalendarApiKey != nil))")
86+
log("APIKeyService: Fetched keys from backend (deepgram=\(keys.deepgramApiKey != nil), gemini=\(keys.geminiApiKey != nil), anthropic=\(keys.anthropicApiKey != nil), elevenlabs=\(keys.elevenLabsApiKey != nil), firebase=\(keys.firebaseApiKey != nil), calendar=\(keys.googleCalendarApiKey != nil))")
8187
return
8288
} catch {
8389
let delay = pow(2.0, Double(attempt - 1))
@@ -100,6 +106,7 @@ final class APIKeyService: ObservableObject {
100106
deepgramApiKey = nil
101107
geminiApiKey = nil
102108
anthropicApiKey = nil
109+
elevenLabsApiKey = nil
103110
firebaseApiKey = nil
104111
googleCalendarApiKey = nil
105112
isLoaded = false
@@ -108,6 +115,7 @@ final class APIKeyService: ObservableObject {
108115
unsetenv("DEEPGRAM_API_KEY")
109116
unsetenv("GEMINI_API_KEY")
110117
unsetenv("ANTHROPIC_API_KEY")
118+
unsetenv("ELEVENLABS_API_KEY")
111119
// NOTE: Do NOT unset FIREBASE_API_KEY — it's needed for the next sign-in
112120
// (auth bootstrap requires Firebase key before backend is reachable)
113121
unsetenv("GOOGLE_CALENDAR_API_KEY")
@@ -124,6 +132,9 @@ final class APIKeyService: ObservableObject {
124132
if let key = effectiveAnthropicKey {
125133
setenv("ANTHROPIC_API_KEY", key, 1)
126134
}
135+
if let key = effectiveElevenLabsKey {
136+
setenv("ELEVENLABS_API_KEY", key, 1)
137+
}
127138
if let key = effectiveFirebaseApiKey {
128139
setenv("FIREBASE_API_KEY", key, 1)
129140
}
@@ -156,6 +167,11 @@ final class APIKeyService: ObservableObject {
156167
?? (getenv("ANTHROPIC_API_KEY").flatMap { String(validatingUTF8: $0) })
157168
}
158169

170+
nonisolated static var currentElevenLabsKey: String? {
171+
nonEmptyStatic(UserDefaults.standard.string(forKey: "dev_elevenlabs_api_key"))
172+
?? (getenv("ELEVENLABS_API_KEY").flatMap { String(validatingUTF8: $0) })
173+
}
174+
159175
/// True when the app has enough configuration to start transcription and screen analysis.
160176
/// In proxy mode (OMI_API_URL set), no client-side Deepgram/Gemini keys are needed.
161177
nonisolated static var keysAvailable: Bool {

desktop/Desktop/Sources/FloatingControlBar/FloatingBarVoicePlaybackService.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,15 @@ final class FloatingBarVoicePlaybackService: NSObject, AVAudioPlayerDelegate {
6363
}
6464

6565
private func resolvePlaybackMode() -> PlaybackMode {
66-
let defaults = UserDefaults.standard
6766
guard
68-
let apiKey = defaults.string(forKey: Self.devAPIKeyDefaultsKey)?.trimmingCharacters(
67+
let apiKey = APIKeyService.currentElevenLabsKey?.trimmingCharacters(
6968
in: .whitespacesAndNewlines),
7069
!apiKey.isEmpty
7170
else {
7271
return .systemFallback
7372
}
7473

75-
let voiceID = defaults.string(forKey: Self.devVoiceIDDefaultsKey)?
76-
.trimmingCharacters(in: .whitespacesAndNewlines)
77-
let resolvedVoiceID = (voiceID?.isEmpty == false) ? voiceID! : Self.defaultVoiceID
78-
return .elevenLabs(apiKey: apiKey, voiceID: resolvedVoiceID)
74+
return .elevenLabs(apiKey: apiKey, voiceID: Self.defaultVoiceID)
7975
}
8076

8177
private func drainBufferedText(isFinal: Bool, mode: PlaybackMode) {

desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ struct SettingsContentView: View {
314314
@AppStorage("dev_gemini_api_key") private var devGeminiKey: String = ""
315315
@AppStorage("dev_anthropic_api_key") private var devAnthropicKey: String = ""
316316
@AppStorage("dev_elevenlabs_api_key") private var devElevenLabsKey: String = ""
317-
@AppStorage("dev_elevenlabs_voice_id") private var devElevenLabsVoiceID: String = ""
318317

319318
init(
320319
appState: AppState,
@@ -4294,7 +4293,7 @@ struct SettingsContentView: View {
42944293
.scaledFont(size: 13)
42954294
.foregroundColor(OmiColors.textSecondary)
42964295
if devElevenLabsKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
4297-
Text("No ElevenLabs key saved yet, so the app will use the local Samantha voice.")
4296+
Text("No personal ElevenLabs key saved. The app will use Omi's default Sloane voice when available, otherwise it falls back to Samantha.")
42984297
.scaledFont(size: 12)
42994298
.foregroundColor(OmiColors.textTertiary)
43004299
}
@@ -4329,20 +4328,12 @@ struct SettingsContentView: View {
43294328

43304329
developerKeyField(
43314330
title: "ElevenLabs API Key",
4332-
subtitle: "For experimental floating-bar voice answers",
4331+
subtitle: "For experimental floating-bar voice answers with the Sloane voice",
43334332
settingId: "advanced.devkeys.elevenlabs",
43344333
value: syncedElevenLabsKeyBinding
43354334
)
43364335

4337-
developerTextField(
4338-
title: "ElevenLabs Voice ID",
4339-
subtitle: "Optional override. Leave blank to use the default Sloane voice.",
4340-
placeholder: "BAMYoBHLZM7lJgJAmFz0",
4341-
settingId: "advanced.devkeys.elevenlabsvoice",
4342-
value: syncedElevenLabsVoiceIDBinding
4343-
)
4344-
4345-
if !devDeepgramKey.isEmpty || !devGeminiKey.isEmpty || !devAnthropicKey.isEmpty || !devElevenLabsKey.isEmpty || !devElevenLabsVoiceID.isEmpty {
4336+
if !devDeepgramKey.isEmpty || !devGeminiKey.isEmpty || !devAnthropicKey.isEmpty || !devElevenLabsKey.isEmpty {
43464337
settingsCard(settingId: "advanced.devkeys.clear") {
43474338
HStack {
43484339
Spacer()
@@ -4351,7 +4342,6 @@ struct SettingsContentView: View {
43514342
devGeminiKey = ""
43524343
devAnthropicKey = ""
43534344
devElevenLabsKey = ""
4354-
devElevenLabsVoiceID = ""
43554345
SettingsSyncManager.shared.pushPartialUpdate(
43564346
AssistantSettingsResponse(
43574347
floatingBar: FloatingBarSettingsResponse(
@@ -4389,22 +4379,6 @@ struct SettingsContentView: View {
43894379
}
43904380
}
43914381

4392-
private func developerTextField(title: String, subtitle: String, placeholder: String, settingId: String, value: Binding<String>) -> some View {
4393-
settingsCard(settingId: settingId) {
4394-
VStack(alignment: .leading, spacing: 8) {
4395-
Text(title)
4396-
.scaledFont(size: 14, weight: .medium)
4397-
.foregroundColor(OmiColors.textPrimary)
4398-
Text(subtitle)
4399-
.scaledFont(size: 12)
4400-
.foregroundColor(OmiColors.textTertiary)
4401-
TextField(placeholder, text: value)
4402-
.textFieldStyle(.roundedBorder)
4403-
.scaledFont(size: 13)
4404-
}
4405-
}
4406-
}
4407-
44084382
private var floatingBarVoiceAnswersBinding: Binding<Bool> {
44094383
Binding(
44104384
get: { shortcutSettings.floatingBarVoiceAnswersEnabled },
@@ -4433,20 +4407,6 @@ struct SettingsContentView: View {
44334407
)
44344408
}
44354409

4436-
private var syncedElevenLabsVoiceIDBinding: Binding<String> {
4437-
Binding(
4438-
get: { devElevenLabsVoiceID },
4439-
set: { newValue in
4440-
devElevenLabsVoiceID = newValue
4441-
SettingsSyncManager.shared.pushPartialUpdate(
4442-
AssistantSettingsResponse(
4443-
floatingBar: FloatingBarSettingsResponse(elevenLabsVoiceID: newValue)
4444-
)
4445-
)
4446-
}
4447-
)
4448-
}
4449-
44504410
private func tierPickerRow(tier: Int, label: String, subtitle: String) -> some View {
44514411
let isSelected = currentTierLevel == tier
44524412
return Button(action: {

desktop/Desktop/Sources/ProactiveAssistants/Services/SettingsSyncManager.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,14 @@ class SettingsSyncManager {
9999
if let v = floatingBar.elevenLabsApiKey {
100100
UserDefaults.standard.set(v, forKey: FloatingBarVoicePlaybackService.devAPIKeyDefaultsKey)
101101
}
102-
if let v = floatingBar.elevenLabsVoiceID {
103-
UserDefaults.standard.set(v, forKey: FloatingBarVoicePlaybackService.devVoiceIDDefaultsKey)
102+
UserDefaults.standard.removeObject(forKey: FloatingBarVoicePlaybackService.devVoiceIDDefaultsKey)
103+
if let v = floatingBar.elevenLabsVoiceID?.trimmingCharacters(in: .whitespacesAndNewlines),
104+
!v.isEmpty {
105+
pushPartialUpdate(
106+
AssistantSettingsResponse(
107+
floatingBar: FloatingBarSettingsResponse(elevenLabsVoiceID: "")
108+
)
109+
)
104110
}
105111
}
106112

@@ -163,7 +169,7 @@ class SettingsSyncManager {
163169
let floatingBar = FloatingBarSettingsResponse(
164170
voiceAnswersEnabled: ShortcutSettings.shared.floatingBarVoiceAnswersEnabled,
165171
elevenLabsApiKey: UserDefaults.standard.string(forKey: FloatingBarVoicePlaybackService.devAPIKeyDefaultsKey) ?? "",
166-
elevenLabsVoiceID: UserDefaults.standard.string(forKey: FloatingBarVoicePlaybackService.devVoiceIDDefaultsKey) ?? ""
172+
elevenLabsVoiceID: ""
167173
)
168174

169175
return AssistantSettingsResponse(

0 commit comments

Comments
 (0)