1use std::collections::HashMap;
4use std::fmt;
5use std::os::raw::c_char;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Gender {
10 Male,
11 Female,
12 Unknown,
13}
14
15impl fmt::Display for Gender {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 Self::Male => write!(f, "Male"),
19 Self::Female => write!(f, "Female"),
20 Self::Unknown => write!(f, "Unknown"),
21 }
22 }
23}
24
25#[must_use]
27pub fn normalize_gender(value: &str) -> Gender {
28 match value.to_lowercase().as_str() {
29 "female" => Gender::Female,
30 "male" => Gender::Male,
31 _ => Gender::Unknown,
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct LanguageCode {
38 pub bcp47: String,
40 pub iso639_3: String,
42 pub display: String,
44}
45
46#[derive(Debug, Clone)]
49pub struct Voice {
50 pub id: String,
52 pub name: String,
54 pub gender: Gender,
56 pub provider: String,
58 pub language_codes: Vec<LanguageCode>,
60}
61
62impl Voice {
63 #[must_use]
65 pub fn primary_language(&self) -> &str {
66 self.language_codes.first().map_or("", |l| l.bcp47.as_str())
67 }
68}
69
70#[cfg(feature = "display_names")]
77#[must_use]
78pub fn locale_display_name(bcp47: &str) -> String {
79 use icu_displaynames::{DisplayNamesOptions, LocaleDisplayNamesFormatter};
80
81 let Ok(locale) = bcp47.parse::<icu_locid::Locale>() else {
83 return bcp47.to_string();
84 };
85
86 let Ok(formatter) = LocaleDisplayNamesFormatter::try_new(
88 &icu_locid::locale!("en").into(),
89 DisplayNamesOptions::default(),
90 ) else {
91 return bcp47.to_string();
92 };
93
94 let name = formatter.of(&locale);
95 if name.is_empty() {
96 bcp47.to_string()
97 } else {
98 name.into_owned()
99 }
100}
101
102#[cfg(not(feature = "display_names"))]
103#[must_use]
104pub fn locale_display_name(bcp47: &str) -> String {
105 bcp47.to_string()
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum AudioFormat {
111 Mp3,
112 Wav,
113 Ogg,
114 Opus,
115 Aac,
116 Flac,
117 Pcm,
118}
119
120impl fmt::Display for AudioFormat {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 match self {
123 Self::Mp3 => write!(f, "mp3"),
124 Self::Wav => write!(f, "wav"),
125 Self::Ogg => write!(f, "ogg"),
126 Self::Opus => write!(f, "opus"),
127 Self::Aac => write!(f, "aac"),
128 Self::Flac => write!(f, "flac"),
129 Self::Pcm => write!(f, "pcm"),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq)]
136pub enum SpeechRate {
137 XSlow,
138 Slow,
139 Medium,
140 Fast,
141 XFast,
142}
143
144impl SpeechRate {
145 #[must_use]
147 pub fn rate_value(self) -> f32 {
148 match self {
149 Self::XSlow => 0.5,
150 Self::Slow => 0.75,
151 Self::Medium => 1.0,
152 Self::Fast => 1.25,
153 Self::XFast => 1.5,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum SpeechPitch {
161 XLow,
162 Low,
163 Medium,
164 High,
165 XHigh,
166}
167
168impl SpeechPitch {
169 #[must_use]
171 pub fn pitch_value(self) -> f32 {
172 match self {
173 Self::XLow => 0.5,
174 Self::Low => 0.75,
175 Self::Medium => 1.0,
176 Self::High => 1.25,
177 Self::XHigh => 1.5,
178 }
179 }
180}
181
182#[derive(Debug, Clone, Default)]
184pub struct SpeakOptions {
185 pub rate: Option<f32>,
187 pub speech_rate: Option<SpeechRate>,
189 pub pitch: Option<f32>,
191 pub speech_pitch: Option<SpeechPitch>,
193 pub volume: Option<f32>,
195 pub voice: Option<String>,
197 pub format: Option<AudioFormat>,
199 pub use_speech_markdown: bool,
201 pub use_word_boundary: bool,
203 pub raw_ssml: bool,
205 pub extra: HashMap<String, String>,
207}
208
209impl SpeakOptions {
210 #[must_use]
212 pub fn effective_rate(&self) -> f32 {
213 self.rate
214 .or_else(|| self.speech_rate.map(SpeechRate::rate_value))
215 .unwrap_or(1.0)
216 }
217
218 #[must_use]
220 pub fn effective_pitch(&self) -> f32 {
221 self.pitch
222 .or_else(|| self.speech_pitch.map(SpeechPitch::pitch_value))
223 .unwrap_or(1.0)
224 }
225
226 #[must_use]
228 pub fn effective_volume(&self) -> f32 {
229 self.volume.unwrap_or(1.0)
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
236pub struct WordBoundary {
237 pub text: String,
239 pub offset: u64,
241 pub duration: u64,
243 pub estimated: bool,
247}
248
249#[derive(Debug, Clone)]
251pub struct EngineDescriptor {
252 pub id: String,
254 pub name: String,
256 pub needs_credentials: bool,
258 pub credential_keys_json: String,
260}
261
262#[derive(Debug, Clone)]
264pub struct SherpaModelInfo {
265 pub id: String,
267 pub model_type: String,
269 pub engines: String,
272 pub name: String,
274 pub language: Vec<SherpaLanguage>,
276 pub sample_rate: u32,
278 pub num_speakers: u32,
280 pub quality: String,
284 pub url: String,
286 pub compression: bool,
288 pub filesize_mb: f64,
290 pub license: String,
293 pub license_url: String,
295}
296
297#[derive(Debug, Clone)]
299pub struct SherpaLanguage {
300 pub lang_code: String,
302 pub language_name: String,
304 pub country: String,
306}
307
308#[repr(C)]
310pub struct tts_voice {
311 pub id: *mut c_char,
313 pub name: *mut c_char,
315 pub language: *mut c_char,
317 pub gender: *mut c_char,
319 pub engine: *mut c_char,
321}
322
323#[repr(C)]
325pub struct tts_engine_info {
326 pub id: *mut c_char,
328 pub name: *mut c_char,
330 pub needs_credentials: bool,
332 pub credential_keys_json: *mut c_char,
334}
335
336#[derive(Debug)]
338pub struct TtsError(pub String);
339
340impl fmt::Display for TtsError {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 write!(f, "{}", self.0)
343 }
344}
345
346impl std::error::Error for TtsError {}
347
348impl From<anyhow::Error> for TtsError {
349 fn from(e: anyhow::Error) -> Self {
350 TtsError(e.to_string())
351 }
352}
353
354pub type TtsResult<T> = Result<T, TtsError>;
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 #[cfg(feature = "display_names")]
363 fn test_locale_display_name_known() {
364 let name = locale_display_name("en-US");
365 assert!(
366 name.to_lowercase().contains("english"),
367 "expected 'English' in display name for en-US, got {name}"
368 );
369 }
370
371 #[test]
372 #[cfg(feature = "display_names")]
373 fn test_locale_display_name_non_latin() {
374 let name = locale_display_name("ar-EG");
375 assert!(
376 name.contains("Arabic") || name.contains("ar-EG"),
377 "expected Arabic in display name, got {name}"
378 );
379 }
380
381 #[test]
382 #[cfg(feature = "display_names")]
383 fn test_locale_display_name_unknown_falls_back() {
384 let name = locale_display_name("xx-YY");
385 assert!(!name.is_empty());
386 }
387
388 #[test]
389 #[cfg(not(feature = "display_names"))]
390 fn test_locale_display_name_fallback_is_raw_code() {
391 assert_eq!(locale_display_name("en-US"), "en-US");
392 }
393}