From f672d84951cf7dea4f47429475b109426f2be944 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:12:12 +0100 Subject: tts: run piper loading and synthesis on blocking threads --- src/tts.rs | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/tts.rs b/src/tts.rs index dbe0d88..4754054 100644 --- a/src/tts.rs +++ b/src/tts.rs @@ -1,12 +1,11 @@ use std::io::Cursor; use std::path::PathBuf; -use std::sync::OnceLock; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; use std::time::{Duration, Instant}; use dashmap::DashMap; use piper_rs::Piper; -use tokio::sync::Mutex; use tracing::{debug, info, warn}; use crate::error::{Error, Result}; @@ -44,7 +43,7 @@ const EVENT_TYPES: &[&str] = &[ /// Held in poise `Data`. Piper is loaded lazily on first synthesis call. /// Cache maps full text string → resampled 48 kHz stereo PCM bytes. pub struct TtsState { - piper: OnceLock>, + piper: OnceLock>>, cache: DashMap, Instant)>, cache_bytes: AtomicUsize, model_path: Option, @@ -62,9 +61,9 @@ impl TtsState { /// Get or initialize the Piper model. Returns error if model path is /// unset or model fails to load. - async fn get_piper(&self) -> Result<&Mutex> { + async fn get_piper(&self) -> Result>> { if let Some(piper) = self.piper.get() { - return Ok(piper); + return Ok(piper.clone()); } let path = self @@ -88,14 +87,21 @@ impl TtsState { config = %config_path.display(), "loading piper model" ); - let piper = Piper::new(&onnx_path, &config_path).map_err(|e| { - Error::Custom(format!("Failed to load piper model: {e}")) - })?; + // Model loading is CPU/IO heavy; keep it off the async runtime too. + let piper = tokio::task::spawn_blocking(move || { + Piper::new(&onnx_path, &config_path) + .map_err(|e| Error::Custom(format!("Failed to load piper model: {e}"))) + }) + .await??; info!("piper model loaded successfully"); // set() may fail if another task initialized first — that's fine - let _ = self.piper.set(Mutex::new(piper)); - Ok(self.piper.get().unwrap()) + let _ = self.piper.set(Arc::new(Mutex::new(piper))); + Ok(self + .piper + .get() + .expect("piper was just initialized") + .clone()) } /// Look up cached audio for a text string. Returns `None` if uncached @@ -181,15 +187,21 @@ impl TtsState { return Ok(cached); } - // Cache miss — synthesize + // Cache miss — synthesize on a blocking thread. Piper inference is + // synchronous and can take >100 ms; it must not stall a Tokio worker. let piper = self.get_piper().await?; + let text_owned = text.to_string(); let started = Instant::now(); - let (samples, sample_rate) = { - let mut lock = piper.lock().await; - lock.create(text, false, None, None, None, None) - .map_err(|e| Error::Custom(format!("piper synthesis failed: {e}")))? - }; + let (samples, sample_rate) = tokio::task::spawn_blocking(move || { + let mut piper = piper + .lock() + .map_err(|_| Error::Custom("piper mutex poisoned".into()))?; + piper + .create(&text_owned, false, None, None, None, None) + .map_err(|e| Error::Custom(format!("piper synthesis failed: {e}"))) + }) + .await??; let elapsed_ms = started.elapsed().as_millis() as u64; debug!( -- cgit v1.2.3