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/events | |
| 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 'src/events')
| -rw-r--r-- | src/events/mod.rs | 79 |
1 files changed, 48 insertions, 31 deletions
diff --git a/src/events/mod.rs b/src/events/mod.rs index 82b6f19..22fe976 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs | |||
| @@ -28,32 +28,22 @@ fn format_duration(seconds: f64) -> String { | |||
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | async fn play_tts_in_channel( | 30 | async fn play_tts_in_channel( |
| 31 | _ctx: &serenity::Context, | 31 | config: crate::config::Config, |
| 32 | data: &Data, | 32 | http_client: reqwest::Client, |
| 33 | voice_manager: crate::voice::VoiceManager, | ||
| 33 | guild_id: serenity::GuildId, | 34 | guild_id: serenity::GuildId, |
| 34 | channel_id: serenity::ChannelId, | 35 | channel_id: serenity::ChannelId, |
| 35 | text: &str, | 36 | text: String, |
| 36 | ) { | 37 | ) { |
| 37 | let tts_url = format!( | 38 | let audio_data = match crate::tts::synthesize(&config, &http_client, &text).await { |
| 38 | "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl=pl&client=tw-ob", | 39 | Ok((bytes, _)) => bytes, |
| 39 | crate::commands::tts::urlencoding(text), | ||
| 40 | ); | ||
| 41 | |||
| 42 | let audio_data = match data.http_client.get(&tts_url).send().await { | ||
| 43 | Ok(resp) => match resp.bytes().await { | ||
| 44 | Ok(b) => b, | ||
| 45 | Err(e) => { | ||
| 46 | warn!("Failed to read TTS audio bytes: {e}"); | ||
| 47 | return; | ||
| 48 | } | ||
| 49 | }, | ||
| 50 | Err(e) => { | 40 | Err(e) => { |
| 51 | warn!("Failed to fetch TTS audio: {e}"); | 41 | warn!("Failed to synthesize TTS: {e}"); |
| 52 | return; | 42 | return; |
| 53 | } | 43 | } |
| 54 | }; | 44 | }; |
| 55 | 45 | ||
| 56 | let call = match data.voice_manager.connect(guild_id, channel_id).await { | 46 | let call = match voice_manager.connect(guild_id, channel_id).await { |
| 57 | Ok(c) => c, | 47 | Ok(c) => c, |
| 58 | Err(e) => { | 48 | Err(e) => { |
| 59 | warn!("Failed to connect to voice for TTS: {e}"); | 49 | warn!("Failed to connect to voice for TTS: {e}"); |
| @@ -93,9 +83,10 @@ pub async fn handle_voice_state_update( | |||
| 93 | .and_then(|gid| ctx.cache.guild(gid)) | 83 | .and_then(|gid| ctx.cache.guild(gid)) |
| 94 | .and_then(|g| g.members.get(&user_id).cloned()); | 84 | .and_then(|g| g.members.get(&user_id).cloned()); |
| 95 | if let Some(ref m) = member | 85 | if let Some(ref m) = member |
| 96 | && m.user.bot { | 86 | && m.user.bot |
| 97 | return Ok(()); | 87 | { |
| 98 | } | 88 | return Ok(()); |
| 89 | } | ||
| 99 | 90 | ||
| 100 | let member_name = member | 91 | let member_name = member |
| 101 | .as_ref() | 92 | .as_ref() |
| @@ -130,7 +121,9 @@ pub async fn handle_voice_state_update( | |||
| 130 | let mut tts_channel: Option<serenity::ChannelId> = None; | 121 | let mut tts_channel: Option<serenity::ChannelId> = None; |
| 131 | 122 | ||
| 132 | // ── JOIN ── | 123 | // ── JOIN ── |
| 133 | if old_channel.is_none() && let Some(cid) = new_channel { | 124 | if old_channel.is_none() |
| 125 | && let Some(cid) = new_channel | ||
| 126 | { | ||
| 134 | let is_afk = new.self_mute || new.self_deaf; | 127 | let is_afk = new.self_mute || new.self_deaf; |
| 135 | 128 | ||
| 136 | va.join_time = now; | 129 | va.join_time = now; |
| @@ -274,9 +267,17 @@ pub async fn handle_voice_state_update( | |||
| 274 | lang::fmt_ev_undeafened(&display_name) | 267 | lang::fmt_ev_undeafened(&display_name) |
| 275 | }; | 268 | }; |
| 276 | if let Some(gid) = guild_id | 269 | if let Some(gid) = guild_id |
| 277 | && let Some(cid) = tts_channel.or(new_channel) { | 270 | && let Some(cid) = tts_channel.or(new_channel) |
| 278 | play_tts_in_channel(ctx, data, gid, cid, &phrase).await; | 271 | { |
| 279 | } | 272 | tokio::spawn(play_tts_in_channel( |
| 273 | data.config.clone(), | ||
| 274 | data.http_client.clone(), | ||
| 275 | data.voice_manager.clone(), | ||
| 276 | gid, | ||
| 277 | cid, | ||
| 278 | phrase, | ||
| 279 | )); | ||
| 280 | } | ||
| 280 | return Ok(()); | 281 | return Ok(()); |
| 281 | } | 282 | } |
| 282 | 283 | ||
| @@ -287,17 +288,33 @@ pub async fn handle_voice_state_update( | |||
| 287 | lang::fmt_ev_unmuted(&display_name) | 288 | lang::fmt_ev_unmuted(&display_name) |
| 288 | }; | 289 | }; |
| 289 | if let Some(gid) = guild_id | 290 | if let Some(gid) = guild_id |
| 290 | && let Some(cid) = tts_channel.or(new_channel) { | 291 | && let Some(cid) = tts_channel.or(new_channel) |
| 291 | play_tts_in_channel(ctx, data, gid, cid, &phrase).await; | 292 | { |
| 292 | } | 293 | tokio::spawn(play_tts_in_channel( |
| 294 | data.config.clone(), | ||
| 295 | data.http_client.clone(), | ||
| 296 | data.voice_manager.clone(), | ||
| 297 | gid, | ||
| 298 | cid, | ||
| 299 | phrase, | ||
| 300 | )); | ||
| 301 | } | ||
| 293 | return Ok(()); | 302 | return Ok(()); |
| 294 | } | 303 | } |
| 295 | 304 | ||
| 296 | // ── Play join/leave TTS if no mute/deafen change ── | 305 | // ── Play join/leave TTS if no mute/deafen change ── |
| 297 | if let (Some(text), Some(cid)) = (tts_text, tts_channel) | 306 | if let (Some(text), Some(cid)) = (tts_text, tts_channel) |
| 298 | && let Some(gid) = guild_id { | 307 | && let Some(gid) = guild_id |
| 299 | play_tts_in_channel(ctx, data, gid, cid, &text).await; | 308 | { |
| 300 | } | 309 | tokio::spawn(play_tts_in_channel( |
| 310 | data.config.clone(), | ||
| 311 | data.http_client.clone(), | ||
| 312 | data.voice_manager.clone(), | ||
| 313 | gid, | ||
| 314 | cid, | ||
| 315 | text, | ||
| 316 | )); | ||
| 317 | } | ||
| 301 | 318 | ||
| 302 | Ok(()) | 319 | Ok(()) |
| 303 | } | 320 | } |
