natural-tts 0.3.1

High-level bindings to a variety of text-to-speech libraries.
Documentation
// Copyright (c) 2024-2025 natural-tts
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub mod error;
pub mod models;
mod utils;

#[cfg(test)]
mod test;

use crate::models::NaturalModelTrait;
use derive_builder::Builder;
use models::AudioHandler;
use rodio::{OutputStream, Sink};
use std::path::PathBuf;

#[cfg(feature = "tts-rs")]
use tts::Tts;

#[cfg(feature = "coqui")]
use crate::models::coqui;
#[cfg(feature = "gtts")]
use crate::models::gtts;
#[cfg(feature = "meta")]
use crate::models::meta;
#[cfg(feature = "msedge")]
use crate::models::msedge;
#[cfg(feature = "parler")]
use crate::models::parler;
#[cfg(feature = "tts-rs")]
use crate::models::tts_rs::TtsModel;

use crate::error::TtsError;
#[derive(Builder, Default, Clone)]
#[builder(setter(into))]
pub struct NaturalTts {
    pub default_model: Option<Model>,
    #[builder(default = "None")]
    pub audio_handler: Option<AudioHandler>,

    #[cfg(feature = "tts-rs")]
    #[builder(default = "None")]
    pub tts_model: Option<TtsModel>,

    #[cfg(feature = "parler")]
    #[builder(default = "None")]
    pub parler_model: Option<parler::ParlerModel>,

    #[cfg(feature = "coqui")]
    #[builder(default = "None")]
    pub coqui_model: Option<coqui::CoquiModel>,

    #[cfg(feature = "gtts")]
    #[builder(default = "None")]
    pub gtts_model: Option<gtts::GttsModel>,

    #[cfg(feature = "msedge")]
    #[builder(default = "None")]
    pub msedge_model: Option<msedge::MSEdgeModel>,

    #[cfg(feature = "meta")]
    #[builder(default = "None")]
    pub meta_model: Option<meta::MetaModel>,
}

impl NaturalTts {
    #[cfg(feature = "tts-rs")]
    pub fn get_tts_handler(&mut self) -> Result<&mut Tts, TtsError> {
        if let Some(tts) = &mut self.tts_model {
            Ok(&mut tts.0)
        } else {
            Err(TtsError::NotLoaded)
        }
    }

    pub fn get_rodio_sink_mut(&mut self) -> Result<(&mut Sink, &mut OutputStream), TtsError> {
        match &mut self.audio_handler {
            Some(AudioHandler::Sink { sink, stream }) => Ok((sink, stream)),
            #[cfg(feature = "tts-rs")]
            Some(_) => Err(TtsError::NotSupported),
            _ => Err(TtsError::NotLoaded),
        }
    }

    pub fn get_rodio_sink(&self) -> Result<(&Sink, &OutputStream), TtsError> {
        match &self.audio_handler {
            Some(AudioHandler::Sink { sink, stream }) => Ok((sink, stream)),
            #[cfg(feature = "tts-rs")]
            Some(_) => Err(TtsError::NotSupported),
            _ => Err(TtsError::NotLoaded),
        }
    }

    pub fn sleep_until_end(&self) {
        match &self.audio_handler {
            Some(AudioHandler::Sink { sink, stream: _ }) => sink.sleep_until_end(),
            #[cfg(feature = "tts-rs")]
            _ => {
                use std::{thread, time::Duration};
                if let Some(x) = &self.tts_model {
                    while x.0.is_speaking().unwrap_or_default() {
                        thread::sleep(Duration::from_millis(100));
                    }
                }
            }
            #[allow(unreachable_patterns)]
            _ => {}
        }
    }

    pub fn start(&mut self, message: String, path: &PathBuf) -> Result<(), TtsError> {
        if let Some(model) = &self.default_model {
            self.audio_handler = Some(match model {
                #[cfg(feature = "coqui")]
                Model::Coqui => match &mut self.coqui_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "parler")]
                Model::Parler => match &mut self.parler_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "tts-rs")]
                Model::TTS => match &mut self.tts_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "msedge")]
                Model::MSEdge => match &mut self.msedge_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "meta")]
                Model::Meta => match &mut self.meta_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "gtts")]
                _ => match &mut self.gtts_model {
                    Some(x) => x.start(message, path),
                    None => Err(TtsError::NotLoaded),
                },
            }?);

            return Ok(());
        }

        Err(TtsError::NoDefaultModel)
    }

    pub fn synthesize(
        &mut self,
        message: String,
        path: &PathBuf,
    ) -> Result<models::SynthesizedAudio<f32>, TtsError> {
        if let Some(model) = &self.default_model {
            return match model {
                #[cfg(feature = "coqui")]
                Model::Coqui => match &mut self.coqui_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "parler")]
                Model::Parler => match &mut self.parler_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "tts-rs")]
                Model::TTS => match &mut self.tts_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "msedge")]
                Model::MSEdge => match &mut self.msedge_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "meta")]
                Model::Meta => match &mut self.meta_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "gtts")]
                _ => match &mut self.gtts_model {
                    Some(x) => x.synthesize(message, path),
                    None => Err(TtsError::NotLoaded),
                },
            };
        }

        Err(TtsError::NoDefaultModel)
    }

    pub fn save(&mut self, message: String, path: &PathBuf) -> Result<(), TtsError> {
        if let Some(model) = &self.default_model {
            return match model {
                #[cfg(feature = "coqui")]
                Model::Coqui => match &mut self.coqui_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "parler")]
                Model::Parler => match &mut self.parler_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "tts-rs")]
                Model::TTS => match &mut self.tts_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "msedge")]
                Model::MSEdge => match &mut self.msedge_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "meta")]
                Model::Meta => match &mut self.meta_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
                #[cfg(feature = "gtts")]
                _ => match &mut self.gtts_model {
                    Some(x) => x.save(message, path),
                    None => Err(TtsError::NotLoaded),
                },
            };
        }

        Err(TtsError::NoDefaultModel)
    }

    pub fn resume(&mut self) -> Result<(), TtsError> {
        match &mut self.audio_handler {
            Some(AudioHandler::Sink { sink, stream: _ }) => sink.play(),
            #[cfg(feature = "tts-rs")]
            Some(_) => return Err(TtsError::NotSupported),
            _ => return Err(TtsError::NotLoaded),
        }

        Ok(())
    }

    pub fn stop(&mut self) -> Result<(), TtsError> {
        match &mut self.audio_handler {
            Some(AudioHandler::Sink { sink, stream: _ }) => sink.stop(),
            #[cfg(feature = "tts-rs")]
            Some(AudioHandler::Tts) => match &mut self.tts_model {
                Some(x) => {
                    let _ = x.0.stop()?;
                }
                None => return Err(TtsError::NotSupported),
            },
            _ => return Err(TtsError::NotLoaded),
        }

        Ok(())
    }
}

#[derive(Clone)]
pub enum Model {
    #[cfg(feature = "coqui")]
    Coqui,

    #[cfg(feature = "parler")]
    Parler,

    #[cfg(feature = "tts-rs")]
    TTS,

    #[cfg(feature = "msedge")]
    MSEdge,

    #[cfg(feature = "meta")]
    Meta,

    #[cfg(feature = "gtts")]
    Gtts,
}

impl Default for Model {
    fn default() -> Self {
        #[cfg(feature = "gtts")]
        #[allow(unreachable_code)]
        {
            return Self::Gtts;
        }
        #[cfg(feature = "parler")]
        #[allow(unreachable_code)]
        {
            return Self::Parler;
        }
        #[cfg(feature = "meta")]
        #[allow(unreachable_code)]
        {
            return Self::Meta;
        }
        #[cfg(feature = "msedge")]
        #[allow(unreachable_code)]
        {
            return Self::MSEdge;
        }
        #[cfg(feature = "tts-rs")]
        #[allow(unreachable_code)]
        {
            return Self::TTS;
        }
        #[cfg(feature = "coqui")]
        #[allow(unreachable_code)]
        {
            return Self::Coqui;
        }
    }
}