From fcff527349d49ef9419c4ac7f4b866ad04e8d754 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:34:59 +0100 Subject: tts: initialize piper exactly once --- src/tts.rs | 81 ++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/tts.rs b/src/tts.rs index 4754054..c65e5cd 100644 --- a/src/tts.rs +++ b/src/tts.rs @@ -1,7 +1,7 @@ use std::io::Cursor; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use dashmap::DashMap; @@ -43,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: tokio::sync::OnceCell>>, cache: DashMap, Instant)>, cache_bytes: AtomicUsize, model_path: Option, @@ -52,7 +52,7 @@ pub struct TtsState { impl TtsState { pub fn new(model_path: Option) -> Self { Self { - piper: OnceLock::new(), + piper: tokio::sync::OnceCell::new(), cache: DashMap::new(), cache_bytes: AtomicUsize::new(0), model_path, @@ -62,46 +62,43 @@ 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>> { - if let Some(piper) = self.piper.get() { - return Ok(piper.clone()); - } - - let path = self - .model_path - .as_ref() - .ok_or_else(|| Error::Custom("TTS_MODEL_PATH is not set".into()))?; - - // Resolve .onnx.json config path from model path - let onnx_path = path.clone(); - let config_path = { - let s = path.to_string_lossy(); - if s.ends_with(".onnx") { - PathBuf::from(format!("{}.json", s)) - } else { - path.clone() - } - }; - - debug!( - onnx = %onnx_path.display(), - config = %config_path.display(), - "loading piper model" - ); - // 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(Arc::new(Mutex::new(piper))); - Ok(self + // `get_or_try_init` guarantees only one task loads the model even when + // several `/tts` calls arrive at once. + let piper = self .piper - .get() - .expect("piper was just initialized") - .clone()) + .get_or_try_init(|| async { + let path = self + .model_path + .as_ref() + .ok_or_else(|| Error::Custom("TTS_MODEL_PATH is not set".into()))?; + + // Resolve .onnx.json config path from model path + let onnx_path = path.clone(); + let config_path = { + let s = path.to_string_lossy(); + if s.ends_with(".onnx") { + PathBuf::from(format!("{}.json", s)) + } else { + path.clone() + } + }; + + debug!( + onnx = %onnx_path.display(), + config = %config_path.display(), + "loading piper model" + ); + // Model loading is CPU/IO heavy; keep it off the async runtime. + 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"); + Ok::<_, Error>(Arc::new(Mutex::new(piper))) + }) + .await?; + Ok(piper.clone()) } /// Look up cached audio for a text string. Returns `None` if uncached -- cgit v1.2.3