diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-18 23:18:37 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-18 23:18:37 +0200 |
| commit | b252c39517b9fce181d1a0fe4761404c7d981397 (patch) | |
| tree | 564cf01aed601f1974e0de8801d4452215ec05f5 /src/main.rs | |
| parent | fir auto completion and send file in the response body upon grajcuj file (diff) | |
| download | senator-sperma-b252c39517b9fce181d1a0fe4761404c7d981397.tar.gz senator-sperma-b252c39517b9fce181d1a0fe4761404c7d981397.zip | |
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)
Diffstat (limited to '')
| -rw-r--r-- | src/main.rs | 38 |
1 files changed, 31 insertions, 7 deletions
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; | |||
| 15 | mod lang; | 15 | mod lang; |
| 16 | mod openrouter; | 16 | mod openrouter; |
| 17 | mod tasks; | 17 | mod tasks; |
| 18 | mod tts; | ||
| 18 | mod voice; | 19 | mod voice; |
| 19 | 20 | ||
| 20 | use database::DbPool; | 21 | use database::DbPool; |
| @@ -26,9 +27,7 @@ const AC_SEARCH_CACHE_TTL: Duration = Duration::from_secs(10); | |||
| 26 | type AcCache = tokio::sync::Mutex<HashMap<(serenity::UserId, String), copyparty::SearchHit>>; | 27 | type AcCache = tokio::sync::Mutex<HashMap<(serenity::UserId, String), copyparty::SearchHit>>; |
| 27 | 28 | ||
| 28 | /// Shared autocomplete search cache: query → (hits, cached_at). | 29 | /// Shared autocomplete search cache: query → (hits, cached_at). |
| 29 | type AcSearchCache = Arc< | 30 | type AcSearchCache = Arc<tokio::sync::Mutex<HashMap<String, (Vec<copyparty::SearchHit>, Instant)>>>; |
| 30 | tokio::sync::Mutex<HashMap<String, (Vec<copyparty::SearchHit>, Instant)>>, | ||
| 31 | >; | ||
| 32 | 31 | ||
| 33 | pub struct Data { | 32 | pub struct Data { |
| 34 | pub db_pool: DbPool, | 33 | pub db_pool: DbPool, |
| @@ -84,10 +83,7 @@ impl Data { | |||
| 84 | 83 | ||
| 85 | /// Look up search cache for an autocomplete query. Returns `None` if | 84 | /// Look up search cache for an autocomplete query. Returns `None` if |
| 86 | /// uncached or stale. | 85 | /// uncached or stale. |
| 87 | pub async fn ac_search_cache_lookup( | 86 | pub async fn ac_search_cache_lookup(&self, query: &str) -> Option<Vec<copyparty::SearchHit>> { |
| 88 | &self, | ||
| 89 | query: &str, | ||
| 90 | ) -> Option<Vec<copyparty::SearchHit>> { | ||
| 91 | let cache = self.ac_search_cache.lock().await; | 87 | let cache = self.ac_search_cache.lock().await; |
| 92 | let (hits, cached_at) = cache.get(query)?; | 88 | let (hits, cached_at) = cache.get(query)?; |
| 93 | if cached_at.elapsed() > AC_SEARCH_CACHE_TTL { | 89 | if cached_at.elapsed() > AC_SEARCH_CACHE_TTL { |
| @@ -150,6 +146,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { | |||
| 150 | Ok(()) | 146 | Ok(()) |
| 151 | }) | 147 | }) |
| 152 | }, | 148 | }, |
| 149 | on_error: |error| { | ||
| 150 | Box::pin(async move { | ||
| 151 | match error { | ||
| 152 | poise::FrameworkError::Command { ctx, error, .. } => { | ||
| 153 | tracing::error!(command = %ctx.command().name, "Command failed: {error}"); | ||
| 154 | if let Err(e) = ctx | ||
| 155 | .send( | ||
| 156 | poise::CreateReply::default() | ||
| 157 | .content(format!("Błąd: {error}")) | ||
| 158 | .ephemeral(true), | ||
| 159 | ) | ||
| 160 | .await | ||
| 161 | { | ||
| 162 | tracing::debug!( | ||
| 163 | "Could not send error reply (interaction likely expired): {e}" | ||
| 164 | ); | ||
| 165 | } | ||
| 166 | } | ||
| 167 | other => { | ||
| 168 | if let Err(e) = poise::builtins::on_error(other).await { | ||
| 169 | tracing::warn!( | ||
| 170 | "Error while sending error message (interaction likely expired): {e}" | ||
| 171 | ); | ||
| 172 | } | ||
| 173 | } | ||
| 174 | } | ||
| 175 | }) | ||
| 176 | }, | ||
| 153 | ..Default::default() | 177 | ..Default::default() |
| 154 | }) | 178 | }) |
| 155 | .setup(move |ctx, _ready, framework| { | 179 | .setup(move |ctx, _ready, framework| { |
