aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands/tts.rs
blob: f45d2e8b9297048df3f698692cde49fed517423e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 (local Piper) 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<OutputMode>,
) -> Result<()> {
    ctx.defer().await?;

    let output = output.unwrap_or(OutputMode::Vc);
    let audio_data = ctx.data().tts.synthesize(&text).await?;

    match output {
        OutputMode::File => {
            let wav = crate::tts::encode_wav(&audio_data, 48000, 2);

            tokio::fs::create_dir_all(crate::config::TEMP_PATH)
                .await
                .ok();
            let path = format!(
                "{}/tts_{}.wav",
                crate::config::TEMP_PATH,
                uuid::Uuid::new_v4(),
            );
            tokio::fs::write(&path, &wav).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::tts::pcm_bytes_to_input(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<poise::Command<Data, Error>> {
    vec![tts()]
}