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/events/mod.rs | 79 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 31 deletions(-) (limited to 'src/events') 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 { } async fn play_tts_in_channel( - _ctx: &serenity::Context, - data: &Data, + config: crate::config::Config, + http_client: reqwest::Client, + voice_manager: crate::voice::VoiceManager, guild_id: serenity::GuildId, channel_id: serenity::ChannelId, - text: &str, + text: String, ) { - let tts_url = format!( - "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl=pl&client=tw-ob", - crate::commands::tts::urlencoding(text), - ); - - let audio_data = match data.http_client.get(&tts_url).send().await { - Ok(resp) => match resp.bytes().await { - Ok(b) => b, - Err(e) => { - warn!("Failed to read TTS audio bytes: {e}"); - return; - } - }, + let audio_data = match crate::tts::synthesize(&config, &http_client, &text).await { + Ok((bytes, _)) => bytes, Err(e) => { - warn!("Failed to fetch TTS audio: {e}"); + warn!("Failed to synthesize TTS: {e}"); return; } }; - let call = match data.voice_manager.connect(guild_id, channel_id).await { + let call = match voice_manager.connect(guild_id, channel_id).await { Ok(c) => c, Err(e) => { warn!("Failed to connect to voice for TTS: {e}"); @@ -93,9 +83,10 @@ pub async fn handle_voice_state_update( .and_then(|gid| ctx.cache.guild(gid)) .and_then(|g| g.members.get(&user_id).cloned()); if let Some(ref m) = member - && m.user.bot { - return Ok(()); - } + && m.user.bot + { + return Ok(()); + } let member_name = member .as_ref() @@ -130,7 +121,9 @@ pub async fn handle_voice_state_update( let mut tts_channel: Option = None; // ── JOIN ── - if old_channel.is_none() && let Some(cid) = new_channel { + if old_channel.is_none() + && let Some(cid) = new_channel + { let is_afk = new.self_mute || new.self_deaf; va.join_time = now; @@ -274,9 +267,17 @@ pub async fn handle_voice_state_update( lang::fmt_ev_undeafened(&display_name) }; if let Some(gid) = guild_id - && let Some(cid) = tts_channel.or(new_channel) { - play_tts_in_channel(ctx, data, gid, cid, &phrase).await; - } + && let Some(cid) = tts_channel.or(new_channel) + { + tokio::spawn(play_tts_in_channel( + data.config.clone(), + data.http_client.clone(), + data.voice_manager.clone(), + gid, + cid, + phrase, + )); + } return Ok(()); } @@ -287,17 +288,33 @@ pub async fn handle_voice_state_update( lang::fmt_ev_unmuted(&display_name) }; if let Some(gid) = guild_id - && let Some(cid) = tts_channel.or(new_channel) { - play_tts_in_channel(ctx, data, gid, cid, &phrase).await; - } + && let Some(cid) = tts_channel.or(new_channel) + { + tokio::spawn(play_tts_in_channel( + data.config.clone(), + data.http_client.clone(), + data.voice_manager.clone(), + gid, + cid, + phrase, + )); + } return Ok(()); } // ── Play join/leave TTS if no mute/deafen change ── if let (Some(text), Some(cid)) = (tts_text, tts_channel) - && let Some(gid) = guild_id { - play_tts_in_channel(ctx, data, gid, cid, &text).await; - } + && let Some(gid) = guild_id + { + tokio::spawn(play_tts_in_channel( + data.config.clone(), + data.http_client.clone(), + data.voice_manager.clone(), + gid, + cid, + text, + )); + } Ok(()) } -- cgit v1.2.3