129 lines
3.8 KiB
Rust
129 lines
3.8 KiB
Rust
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<Mutex<Option<Child>>>,
|
|
pub server_url: String,
|
|
client: reqwest::Client,
|
|
voice_path: String,
|
|
}
|
|
|
|
impl TtsEngine {
|
|
pub async fn start(voice_dir: &str, port: u16) -> Result<Self> {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|