From b252c39517b9fce181d1a0fe4761404c7d981397 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 18 Aug 2026 23:18:37 +0200 Subject: feat: local TTS via piper subprocess with Google fallback - New src/tts.rs: centralize TTS in synthesize(config, http_client, text) Piper subprocess primary, Google Translate fallback on error/missing - TTS_MODEL_PATH/TTS_VOICE env config (defaults to pl_PL-bass-high) - Fix Unknown interaction errors: spawn play_tts_in_channel in background so voice-state TTS no longer blocks the shard event loop (3s Discord limit) - Best-effort confirmation replies in /tts to prevent error cascade - Custom on_error handler: graceful interaction-expired error handling - Comprehensive tracing logging (engine, model, duration, stderr) --- src/config.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'src/config.rs') diff --git a/src/config.rs b/src/config.rs index 989a060..3b92dd8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,12 @@ pub struct Config { pub llm_model: Option, pub llm_api_url: String, pub database_path: String, + /// Optional path to the Piper TTS `.onnx` model (or a directory of models). + /// When unset, TTS falls back to Google Translate TTS. + pub tts_model_path: Option, + /// Piper voice name, used to resolve the model file when `tts_model_path` + /// points at a directory. Ignored when it points at a `.onnx` file directly. + pub tts_voice: String, } struct EnvVar { @@ -100,6 +106,18 @@ const ENV_VARS: &[EnvVar] = &[ default: Some("data/senator.db"), description: "SQLite database path", }, + EnvVar { + name: "TTS_MODEL_PATH", + required: false, + default: None, + description: "Path to a Piper TTS .onnx model (or a models directory). Unset => Google TTS fallback", + }, + EnvVar { + name: "TTS_VOICE", + required: false, + default: Some("pl_PL-bass-high"), + description: "Piper TTS voice name (used when TTS_MODEL_PATH points at a directory)", + }, EnvVar { name: "RUST_LOG", required: false, @@ -180,6 +198,23 @@ impl Config { llm_api_url: env::var("LLM_API_URL") .unwrap_or_else(|_| "https://openrouter.ai/api/v1/chat/completions".into()), database_path: env::var("SENATOR_DB_PATH").unwrap_or_else(|_| "data/senator.db".into()), + tts_model_path: env::var("TTS_MODEL_PATH").ok(), + tts_voice: env::var("TTS_VOICE").unwrap_or_else(|_| "pl_PL-bass-high".into()), + } + } + + /// Resolve the Piper model path to pass to the `piper` CLI. + /// + /// - `TTS_MODEL_PATH` set to a `.onnx` file => used verbatim. + /// - `TTS_MODEL_PATH` set to a directory => `{dir}/{TTS_VOICE}.onnx`. + /// - `TTS_MODEL_PATH` unset => `None` (caller falls back to Google TTS). + pub fn piper_model_path(&self) -> Option { + let configured = self.tts_model_path.as_ref()?; + let path = std::path::PathBuf::from(configured); + if path.is_dir() { + Some(path.join(format!("{}.onnx", self.tts_voice))) + } else { + Some(path) } } -- cgit v1.2.3