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