use poise::serenity_prelude as serenity; use crate::Data; use crate::error::{Error, Result}; use crate::lang; use tracing::warn; type Context<'a> = poise::Context<'a, Data, Error>; #[derive(poise::ChoiceParameter)] pub enum OutputMode { #[name = "File"] File, #[name = "Voice Channel"] Vc, } /// Generate TTS (Piper locally, Google as fallback) and output to file or voice channel #[poise::command(slash_command)] pub async fn tts( ctx: Context<'_>, #[description = "Text to synthesize"] text: String, #[description = "Output destination (file or vc)"] output: Option, ) -> Result<()> { ctx.defer().await?; let output = output.unwrap_or(OutputMode::Vc); let (audio_data, ext) = crate::tts::synthesize(&ctx.data().config, &ctx.data().http_client, &text).await?; match output { OutputMode::File => { tokio::fs::create_dir_all(crate::config::TEMP_PATH) .await .ok(); let path = format!( "{}/tts_{}.{}", crate::config::TEMP_PATH, uuid::Uuid::new_v4(), ext ); tokio::fs::write(&path, &audio_data).await?; if let Err(e) = ctx .send( poise::CreateReply::default() .content(lang::L_TTS_SAVED) .attachment(serenity::CreateAttachment::path(&path).await.unwrap()), ) .await { warn!("Failed to send /tts file reply: {e}"); } } OutputMode::Vc => { let guild_id = ctx.guild_id().ok_or_else(lang::err_not_in_guild)?; let channel_id = ctx .serenity_context() .cache .guild(guild_id) .and_then(|g| { g.voice_states .get(&ctx.author().id) .and_then(|vs| vs.channel_id) }) .ok_or_else(lang::err_not_in_voice)?; let call = ctx .data() .voice_manager .connect(guild_id, channel_id) .await?; let input = crate::voice::decode_audio(&audio_data)?; let mut call_lock = call.lock().await; if call_lock.current_channel().is_some() { call_lock.play_input(input); } if let Err(e) = ctx.say(lang::L_TTS_PLAYING).await { warn!("Failed to send /tts voice reply: {e}"); } } } Ok(()) } pub fn commands() -> Vec> { vec![tts()] }