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/main.rs | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index d06176d..31c9e94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod events; mod lang; mod openrouter; mod tasks; +mod tts; mod voice; use database::DbPool; @@ -26,9 +27,7 @@ const AC_SEARCH_CACHE_TTL: Duration = Duration::from_secs(10); type AcCache = tokio::sync::Mutex>; /// Shared autocomplete search cache: query → (hits, cached_at). -type AcSearchCache = Arc< - tokio::sync::Mutex, Instant)>>, ->; +type AcSearchCache = Arc, Instant)>>>; pub struct Data { pub db_pool: DbPool, @@ -84,10 +83,7 @@ impl Data { /// Look up search cache for an autocomplete query. Returns `None` if /// uncached or stale. - pub async fn ac_search_cache_lookup( - &self, - query: &str, - ) -> Option> { + pub async fn ac_search_cache_lookup(&self, query: &str) -> Option> { let cache = self.ac_search_cache.lock().await; let (hits, cached_at) = cache.get(query)?; if cached_at.elapsed() > AC_SEARCH_CACHE_TTL { @@ -150,6 +146,34 @@ async fn main() -> Result<(), Box> { Ok(()) }) }, + on_error: |error| { + Box::pin(async move { + match error { + poise::FrameworkError::Command { ctx, error, .. } => { + tracing::error!(command = %ctx.command().name, "Command failed: {error}"); + if let Err(e) = ctx + .send( + poise::CreateReply::default() + .content(format!("Błąd: {error}")) + .ephemeral(true), + ) + .await + { + tracing::debug!( + "Could not send error reply (interaction likely expired): {e}" + ); + } + } + other => { + if let Err(e) = poise::builtins::on_error(other).await { + tracing::warn!( + "Error while sending error message (interaction likely expired): {e}" + ); + } + } + } + }) + }, ..Default::default() }) .setup(move |ctx, _ready, framework| { -- cgit v1.2.3