use crate::config::{Config, SynthesisProvider};
use crate::error::TalkError;
use async_trait::async_trait;
pub mod lang;
pub mod mistral;
pub mod mistral_presets;
pub mod resolve;
#[cfg(feature = "kokoro")]
pub mod kokoro;
pub use lang::detect_lang;
pub use resolve::{guard_voice_lang, primary_subtag, LangSource, VoiceMeta};
#[derive(Debug, Clone)]
pub struct SynthesisRequest {
pub text: String,
pub voice: Option<String>,
pub speed: Option<f32>,
pub lang: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SynthesisResult {
pub pcm: Vec<i16>,
pub sample_rate: u32,
}
impl SynthesisResult {
pub fn duration_secs(&self) -> f64 {
if self.sample_rate == 0 {
0.0
} else {
self.pcm.len() as f64 / self.sample_rate as f64
}
}
}
#[async_trait]
pub(crate) trait OneShotSynthesizer: Send + Sync {
async fn validate(&self) -> Result<(), TalkError>;
async fn synthesize(&self, req: SynthesisRequest) -> Result<SynthesisResult, TalkError>;
}
pub async fn synthesize(
config: &Config,
provider: SynthesisProvider,
request: SynthesisRequest,
) -> Result<SynthesisResult, TalkError> {
let synthesizer = create_oneshot_synthesizer(config, provider)?;
synthesizer.validate().await?;
synthesizer.synthesize(request).await
}
pub(crate) fn create_oneshot_synthesizer(
config: &Config,
provider: SynthesisProvider,
) -> Result<Box<dyn OneShotSynthesizer>, TalkError> {
match provider {
SynthesisProvider::Mistral => {
let cfg = config.providers.mistral.clone().ok_or_else(|| {
TalkError::Config(
"Mistral synthesis selected but providers.mistral is not configured"
.to_string(),
)
})?;
if cfg.api_key.is_empty() {
return Err(TalkError::Config(
"providers.mistral.api_key is required".to_string(),
));
}
Ok(Box::new(mistral::MistralOneShotSynthesizer::new(cfg)?))
}
#[cfg(feature = "kokoro")]
SynthesisProvider::Kokoro => {
let cfg = config.providers.kokoro.clone().unwrap_or_default();
Ok(Box::new(kokoro::KokoroOneShotSynthesizer::new(cfg)?))
}
#[cfg(not(feature = "kokoro"))]
SynthesisProvider::Kokoro => Err(TalkError::Config(
"talk-rs was built without the 'kokoro' feature; rebuild with \
--features kokoro to enable the local Kokoro TTS backend"
.to_string(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn synthesis_result_duration() {
let r = SynthesisResult {
pcm: vec![0i16; 24_000],
sample_rate: 24_000,
};
assert!((r.duration_secs() - 1.0).abs() < 1e-9);
}
#[test]
fn synthesis_result_duration_zero_rate_is_zero() {
let r = SynthesisResult {
pcm: vec![0i16; 100],
sample_rate: 0,
};
assert_eq!(r.duration_secs(), 0.0);
}
}