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