aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:12:12 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:12:12 +0100
commitf672d84951cf7dea4f47429475b109426f2be944 (patch)
treec6b39d0af495ee7612a45f84f290cf23babba541
parentprocesses: add timeouts and keep decoding off the runtime (diff)
downloadsenator-sperma-f672d84951cf7dea4f47429475b109426f2be944.tar.gz
senator-sperma-f672d84951cf7dea4f47429475b109426f2be944.zip
tts: run piper loading and synthesis on blocking threads
-rw-r--r--src/tts.rs44
1 files changed, 28 insertions, 16 deletions
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 @@
1use std::io::Cursor; 1use std::io::Cursor;
2use std::path::PathBuf; 2use std::path::PathBuf;
3use std::sync::OnceLock;
4use std::sync::atomic::{AtomicUsize, Ordering}; 3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex, OnceLock};
5use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
6 6
7use dashmap::DashMap; 7use dashmap::DashMap;
8use piper_rs::Piper; 8use piper_rs::Piper;
9use tokio::sync::Mutex;
10use tracing::{debug, info, warn}; 9use tracing::{debug, info, warn};
11 10
12use crate::error::{Error, Result}; 11use crate::error::{Error, Result};
@@ -44,7 +43,7 @@ const EVENT_TYPES: &[&str] = &[
44/// Held in poise `Data`. Piper is loaded lazily on first synthesis call. 43/// Held in poise `Data`. Piper is loaded lazily on first synthesis call.
45/// Cache maps full text string → resampled 48 kHz stereo PCM bytes. 44/// Cache maps full text string → resampled 48 kHz stereo PCM bytes.
46pub struct TtsState { 45pub struct TtsState {
47 piper: OnceLock<Mutex<Piper>>, 46 piper: OnceLock<Arc<Mutex<Piper>>>,
48 cache: DashMap<String, (Vec<u8>, Instant)>, 47 cache: DashMap<String, (Vec<u8>, Instant)>,
49 cache_bytes: AtomicUsize, 48 cache_bytes: AtomicUsize,
50 model_path: Option<PathBuf>, 49 model_path: Option<PathBuf>,
@@ -62,9 +61,9 @@ impl TtsState {
62 61
63 /// Get or initialize the Piper model. Returns error if model path is 62 /// Get or initialize the Piper model. Returns error if model path is
64 /// unset or model fails to load. 63 /// unset or model fails to load.
65 async fn get_piper(&self) -> Result<&Mutex<Piper>> { 64 async fn get_piper(&self) -> Result<Arc<Mutex<Piper>>> {
66 if let Some(piper) = self.piper.get() { 65 if let Some(piper) = self.piper.get() {
67 return Ok(piper); 66 return Ok(piper.clone());
68 } 67 }
69 68
70 let path = self 69 let path = self
@@ -88,14 +87,21 @@ impl TtsState {
88 config = %config_path.display(), 87 config = %config_path.display(),
89 "loading piper model" 88 "loading piper model"
90 ); 89 );
91 let piper = Piper::new(&onnx_path, &config_path).map_err(|e| { 90 // Model loading is CPU/IO heavy; keep it off the async runtime too.
92 Error::Custom(format!("Failed to load piper model: {e}")) 91 let piper = tokio::task::spawn_blocking(move || {
93 })?; 92 Piper::new(&onnx_path, &config_path)
93 .map_err(|e| Error::Custom(format!("Failed to load piper model: {e}")))
94 })
95 .await??;
94 info!("piper model loaded successfully"); 96 info!("piper model loaded successfully");
95 97
96 // set() may fail if another task initialized first — that's fine 98 // set() may fail if another task initialized first — that's fine
97 let _ = self.piper.set(Mutex::new(piper)); 99 let _ = self.piper.set(Arc::new(Mutex::new(piper)));
98 Ok(self.piper.get().unwrap()) 100 Ok(self
101 .piper
102 .get()
103 .expect("piper was just initialized")
104 .clone())
99 } 105 }
100 106
101 /// Look up cached audio for a text string. Returns `None` if uncached 107 /// Look up cached audio for a text string. Returns `None` if uncached
@@ -181,15 +187,21 @@ impl TtsState {
181 return Ok(cached); 187 return Ok(cached);
182 } 188 }
183 189
184 // Cache miss — synthesize 190 // Cache miss — synthesize on a blocking thread. Piper inference is
191 // synchronous and can take >100 ms; it must not stall a Tokio worker.
185 let piper = self.get_piper().await?; 192 let piper = self.get_piper().await?;
193 let text_owned = text.to_string();
186 let started = Instant::now(); 194 let started = Instant::now();
187 195
188 let (samples, sample_rate) = { 196 let (samples, sample_rate) = tokio::task::spawn_blocking(move || {
189 let mut lock = piper.lock().await; 197 let mut piper = piper
190 lock.create(text, false, None, None, None, None) 198 .lock()
191 .map_err(|e| Error::Custom(format!("piper synthesis failed: {e}")))? 199 .map_err(|_| Error::Custom("piper mutex poisoned".into()))?;
192 }; 200 piper
201 .create(&text_owned, false, None, None, None, None)
202 .map_err(|e| Error::Custom(format!("piper synthesis failed: {e}")))
203 })
204 .await??;
193 205
194 let elapsed_ms = started.elapsed().as_millis() as u64; 206 let elapsed_ms = started.elapsed().as_millis() as u64;
195 debug!( 207 debug!(