diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 01:34:59 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 01:34:59 +0100 |
| commit | fcff527349d49ef9419c4ac7f4b866ad04e8d754 (patch) | |
| tree | c21241d6f2eb3823f38f5d119870c457280166a9 | |
| parent | music: keep preparation out of the guild lock and requeue failed connects (diff) | |
| download | senator-sperma-fcff527349d49ef9419c4ac7f4b866ad04e8d754.tar.gz senator-sperma-fcff527349d49ef9419c4ac7f4b866ad04e8d754.zip | |
tts: initialize piper exactly once
| -rw-r--r-- | src/tts.rs | 81 |
1 files changed, 39 insertions, 42 deletions
| @@ -1,7 +1,7 @@ | |||
| 1 | use std::io::Cursor; | 1 | use std::io::Cursor; |
| 2 | use std::path::PathBuf; | 2 | use std::path::PathBuf; |
| 3 | use std::sync::atomic::{AtomicUsize, Ordering}; | 3 | use std::sync::atomic::{AtomicUsize, Ordering}; |
| 4 | use std::sync::{Arc, Mutex, OnceLock}; | 4 | use std::sync::{Arc, Mutex}; |
| 5 | use std::time::{Duration, Instant}; | 5 | use std::time::{Duration, Instant}; |
| 6 | 6 | ||
| 7 | use dashmap::DashMap; | 7 | use dashmap::DashMap; |
| @@ -43,7 +43,7 @@ const EVENT_TYPES: &[&str] = &[ | |||
| 43 | /// 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. |
| 44 | /// Cache maps full text string → resampled 48 kHz stereo PCM bytes. | 44 | /// Cache maps full text string → resampled 48 kHz stereo PCM bytes. |
| 45 | pub struct TtsState { | 45 | pub struct TtsState { |
| 46 | piper: OnceLock<Arc<Mutex<Piper>>>, | 46 | piper: tokio::sync::OnceCell<Arc<Mutex<Piper>>>, |
| 47 | cache: DashMap<String, (Vec<u8>, Instant)>, | 47 | cache: DashMap<String, (Vec<u8>, Instant)>, |
| 48 | cache_bytes: AtomicUsize, | 48 | cache_bytes: AtomicUsize, |
| 49 | model_path: Option<PathBuf>, | 49 | model_path: Option<PathBuf>, |
| @@ -52,7 +52,7 @@ pub struct TtsState { | |||
| 52 | impl TtsState { | 52 | impl TtsState { |
| 53 | pub fn new(model_path: Option<PathBuf>) -> Self { | 53 | pub fn new(model_path: Option<PathBuf>) -> Self { |
| 54 | Self { | 54 | Self { |
| 55 | piper: OnceLock::new(), | 55 | piper: tokio::sync::OnceCell::new(), |
| 56 | cache: DashMap::new(), | 56 | cache: DashMap::new(), |
| 57 | cache_bytes: AtomicUsize::new(0), | 57 | cache_bytes: AtomicUsize::new(0), |
| 58 | model_path, | 58 | model_path, |
| @@ -62,46 +62,43 @@ impl TtsState { | |||
| 62 | /// 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 |
| 63 | /// unset or model fails to load. | 63 | /// unset or model fails to load. |
| 64 | async fn get_piper(&self) -> Result<Arc<Mutex<Piper>>> { | 64 | async fn get_piper(&self) -> Result<Arc<Mutex<Piper>>> { |
| 65 | if let Some(piper) = self.piper.get() { | 65 | // `get_or_try_init` guarantees only one task loads the model even when |
| 66 | return Ok(piper.clone()); | 66 | // several `/tts` calls arrive at once. |
| 67 | } | 67 | let piper = self |
| 68 | |||
| 69 | let path = self | ||
| 70 | .model_path | ||
| 71 | .as_ref() | ||
| 72 | .ok_or_else(|| Error::Custom("TTS_MODEL_PATH is not set".into()))?; | ||
| 73 | |||
| 74 | // Resolve .onnx.json config path from model path | ||
| 75 | let onnx_path = path.clone(); | ||
| 76 | let config_path = { | ||
| 77 | let s = path.to_string_lossy(); | ||
| 78 | if s.ends_with(".onnx") { | ||
| 79 | PathBuf::from(format!("{}.json", s)) | ||
| 80 | } else { | ||
| 81 | path.clone() | ||
| 82 | } | ||
| 83 | }; | ||
| 84 | |||
| 85 | debug!( | ||
| 86 | onnx = %onnx_path.display(), | ||
| 87 | config = %config_path.display(), | ||
| 88 | "loading piper model" | ||
| 89 | ); | ||
| 90 | // Model loading is CPU/IO heavy; keep it off the async runtime too. | ||
| 91 | let piper = tokio::task::spawn_blocking(move || { | ||
| 92 | Piper::new(&onnx_path, &config_path) | ||
| 93 | .map_err(|e| Error::Custom(format!("Failed to load piper model: {e}"))) | ||
| 94 | }) | ||
| 95 | .await??; | ||
| 96 | info!("piper model loaded successfully"); | ||
| 97 | |||
| 98 | // set() may fail if another task initialized first — that's fine | ||
| 99 | let _ = self.piper.set(Arc::new(Mutex::new(piper))); | ||
| 100 | Ok(self | ||
| 101 | .piper | 68 | .piper |
| 102 | .get() | 69 | .get_or_try_init(|| async { |
| 103 | .expect("piper was just initialized") | 70 | let path = self |
| 104 | .clone()) | 71 | .model_path |
| 72 | .as_ref() | ||
| 73 | .ok_or_else(|| Error::Custom("TTS_MODEL_PATH is not set".into()))?; | ||
| 74 | |||
| 75 | // Resolve .onnx.json config path from model path | ||
| 76 | let onnx_path = path.clone(); | ||
| 77 | let config_path = { | ||
| 78 | let s = path.to_string_lossy(); | ||
| 79 | if s.ends_with(".onnx") { | ||
| 80 | PathBuf::from(format!("{}.json", s)) | ||
| 81 | } else { | ||
| 82 | path.clone() | ||
| 83 | } | ||
| 84 | }; | ||
| 85 | |||
| 86 | debug!( | ||
| 87 | onnx = %onnx_path.display(), | ||
| 88 | config = %config_path.display(), | ||
| 89 | "loading piper model" | ||
| 90 | ); | ||
| 91 | // Model loading is CPU/IO heavy; keep it off the async runtime. | ||
| 92 | let piper = tokio::task::spawn_blocking(move || { | ||
| 93 | Piper::new(&onnx_path, &config_path) | ||
| 94 | .map_err(|e| Error::Custom(format!("Failed to load piper model: {e}"))) | ||
| 95 | }) | ||
| 96 | .await??; | ||
| 97 | info!("piper model loaded successfully"); | ||
| 98 | Ok::<_, Error>(Arc::new(Mutex::new(piper))) | ||
| 99 | }) | ||
| 100 | .await?; | ||
| 101 | Ok(piper.clone()) | ||
| 105 | } | 102 | } |
| 106 | 103 | ||
| 107 | /// Look up cached audio for a text string. Returns `None` if uncached | 104 | /// Look up cached audio for a text string. Returns `None` if uncached |
