playing with AI voice didnt test yet
This commit is contained in:
+15
@@ -12,6 +12,7 @@ mod executor;
|
||||
mod gui;
|
||||
mod knowledge_index;
|
||||
mod pdf_import;
|
||||
mod tts;
|
||||
mod web_server;
|
||||
|
||||
use crate::data_ls::loader::load_english;
|
||||
@@ -136,6 +137,7 @@ async fn build_state() -> Result<AppState> {
|
||||
target: Arc::new(Mutex::new(None)),
|
||||
shutdown: Arc::new(tokio::sync::Notify::new()),
|
||||
embed_cache: Arc::new(Mutex::new(EmbeddingCache::new())),
|
||||
tts: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -153,6 +155,18 @@ async fn build_state() -> Result<AppState> {
|
||||
}
|
||||
});
|
||||
|
||||
// Start TTS engine
|
||||
let tts = match crate::tts::TtsEngine::start("voices", 8020).await {
|
||||
Ok(engine) => {
|
||||
println!("TTS engine ready");
|
||||
Some(Arc::new(engine))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("TTS engine unavailable: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(AppState {
|
||||
ollama,
|
||||
knowledge,
|
||||
@@ -160,6 +174,7 @@ async fn build_state() -> Result<AppState> {
|
||||
target: Arc::new(Mutex::new(None)),
|
||||
shutdown: Arc::new(tokio::sync::Notify::new()),
|
||||
embed_cache: Arc::new(Mutex::new(EmbeddingCache::new())),
|
||||
tts,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ use crate::executor::{detect_language, execute_command, execute_script, Executio
|
||||
use crate::knowledge_index::{
|
||||
format_knowledge, EmbeddingCache, KnowledgeIndex, SearchProfile, OLLAMA_SEM,
|
||||
};
|
||||
use crate::tts::TtsEngine;
|
||||
use crate::training::websearch::{
|
||||
append_sample_to_jsonl, compute_sample_embedding, web_search, SearchEngine,
|
||||
};
|
||||
@@ -64,6 +65,7 @@ pub struct AppState {
|
||||
pub target: Arc<Mutex<Option<String>>>,
|
||||
pub shutdown: Arc<tokio::sync::Notify>,
|
||||
pub embed_cache: Arc<Mutex<EmbeddingCache>>,
|
||||
pub tts: Option<Arc<TtsEngine>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -164,6 +166,8 @@ pub async fn start_server(addr: &str, state: AppState, open_browser: bool) -> an
|
||||
.route("/api/target/set", post(handle_target_set))
|
||||
.route("/api/target/clear", post(handle_target_clear))
|
||||
.route("/api/chat/clear", post(handle_chat_clear))
|
||||
.route("/api/tts/speak", post(handle_tts_speak))
|
||||
.route("/api/tts/status", get(handle_tts_status))
|
||||
.route("/api/shutdown", post(handle_shutdown))
|
||||
.layer(cors)
|
||||
.with_state(state.clone());
|
||||
|
||||
Reference in New Issue
Block a user