1#![allow(clippy::wildcard_imports)]
3
4use super::*;
5
6pub const QWEN_DEFAULT_MODEL: &str = "qwen-audio-3.0-tts-flash";
7pub const QWEN_DEFAULT_VOICE: &str = "longanhuan_v3.6";
8
9#[derive(Debug, Clone, Default)]
11pub struct CloudConfig {
12 pub synth_url: String,
13 pub auth_header: String,
14 pub auth_prefix: String,
15 pub voice_param: String,
16 pub model_param: Option<String>,
17 pub model_default: Option<String>,
18 pub default_voice: Option<String>,
19 pub text_field: String,
20 pub extra_body: HashMap<String, serde_json::Value>,
21 pub body_is_ssml: bool,
23 pub content_type: Option<String>,
25 pub extra_headers: HashMap<String, String>,
27 pub voices_url: Option<String>,
29 pub provider_id: String,
31 pub response_is_pcm: bool,
36}
37
38#[allow(clippy::too_many_lines)]
39pub fn build_config(id: &str, creds: &HashMap<String, String>) -> Option<CloudConfig> {
40 match id {
41 "openai" => Some(CloudConfig {
42 synth_url: "https://api.openai.com/v1/audio/speech".into(),
43 auth_header: "Authorization".into(),
44 auth_prefix: "Bearer ".into(),
45 voice_param: "voice".into(),
46 model_param: Some("model".into()),
47 model_default: Some("gpt-4o-mini-tts".into()),
48 default_voice: Some("alloy".into()),
49 text_field: "input".into(),
50 provider_id: "openai".into(),
51 ..Default::default()
52 }),
53 "elevenlabs" => {
54 let voice_id = creds
55 .get("voiceId")
56 .cloned()
57 .unwrap_or_else(|| "21m00Tcm4TlvDq8ikWAM".into());
58 let model = creds
66 .get("modelId")
67 .filter(|m| !m.is_empty())
68 .cloned()
69 .unwrap_or_else(|| "eleven_v4".into());
70 Some(CloudConfig {
71 synth_url: format!("https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"),
72 auth_header: "xi-api-key".into(),
73 model_param: Some("model_id".into()),
74 model_default: Some(model),
75 text_field: "text".into(),
76 voices_url: Some("https://api.elevenlabs.io/v1/voices".into()),
77 provider_id: "elevenlabs".into(),
78 ..Default::default()
79 })
80 }
81 "azure" => {
82 let region = creds
83 .get("region")
84 .cloned()
85 .unwrap_or_else(|| "eastus".into());
86 let mut extra = HashMap::new();
87 extra.insert(
88 "X-Microsoft-OutputFormat".into(),
89 "raw-24khz-16bit-mono-pcm".into(),
93 );
94 extra.insert("User-Agent".into(), "rust-tts-wrapper".into());
95 Some(CloudConfig {
96 synth_url: format!(
97 "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
98 ),
99 auth_header: "Ocp-Apim-Subscription-Key".into(),
100 default_voice: Some("en-US-AriaNeural".into()),
101 body_is_ssml: true,
102 content_type: Some("application/ssml+xml".into()),
103 extra_headers: extra,
104 voices_url: Some(format!(
105 "https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
106 )),
107 provider_id: "azure".into(),
108 response_is_pcm: true,
110 ..Default::default()
111 })
112 }
113 "google" => {
114 let api_key = creds.get("apiKey").cloned().unwrap_or_default();
115 Some(CloudConfig {
116 synth_url: format!(
117 "https://texttospeech.googleapis.com/v1/text:synthesize?key={api_key}"
118 ),
119 text_field: "text".into(),
120 voices_url: Some(format!(
121 "https://texttospeech.googleapis.com/v1/voices?key={api_key}"
122 )),
123 provider_id: "google".into(),
124 ..Default::default()
125 })
126 }
127 "gemini" => {
128 let model = creds
136 .get("modelId")
137 .filter(|m| !m.is_empty())
138 .cloned()
139 .unwrap_or_else(|| "gemini-3.8-flash-tts".into());
140 let voice = creds
141 .get("voice")
142 .filter(|v| !v.is_empty())
143 .cloned()
144 .unwrap_or_else(|| "Kore".into());
145 Some(CloudConfig {
146 synth_url: "https://generativelanguage.googleapis.com/v1beta/interactions".into(),
147 auth_header: "x-goog-api-key".into(),
148 auth_prefix: String::new(),
149 model_default: Some(model),
150 default_voice: Some(voice),
151 voices_url: Some("https://generativelanguage.googleapis.com/v1beta/voices".into()),
152 provider_id: "gemini".into(),
153 ..Default::default()
154 })
155 }
156 "cartesia" => Some(CloudConfig {
157 synth_url: "https://api.cartesia.ai/tts/bytes".into(),
158 auth_header: "X-API-Key".into(),
159 voice_param: "voice_id".into(),
160 model_param: Some("model_id".into()),
161 model_default: Some("sonic-3.6".into()),
162 text_field: "text".into(),
163 voices_url: Some("https://api.cartesia.ai/voices".into()),
164 provider_id: "cartesia".into(),
165 response_is_pcm: true,
167 ..Default::default()
168 }),
169 "deepgram" => Some(CloudConfig {
170 synth_url: "https://api.deepgram.com/v1/speak".into(),
171 auth_header: "Authorization".into(),
172 auth_prefix: "Token ".into(),
173 voice_param: "model".into(), default_voice: Some("aura-asteria-en".into()),
175 text_field: "text".into(),
176 voices_url: Some("https://api.deepgram.com/v1/voices".into()),
177 provider_id: "deepgram".into(),
178 ..Default::default()
179 }),
180 "playht" => {
181 let user_id = creds.get("userId").cloned().unwrap_or_default();
182 let mut extra_headers = HashMap::new();
183 extra_headers.insert("X-User-ID".into(), user_id);
184 Some(CloudConfig {
185 synth_url: "https://api.play.ht/api/v2/tts".into(),
186 auth_header: "Authorization".into(),
187 auth_prefix: "Bearer ".into(),
188 voice_param: "voice".into(),
189 text_field: "text".into(),
190 voices_url: Some("https://api.play.ht/api/v2/voices".into()),
191 extra_headers,
192 provider_id: "playht".into(),
193 ..Default::default()
194 })
195 }
196 "fishaudio" => Some(CloudConfig {
197 synth_url: "https://api.fish.audio/v1/tts".into(),
198 auth_header: "Authorization".into(),
199 auth_prefix: "Bearer ".into(),
200 voice_param: "reference_id".into(),
201 text_field: "text".into(),
202 voices_url: Some("https://api.fish.audio/v1/model".into()),
203 provider_id: "fishaudio".into(),
204 ..Default::default()
205 }),
206 "hume" => {
207 let voice_name = creds.get("voice").cloned().unwrap_or_default();
209 let mut extra_body = HashMap::new();
210 extra_body.insert("voice".into(), serde_json::json!({"name": voice_name}));
211 extra_body.insert(
212 "audio_format".into(),
213 serde_json::Value::String("wav".into()),
214 );
215
216 Some(CloudConfig {
217 synth_url: "https://api.hume.ai/v0/tts".into(),
218 auth_header: "Authorization".into(),
219 auth_prefix: "Bearer ".into(),
220 voice_param: String::new(), text_field: "text".into(),
222 extra_body,
223 provider_id: "hume".into(),
224 ..Default::default()
225 })
226 }
227 "mistral" => Some(CloudConfig {
228 synth_url: "https://api.mistral.ai/v1/tts".into(),
229 auth_header: "Authorization".into(),
230 auth_prefix: "Bearer ".into(),
231 voice_param: "voice".into(),
232 text_field: "text".into(),
233 provider_id: "mistral".into(),
234 ..Default::default()
235 }),
236 "murf" => Some(CloudConfig {
237 synth_url: "https://api.murf.ai/v1/speech/generate".into(),
238 auth_header: "api-key".into(),
239 voice_param: "voice_id".into(),
240 text_field: "text".into(),
241 provider_id: "murf".into(),
242 ..Default::default()
243 }),
244 "resemble" => Some(CloudConfig {
245 synth_url: "https://app.resemble.ai/api/v2/synthesize".into(),
246 auth_header: "Authorization".into(),
247 auth_prefix: "Token ".into(),
248 voice_param: "voice_uuid".into(),
249 text_field: "text".into(),
250 voices_url: Some("https://app.resemble.ai/api/v2/voices".into()),
251 provider_id: "resemble".into(),
252 ..Default::default()
253 }),
254 "unrealspeech" => Some(CloudConfig {
255 synth_url: "https://api.v7.unrealspeech.com/speech".into(),
256 auth_header: "Authorization".into(),
257 auth_prefix: "Bearer ".into(),
258 voice_param: "voice_id".into(),
259 default_voice: Some("Scarlett".into()),
260 text_field: "text".into(),
261 provider_id: "unrealspeech".into(),
262 ..Default::default()
263 }),
264 "upliftai" => Some(CloudConfig {
265 synth_url: "https://api.upliftai.org/v1/tts".into(),
266 auth_header: "Authorization".into(),
267 auth_prefix: "Bearer ".into(),
268 voice_param: "voice".into(),
269 text_field: "text".into(),
270 provider_id: "upliftai".into(),
271 ..Default::default()
272 }),
273 "watson" => {
274 let region = creds
275 .get("region")
276 .cloned()
277 .unwrap_or_else(|| "us-east".into());
278 let instance_id = creds.get("instanceId").cloned().unwrap_or_default();
279 Some(CloudConfig {
280 synth_url: format!(
281 "https://{region}.text-to-speech.watson.cloud.ibm.com/instances/{instance_id}/v1/synthesize"
282 ),
283 auth_header: "Authorization".into(),
284 auth_prefix: format!(
285 "Basic {}",
286 base64_encode(&format!("apikey:{}", creds.get("apiKey").cloned().unwrap_or_default()))
291 ),
292 voice_param: "voice".into(),
293 text_field: "text".into(),
294 voices_url: Some(format!(
295 "https://{region}.text-to-speech.watson.cloud.ibm.com/instances/{instance_id}/v1/voices"
296 )),
297 provider_id: "watson".into(),
298 ..Default::default()
299 })
300 }
301 "witai" => Some(CloudConfig {
302 synth_url: "https://api.wit.ai/synthesize?v=20240304".into(),
303 auth_header: "Authorization".into(),
304 auth_prefix: "Bearer ".into(),
305 voices_url: Some("https://api.wit.ai/voices?v=20240304".into()),
306 provider_id: "witai".into(),
307 ..Default::default()
308 }),
309 "xai" => Some(CloudConfig {
310 synth_url: "https://api.x.ai/v1/audio/speech".into(),
311 auth_header: "Authorization".into(),
312 auth_prefix: "Bearer ".into(),
313 voice_param: "voice".into(),
314 text_field: "input".into(),
315 provider_id: "xai".into(),
316 ..Default::default()
317 }),
318 "modelslab" => Some(CloudConfig {
319 synth_url: "https://modelslab.com/api/v1/text_to_speech".into(),
320 voice_param: "voice".into(),
321 text_field: "text".into(),
322 provider_id: "modelslab".into(),
323 ..Default::default()
324 }),
325 "polly" => {
326 let region = creds
329 .get("region")
330 .cloned()
331 .unwrap_or_else(|| "us-east-1".into());
332 let mut extra_body = HashMap::new();
333 extra_body.insert(
336 "OutputFormat".to_string(),
337 serde_json::Value::String("mp3".into()),
338 );
339 extra_body.insert(
340 "LanguageCode".to_string(),
341 serde_json::Value::String("en-US".into()),
342 );
343 Some(CloudConfig {
344 synth_url: format!("https://polly.{region}.amazonaws.com/v1/speech"),
347 voice_param: "VoiceId".into(),
348 model_param: Some("Engine".into()),
349 model_default: Some("neural".into()),
350 default_voice: Some("Joanna".into()),
351 text_field: "Text".into(),
352 extra_body,
353 voices_url: Some(format!("https://polly.{region}.amazonaws.com/v1/voices")),
354 provider_id: "polly".into(),
355 ..Default::default()
356 })
357 }
358 "qwen" => {
366 let model = creds
367 .get("modelId")
368 .filter(|m| !m.is_empty())
369 .cloned()
370 .unwrap_or_else(|| QWEN_DEFAULT_MODEL.into());
371 Some(CloudConfig {
372 default_voice: Some(QWEN_DEFAULT_VOICE.into()),
373 model_default: Some(model),
374 provider_id: "qwen".into(),
375 response_is_pcm: true,
378 ..Default::default()
379 })
380 }
381 "edge" => Some(CloudConfig {
388 default_voice: Some(EDGE_DEFAULT_VOICE.into()),
389 voices_url: Some(EDGE_VOICE_LIST_URL.into()),
390 provider_id: "edge".into(),
391 response_is_pcm: false,
392 ..Default::default()
393 }),
394 _ => None,
395 }
396}
397
398pub fn effective_model(config: &CloudConfig) -> Option<&str> {
402 config
403 .extra_body
404 .get("model_id")
405 .and_then(|v| v.as_str())
406 .or(config.model_default.as_deref())
407}