use anyhow::{anyhow, Context, Result}; use rodio::{Decoder, OutputStream, Sink}; use std::io::Cursor; use std::process::{Child, Command}; use std::sync::Arc; use tokio::sync::Mutex; pub struct TtsEngine { process: Arc>>, pub server_url: String, client: reqwest::Client, voice_path: String, } impl TtsEngine { pub async fn start(voice_dir: &str, port: u16) -> Result { let server_url = format!("http://127.0.0.1:{}", port); let client = reqwest::Client::new(); // Check if server is already running let already_running = client .get(&format!("{}/", server_url)) .send() .await .is_ok(); let process = if !already_running { println!("Starting XTTS TTS server on port {}...", port); match Command::new("python") .args([ "-m", "xtts_api_server", "--port", &port.to_string(), "--speaker-folder", voice_dir, ]) .spawn() { Ok(child) => { println!("XTTS server starting (model loading may take a minute)..."); Some(child) } Err(e) => { eprintln!("Could not start XTTS server: {}. Voice will be unavailable.", e); eprintln!("Install with: pip install xtts-api-server"); None } } } else { println!("XTTS server already running on {}", server_url); None }; Ok(Self { process: Arc::new(Mutex::new(process)), server_url, client, voice_path: voice_dir.to_string(), }) } pub async fn speak(&self, text: &str, speaker: &str) -> Result<()> { let speaker_path = format!("{}/{}.wav", self.voice_path, speaker); let response = self .client .post(format!("{}/tts", self.server_url)) .json(&serde_json::json!({ "text": text, "speaker_wav": speaker_path, "language": "en" })) .send() .await .context("Failed to reach XTTS server")?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(anyhow!("XTTS error {}: {}", status, body)); } let wav_bytes = response.bytes().await?; // Play audio in a blocking thread (rodio requires sync) let bytes = wav_bytes.to_vec(); std::thread::spawn(move || { if let Ok((_stream, stream_handle)) = OutputStream::try_default() { if let Ok(sink) = Sink::try_new(&stream_handle) { if let Ok(source) = Decoder::new(Cursor::new(bytes)) { sink.append(source); sink.sleep_until_end(); } } } }); Ok(()) } pub async fn stop(&self) { if let Ok(mut guard) = self.process.lock().await { if let Some(ref mut child) = *guard { let _ = child.kill(); let _ = child.wait(); } } } pub async fn is_available(&self) -> bool { self.client .get(&format!("{}/", self.server_url)) .send() .await .is_ok() } } impl Drop for TtsEngine { fn drop(&mut self) { if let Ok(mut guard) = self.process.lock() { if let Some(ref mut child) = *guard { let _ = child.kill(); let _ = child.wait(); } } } }