aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:34:59 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:34:59 +0100
commitfcff527349d49ef9419c4ac7f4b866ad04e8d754 (patch)
treec21241d6f2eb3823f38f5d119870c457280166a9
parentmusic: keep preparation out of the guild lock and requeue failed connects (diff)
downloadsenator-sperma-fcff527349d49ef9419c4ac7f4b866ad04e8d754.tar.gz
senator-sperma-fcff527349d49ef9419c4ac7f4b866ad04e8d754.zip
tts: initialize piper exactly once
-rw-r--r--src/tts.rs81
1 files 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 @@
1use std::io::Cursor; 1use std::io::Cursor;
2use std::path::PathBuf; 2use std::path::PathBuf;
3use std::sync::atomic::{AtomicUsize, Ordering}; 3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex, OnceLock}; 4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
6 6
7use dashmap::DashMap; 7use 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.
45pub struct TtsState { 45pub 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 {
52impl TtsState { 52impl 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