aboutsummaryrefslogtreecommitdiffstats
path: root/src/events/mod.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-18 23:18:37 +0200
committersalmonik <salmonik@tuta.io>2026-08-18 23:18:37 +0200
commitb252c39517b9fce181d1a0fe4761404c7d981397 (patch)
tree564cf01aed601f1974e0de8801d4452215ec05f5 /src/events/mod.rs
parentfir auto completion and send file in the response body upon grajcuj file (diff)
downloadsenator-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/events/mod.rs79
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
30async fn play_tts_in_channel( 30async 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}