1pub mod chunk;
4#[cfg(all(feature = "playback", not(target_family = "wasm")))]
6mod playback;
7#[cfg(not(target_family = "wasm"))]
8mod session;
9#[cfg(all(feature = "playback", not(target_family = "wasm")))]
10mod speak;
11mod stream;
12
13use std::path::PathBuf;
14
15use crate::backend::{BackendError, ModelError};
16use crate::cancel::CancelToken;
17pub use chunk::chunks;
18#[cfg(all(feature = "playback", not(target_family = "wasm")))]
19pub use playback::SpeakerOutput;
20#[cfg(not(target_family = "wasm"))]
21pub use session::{Session, Synthesis};
22pub use stream::{SpeechStream, SpeechWriter, StreamClosed, TryRead, stream};
23
24pub mod prelude {
28 #[cfg(not(target_family = "wasm"))]
29 pub use super::{Session, Synthesis};
30 pub use super::{SpeakingRate, SpeechText, SynthesisError, Voice};
31}
32
33const MAX_SPEECH_TEXT_BYTES: usize = 8 * 1024;
34const MAX_VOICE_BYTES: usize = 64;
35
36const MIN_SPEAKING_RATE: f32 = 0.25;
37const MAX_SPEAKING_RATE: f32 = 4.0;
38
39pub trait Synthesizer: Send + Sync + 'static {
54 fn sample_rate(&self) -> u32;
56
57 fn synthesize_with(
60 &self,
61 text: &SpeechText,
62 voice: Option<&Voice>,
63 rate: SpeakingRate,
64 out: SpeechWriter,
65 cancel: &CancelToken,
66 ) -> Result<(), SynthesisError>;
67
68 fn plays_audio_itself(&self) -> bool {
72 false
73 }
74}
75
76pub trait Engine: Synthesizer + Sized {
79 const NAME: &'static str;
80
81 type Config;
82
83 fn check_from(config: &Self::Config) -> Result<PathBuf, SynthesisError>;
86
87 fn load_from(config: Self::Config) -> Result<Self, SynthesisError>;
88}
89
90impl<T: Synthesizer + ?Sized> Synthesizer for std::sync::Arc<T> {
92 fn sample_rate(&self) -> u32 {
93 (**self).sample_rate()
94 }
95
96 fn synthesize_with(
97 &self,
98 text: &SpeechText,
99 voice: Option<&Voice>,
100 rate: SpeakingRate,
101 out: SpeechWriter,
102 cancel: &CancelToken,
103 ) -> Result<(), SynthesisError> {
104 (**self).synthesize_with(text, voice, rate, out, cancel)
105 }
106}
107
108impl<T: Synthesizer + ?Sized> Synthesizer for Box<T> {
109 fn sample_rate(&self) -> u32 {
110 (**self).sample_rate()
111 }
112
113 fn synthesize_with(
114 &self,
115 text: &SpeechText,
116 voice: Option<&Voice>,
117 rate: SpeakingRate,
118 out: SpeechWriter,
119 cancel: &CancelToken,
120 ) -> Result<(), SynthesisError> {
121 (**self).synthesize_with(text, voice, rate, out, cancel)
122 }
123}
124
125#[cfg(not(target_family = "wasm"))]
127pub fn validate_model_file(
128 path: PathBuf,
129 backend: &'static str,
130) -> Result<PathBuf, SynthesisError> {
131 crate::backend::validate_model_file(path)
132 .map_err(|source| SynthesisError::Model { backend, source })
133}
134
135#[derive(Clone, PartialEq, Eq)]
138pub struct SpeechText(String);
139
140impl std::fmt::Debug for SpeechText {
141 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 write!(formatter, "SpeechText({} bytes)", self.0.len())
143 }
144}
145
146impl SpeechText {
147 pub fn new(text: impl Into<String>) -> Result<Self, SpeechTextError> {
148 let text = text.into();
149 let text = text.trim();
150 if text.is_empty() {
151 return Err(SpeechTextError::Empty);
152 }
153 if text.len() > MAX_SPEECH_TEXT_BYTES {
154 return Err(SpeechTextError::TooLong {
155 actual_bytes: text.len(),
156 maximum_bytes: MAX_SPEECH_TEXT_BYTES,
157 });
158 }
159 Ok(Self(text.to_owned()))
160 }
161
162 pub fn as_str(&self) -> &str {
163 &self.0
164 }
165}
166
167#[derive(Debug, thiserror::Error)]
168pub enum SpeechTextError {
169 #[error("speech text must not be empty")]
170 Empty,
171
172 #[error("speech text contains {actual_bytes} bytes; maximum is {maximum_bytes} bytes")]
173 TooLong {
174 actual_bytes: usize,
175 maximum_bytes: usize,
176 },
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Voice(String);
183
184impl Voice {
185 pub fn new(name: impl Into<String>) -> Result<Self, VoiceError> {
186 let name = name.into();
187 let name = name.trim();
188 if name.is_empty() {
189 return Err(VoiceError::Empty);
190 }
191 if name.len() > MAX_VOICE_BYTES {
192 return Err(VoiceError::TooLong {
193 actual_bytes: name.len(),
194 maximum_bytes: MAX_VOICE_BYTES,
195 });
196 }
197 if name.chars().any(char::is_control) {
198 return Err(VoiceError::Malformed(name.to_owned()));
199 }
200 Ok(Self(name.to_owned()))
201 }
202
203 pub fn as_str(&self) -> &str {
204 &self.0
205 }
206}
207
208#[derive(Debug, thiserror::Error)]
209pub enum VoiceError {
210 #[error("voice name must not be empty")]
211 Empty,
212
213 #[error("voice name contains {actual_bytes} bytes; maximum is {maximum_bytes} bytes")]
214 TooLong {
215 actual_bytes: usize,
216 maximum_bytes: usize,
217 },
218
219 #[error("voice name contains control characters: {0:?}")]
220 Malformed(String),
221}
222
223#[derive(Debug, Clone, Copy, PartialEq)]
225pub struct SpeakingRate(f32);
226
227impl SpeakingRate {
228 pub fn new(rate: f32) -> Result<Self, SpeakingRateError> {
229 if !rate.is_finite() || !(MIN_SPEAKING_RATE..=MAX_SPEAKING_RATE).contains(&rate) {
230 return Err(SpeakingRateError::OutOfRange {
231 actual: rate,
232 minimum: MIN_SPEAKING_RATE,
233 maximum: MAX_SPEAKING_RATE,
234 });
235 }
236 Ok(Self(rate))
237 }
238
239 pub fn get(self) -> f32 {
240 self.0
241 }
242
243 pub const fn standard() -> Self {
244 Self(1.0)
245 }
246}
247
248#[derive(Debug, thiserror::Error)]
249pub enum SpeakingRateError {
250 #[error("speaking rate must be between {minimum} and {maximum}; got {actual}")]
251 OutOfRange {
252 actual: f32,
253 minimum: f32,
254 maximum: f32,
255 },
256}
257
258#[derive(Debug, thiserror::Error)]
259pub enum SynthesisError {
260 #[error("the {backend} model cannot be used")]
261 Model {
262 backend: &'static str,
263 #[source]
264 source: ModelError,
265 },
266
267 #[error("failed to load the {backend} model {model}")]
268 Load {
269 backend: &'static str,
270 model: String,
271 #[source]
272 source: BackendError,
273 },
274
275 #[error("{backend} inference failed")]
276 Inference {
277 backend: &'static str,
278 #[source]
279 source: BackendError,
280 },
281
282 #[error("the {backend} speech program {program} cannot be used")]
283 Program {
284 backend: &'static str,
285 program: String,
286 #[source]
287 source: BackendError,
288 },
289
290 #[error("{backend} has no voice named {voice:?}")]
291 UnsupportedVoice {
292 backend: &'static str,
293 voice: String,
294 },
295
296 #[error("the {backend} backend is not available on {}", std::env::consts::OS)]
297 UnsupportedBackend { backend: &'static str },
298
299 #[error("the {backend} synthesis worker stopped unexpectedly")]
300 WorkerPanicked { backend: &'static str },
301
302 #[error("the {backend} backend plays audio directly and cannot provide PCM")]
303 PcmUnavailable { backend: &'static str },
304
305 #[error("the PCM read buffer must not be empty")]
306 EmptyReadBuffer,
307
308 #[error("synthesis was cancelled")]
309 Cancelled,
310
311 #[error("the synthesizer stopped before the speech ended")]
312 SourceGone,
313
314 #[error("the {backend} synthesizer ran past its time ceiling")]
315 TimedOut { backend: &'static str },
316
317 #[error("the speech already failed and its cause was reported")]
318 AlreadyFailed,
319
320 #[cfg(not(target_family = "wasm"))]
321 #[error("speech playback failed")]
322 Playback {
323 #[source]
324 source: PlaybackError,
325 },
326}
327
328#[cfg(not(target_family = "wasm"))]
329#[derive(Debug, thiserror::Error)]
330pub enum PlaybackError {
331 #[error("no audio output device is available")]
332 NoOutputDevice,
333
334 #[error("the output device offers no usable configuration")]
335 NoUsableConfig(#[source] Box<dyn std::error::Error + Send + Sync>),
336
337 #[error("the output device wants {0} samples, which this build cannot feed")]
338 UnsupportedFormat(String),
339
340 #[error("the output stream could not be started")]
341 Stream(#[source] Box<dyn std::error::Error + Send + Sync>),
342
343 #[error("the output device stopped mid-utterance")]
344 DeviceLost,
345
346 #[error("playback was cancelled")]
347 Cancelled,
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn speech_text_is_trimmed_and_bounded() {
356 let text = SpeechText::new(" hello ").expect("plain text is accepted");
357 assert_eq!(text.as_str(), "hello");
358
359 assert!(matches!(
360 SpeechText::new(" "),
361 Err(SpeechTextError::Empty)
362 ));
363 assert!(matches!(
364 SpeechText::new("x".repeat(MAX_SPEECH_TEXT_BYTES + 1)),
365 Err(SpeechTextError::TooLong { .. })
366 ));
367 }
368
369 #[test]
370 fn voice_names_reject_what_no_engine_could_mean() {
371 assert_eq!(
372 Voice::new("af_heart")
373 .expect("a plain name is accepted")
374 .as_str(),
375 "af_heart"
376 );
377
378 assert!(matches!(Voice::new(""), Err(VoiceError::Empty)));
379 assert!(matches!(
380 Voice::new("x".repeat(MAX_VOICE_BYTES + 1)),
381 Err(VoiceError::TooLong { .. })
382 ));
383 assert!(matches!(
384 Voice::new("bad\nname"),
385 Err(VoiceError::Malformed(_))
386 ));
387 }
388
389 #[test]
390 fn speaking_rate_is_bounded_and_finite() {
391 assert_eq!(SpeakingRate::standard().get(), 1.0);
392 assert!(SpeakingRate::new(2.0).is_ok());
393
394 assert!(SpeakingRate::new(0.0).is_err());
395 assert!(SpeakingRate::new(5.0).is_err());
396 assert!(SpeakingRate::new(f32::NAN).is_err());
397 }
398}