mirror of
https://github.com/mii443/ncb-tts-r2.git
synced 2025-08-22 16:15:29 +00:00
init
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
Cargo.lock
|
||||
/target
|
||||
config.toml
|
||||
credentials.json
|
||||
/audio
|
||||
*.mp3
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ncb-tts-r2.iml" filepath="$PROJECT_DIR$/.idea/ncb-tts-r2.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
11
.idea/ncb-tts-r2.iml
generated
Normal file
11
.idea/ncb-tts-r2.iml
generated
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="CPP_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "ncb-tts-r2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = "1.0"
|
||||
toml = "0.5"
|
||||
gcp_auth = "0.5.0"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
base64 = "0.13"
|
||||
async-trait = "0.1.57"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "0.8"
|
||||
features = ["serde", "v4"]
|
||||
|
||||
[dependencies.songbird]
|
||||
version = "0.2.0"
|
||||
features = ["builtin-queue"]
|
||||
|
||||
[dependencies.serenity]
|
||||
version = "0.10.9"
|
||||
features = ["builder", "cache", "client", "gateway", "model", "utils", "unstable_discord_api", "collector", "rustls_backend", "framework", "voice"]
|
||||
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.0"
|
||||
features = ["macros", "rt-multi-thread"]
|
8
src/config.rs
Normal file
8
src/config.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub prefix: String,
|
||||
pub token: String,
|
||||
pub application_id: u64
|
||||
}
|
19
src/data.rs
Normal file
19
src/data.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::tts::gcp_tts::gcp_tts::TTS;
|
||||
use serenity::{prelude::{TypeMapKey, RwLock}, model::id::GuildId, futures::lock::Mutex};
|
||||
|
||||
use crate::tts::instance::TTSInstance;
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
/// TTSInstance data
|
||||
pub struct TTSData;
|
||||
|
||||
impl TypeMapKey for TTSData {
|
||||
type Value = Arc<RwLock<HashMap<GuildId, TTSInstance>>>;
|
||||
}
|
||||
|
||||
/// TTS client data
|
||||
pub struct TTSClientData;
|
||||
|
||||
impl TypeMapKey for TTSClientData {
|
||||
type Value = Arc<Mutex<TTS>>;
|
||||
}
|
0
src/database/mod.rs
Normal file
0
src/database/mod.rs
Normal file
128
src/event_handler.rs
Normal file
128
src/event_handler.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use serenity::{client::{EventHandler, Context}, async_trait, model::{gateway::Ready, interactions::{Interaction, application_command::ApplicationCommandInteraction, InteractionApplicationCommandCallbackDataFlags}, id::{GuildId, UserId}, channel::Message}, framework::standard::macros::group};
|
||||
use crate::{data::TTSData, tts::instance::TTSInstance};
|
||||
|
||||
#[group]
|
||||
struct Test;
|
||||
|
||||
pub struct Handler;
|
||||
|
||||
async fn setup_command(ctx: &Context, command: &ApplicationCommandInteraction) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let None = command.guild_id {
|
||||
command.create_interaction_response(&ctx.http, |f| {
|
||||
f.interaction_response_data(|d| {
|
||||
d.content("このコマンドはサーバーでのみ使用可能です.").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let guild = command.guild_id.unwrap().to_guild_cached(&ctx.cache).await;
|
||||
if let None = guild {
|
||||
command.create_interaction_response(&ctx.http, |f| {
|
||||
f.interaction_response_data(|d| {
|
||||
d.content("ギルドキャッシュを取得できませんでした.").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let guild = guild.unwrap();
|
||||
|
||||
let channel_id = guild
|
||||
.voice_states
|
||||
.get(&UserId(command.user.id.0))
|
||||
.and_then(|state| state.channel_id);
|
||||
|
||||
if let None = channel_id {
|
||||
command.create_interaction_response(&ctx.http, |f| {
|
||||
f.interaction_response_data(|d| {
|
||||
d.content("ボイスチャンネルに参加してから実行してください.").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let channel_id = channel_id.unwrap();
|
||||
|
||||
let manager = songbird::get(ctx).await.expect("Cannot get songbird client.").clone();
|
||||
|
||||
let storage_lock = {
|
||||
let data_read = ctx.data.read().await;
|
||||
data_read.get::<TTSData>().expect("Cannot get TTSStorage").clone()
|
||||
};
|
||||
|
||||
{
|
||||
let mut storage = storage_lock.write().await;
|
||||
if storage.contains_key(&guild.id) {
|
||||
command.create_interaction_response(&ctx.http, |f| {
|
||||
f.interaction_response_data(|d| {
|
||||
d.content("すでにセットアップしています.").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
storage.insert(guild.id, TTSInstance {
|
||||
before_message: None,
|
||||
guild: guild.id,
|
||||
text_channel: command.channel_id,
|
||||
voice_channel: channel_id
|
||||
});
|
||||
}
|
||||
|
||||
let _handler = manager.join(guild.id.0, channel_id.0).await;
|
||||
|
||||
command.create_interaction_response(&ctx.http, |f| {
|
||||
f.interaction_response_data(|d| {
|
||||
d.content("セットアップ完了").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
||||
})
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
if let Interaction::ApplicationCommand(command) = interaction {
|
||||
let name = &*command.data.name;
|
||||
match name {
|
||||
"setup" => setup_command(&ctx, &command).await.unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn message(&self, ctx: Context, message: Message) {
|
||||
let guild_id = message.guild(&ctx.cache).await.unwrap().id;
|
||||
|
||||
let storage_lock = {
|
||||
let data_read = ctx.data.read().await;
|
||||
data_read.get::<TTSData>().expect("Cannot get TTSStorage").clone()
|
||||
};
|
||||
|
||||
{
|
||||
let mut storage = storage_lock.write().await;
|
||||
if !storage.contains_key(&guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let instance = storage.get_mut(&guild_id).unwrap();
|
||||
|
||||
instance.read(message, &ctx).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||
println!("{} is connected!", ready.user.name);
|
||||
|
||||
let guild_id = GuildId(696782998799909024);
|
||||
|
||||
let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| {
|
||||
commands.create_application_command(|command| {
|
||||
command.name("setup")
|
||||
.description("Setup tts")
|
||||
})
|
||||
}).await;
|
||||
println!("{:?}", commands);
|
||||
}
|
||||
}
|
70
src/implement/message.rs
Normal file
70
src/implement/message.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use std::{path::Path, fs::File, io::Write};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serenity::{prelude::Context, model::prelude::Message};
|
||||
|
||||
use crate::{
|
||||
data::TTSClientData,
|
||||
tts::{
|
||||
instance::TTSInstance,
|
||||
message::TTSMessage,
|
||||
gcp_tts::structs::{
|
||||
audio_config::AudioConfig, voice_selection_params::VoiceSelectionParams, synthesis_input::SynthesisInput, synthesize_request::SynthesizeRequest
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl TTSMessage for Message {
|
||||
async fn parse(&self, instance: &mut TTSInstance, _: &Context) -> String {
|
||||
let res = if let Some(before_message) = &instance.before_message {
|
||||
if before_message.author.id == self.author.id {
|
||||
self.content.clone()
|
||||
} else {
|
||||
format!("<speak>{} さんの発言<break time=\"200ms\"/>{}</speak>", self.author.name, self.content)
|
||||
}
|
||||
} else {
|
||||
self.content.clone()
|
||||
};
|
||||
|
||||
instance.before_message = Some(self.clone());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn synthesize(&self, instance: &mut TTSInstance, ctx: &Context) -> String {
|
||||
let text = self.parse(instance, ctx).await;
|
||||
|
||||
let data_read = ctx.data.read().await;
|
||||
let storage = data_read.get::<TTSClientData>().expect("Cannot get TTSClientStorage").clone();
|
||||
let storage = storage.lock().await;
|
||||
|
||||
let audio = storage.synthesize(SynthesizeRequest {
|
||||
input: SynthesisInput {
|
||||
text: None,
|
||||
ssml: Some(text)
|
||||
},
|
||||
voice: VoiceSelectionParams {
|
||||
languageCode: String::from("ja-JP"),
|
||||
name: String::from("ja-JP-Wavenet-B"),
|
||||
ssmlGender: String::from("neutral")
|
||||
},
|
||||
audioConfig: AudioConfig {
|
||||
audioEncoding: String::from("mp3"),
|
||||
speakingRate: 1.2f32,
|
||||
pitch: 1.0f32
|
||||
}
|
||||
}).await.unwrap();
|
||||
|
||||
let uuid = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let root = option_env!("CARGO_MANIFEST_DIR").unwrap();
|
||||
let path = Path::new(root);
|
||||
let file_path = path.join("audio").join(format!("{}.mp3", uuid));
|
||||
|
||||
let mut file = File::create(file_path.clone()).unwrap();
|
||||
file.write(&audio).unwrap();
|
||||
|
||||
file_path.into_os_string().into_string().unwrap()
|
||||
}
|
||||
}
|
1
src/implement/mod.rs
Normal file
1
src/implement/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod message;
|
69
src/main.rs
Normal file
69
src/main.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use config::Config;
|
||||
use data::{TTSData, TTSClientData};
|
||||
use event_handler::Handler;
|
||||
use tts::gcp_tts::gcp_tts::TTS;
|
||||
use serenity::{
|
||||
client::{Client, bridge::gateway::GatewayIntents},
|
||||
framework::StandardFramework, prelude::RwLock, futures::lock::Mutex
|
||||
};
|
||||
|
||||
use songbird::SerenityInit;
|
||||
|
||||
mod config;
|
||||
mod event_handler;
|
||||
mod tts;
|
||||
mod implement;
|
||||
mod data;
|
||||
|
||||
/// Create discord client
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// let client = create_client("!", "BOT_TOKEN", 123456789123456789).await;
|
||||
///
|
||||
/// client.start().await;
|
||||
/// ```
|
||||
async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, serenity::Error> {
|
||||
let framework = StandardFramework::new()
|
||||
.configure(|c| c
|
||||
.with_whitespace(true)
|
||||
.prefix(prefix));
|
||||
|
||||
Client::builder(token)
|
||||
.event_handler(Handler)
|
||||
.application_id(id)
|
||||
.framework(framework)
|
||||
.intents(GatewayIntents::all())
|
||||
.register_songbird()
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load config
|
||||
let config = std::fs::read_to_string("./config.toml").expect("Cannot read config file.");
|
||||
let config: Config = toml::from_str(&config).expect("Cannot load config file.");
|
||||
|
||||
// Create discord client
|
||||
let mut client = create_client(&config.prefix, &config.token, config.application_id).await.expect("Err creating client");
|
||||
|
||||
// Create TTS client
|
||||
let tts = match TTS::new("./credentials.json".to_string()).await {
|
||||
Ok(tts) => tts,
|
||||
Err(err) => panic!("{}", err)
|
||||
};
|
||||
|
||||
// Create TTS storage
|
||||
{
|
||||
let mut data = client.data.write().await;
|
||||
data.insert::<TTSData>(Arc::new(RwLock::new(HashMap::default())));
|
||||
data.insert::<TTSClientData>(Arc::new(Mutex::new(tts)));
|
||||
}
|
||||
|
||||
// Run client
|
||||
if let Err(why) = client.start().await {
|
||||
println!("Client error: {:?}", why);
|
||||
}
|
||||
}
|
3
src/tts/config.rs
Normal file
3
src/tts/config.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub struct TTSConfig {
|
||||
|
||||
}
|
57
src/tts/gcp_tts/gcp_tts.rs
Normal file
57
src/tts/gcp_tts/gcp_tts.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use gcp_auth::Token;
|
||||
use crate::tts::gcp_tts::structs::{
|
||||
synthesize_request::SynthesizeRequest,
|
||||
synthesize_response::SynthesizeResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TTS {
|
||||
pub token: Token
|
||||
}
|
||||
|
||||
impl TTS {
|
||||
pub async fn new(credentials_path: String) -> Result<TTS, gcp_auth::Error> {
|
||||
let authenticator = gcp_auth::from_credentials_file(credentials_path).await?;
|
||||
let token = authenticator.get_token(&["https://www.googleapis.com/auth/cloud-platform"]).await?;
|
||||
|
||||
Ok(TTS {
|
||||
token
|
||||
})
|
||||
}
|
||||
|
||||
/// Synthesize text to speech and return the audio data.
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// let audio = storage.synthesize(SynthesizeRequest {
|
||||
/// input: SynthesisInput {
|
||||
/// text: None,
|
||||
/// ssml: Some(String::from("<speak>test</speak>"))
|
||||
/// },
|
||||
/// voice: VoiceSelectionParams {
|
||||
/// languageCode: String::from("ja-JP"),
|
||||
/// name: String::from("ja-JP-Wavenet-B"),
|
||||
/// ssmlGender: String::from("neutral")
|
||||
/// },
|
||||
/// audioConfig: AudioConfig {
|
||||
/// audioEncoding: String::from("mp3"),
|
||||
/// speakingRate: 1.2f32,
|
||||
/// pitch: 1.0f32
|
||||
/// }
|
||||
/// }).await.unwrap();
|
||||
/// ```
|
||||
pub async fn synthesize(&self, request: SynthesizeRequest) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
match client.post("https://texttospeech.googleapis.com/v1/text:synthesize")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", self.token.as_str()))
|
||||
.body(serde_json::to_string(&request).unwrap())
|
||||
.send().await {
|
||||
Ok(ok) => {
|
||||
let response: SynthesizeResponse = serde_json::from_str(&ok.text().await.expect("")).unwrap();
|
||||
Ok(base64::decode(response.audioContent).unwrap()[..].to_vec())
|
||||
},
|
||||
Err(err) => Err(Box::new(err))
|
||||
}
|
||||
}
|
||||
}
|
2
src/tts/gcp_tts/mod.rs
Normal file
2
src/tts/gcp_tts/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod gcp_tts;
|
||||
pub mod structs;
|
17
src/tts/gcp_tts/structs/audio_config.rs
Normal file
17
src/tts/gcp_tts/structs/audio_config.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// AudioConfig {
|
||||
/// audioEncoding: String::from("mp3"),
|
||||
/// speakingRate: 1.2f32,
|
||||
/// pitch: 1.0f32
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct AudioConfig {
|
||||
pub audioEncoding: String,
|
||||
pub speakingRate: f32,
|
||||
pub pitch: f32
|
||||
}
|
5
src/tts/gcp_tts/structs/mod.rs
Normal file
5
src/tts/gcp_tts/structs/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod audio_config;
|
||||
pub mod synthesis_input;
|
||||
pub mod synthesize_request;
|
||||
pub mod voice_selection_params;
|
||||
pub mod synthesize_response;
|
14
src/tts/gcp_tts/structs/synthesis_input.rs
Normal file
14
src/tts/gcp_tts/structs/synthesis_input.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// SynthesisInput {
|
||||
/// text: None,
|
||||
/// ssml: Some(String::from("<speak>test</speak>"))
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SynthesisInput {
|
||||
pub text: Option<String>,
|
||||
pub ssml: Option<String>
|
||||
}
|
33
src/tts/gcp_tts/structs/synthesize_request.rs
Normal file
33
src/tts/gcp_tts/structs/synthesize_request.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::tts::gcp_tts::structs::{
|
||||
synthesis_input::SynthesisInput,
|
||||
audio_config::AudioConfig,
|
||||
voice_selection_params::VoiceSelectionParams,
|
||||
};
|
||||
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// SynthesizeRequest {
|
||||
/// input: SynthesisInput {
|
||||
/// text: None,
|
||||
/// ssml: Some(String::from("<speak>test</speak>"))
|
||||
/// },
|
||||
/// voice: VoiceSelectionParams {
|
||||
/// languageCode: String::from("ja-JP"),
|
||||
/// name: String::from("ja-JP-Wavenet-B"),
|
||||
/// ssmlGender: String::from("neutral")
|
||||
/// },
|
||||
/// audioConfig: AudioConfig {
|
||||
/// audioEncoding: String::from("mp3"),
|
||||
/// speakingRate: 1.2f32,
|
||||
/// pitch: 1.0f32
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct SynthesizeRequest {
|
||||
pub input: SynthesisInput,
|
||||
pub voice: VoiceSelectionParams,
|
||||
pub audioConfig: AudioConfig,
|
||||
}
|
7
src/tts/gcp_tts/structs/synthesize_response.rs
Normal file
7
src/tts/gcp_tts/structs/synthesize_response.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct SynthesizeResponse {
|
||||
pub audioContent: String
|
||||
}
|
17
src/tts/gcp_tts/structs/voice_selection_params.rs
Normal file
17
src/tts/gcp_tts/structs/voice_selection_params.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// VoiceSelectionParams {
|
||||
/// languageCode: String::from("ja-JP"),
|
||||
/// name: String::from("ja-JP-Wavenet-B"),
|
||||
/// ssmlGender: String::from("neutral")
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct VoiceSelectionParams {
|
||||
pub languageCode: String,
|
||||
pub name: String,
|
||||
pub ssmlGender: String
|
||||
}
|
33
src/tts/instance.rs
Normal file
33
src/tts/instance.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use serenity::{model::{channel::Message, id::{ChannelId, GuildId}}, prelude::Context};
|
||||
|
||||
use crate::{tts::message::TTSMessage};
|
||||
|
||||
pub struct TTSInstance {
|
||||
pub before_message: Option<Message>,
|
||||
pub text_channel: ChannelId,
|
||||
pub voice_channel: ChannelId,
|
||||
pub guild: GuildId
|
||||
}
|
||||
|
||||
impl TTSInstance {
|
||||
|
||||
/// Synthesize text to speech and send it to the voice channel.
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// instance.read(message, &ctx).await;
|
||||
/// ```
|
||||
pub async fn read<T>(&mut self, message: T, ctx: &Context)
|
||||
where T: TTSMessage
|
||||
{
|
||||
let path = message.synthesize(self, ctx).await;
|
||||
|
||||
{
|
||||
let manager = songbird::get(&ctx).await.unwrap();
|
||||
let call = manager.get(self.guild).unwrap();
|
||||
let mut call = call.lock().await;
|
||||
let input = songbird::input::ffmpeg(path).await.expect("File not found.");
|
||||
call.enqueue_source(input);
|
||||
}
|
||||
}
|
||||
}
|
25
src/tts/message.rs
Normal file
25
src/tts/message.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use async_trait::async_trait;
|
||||
use serenity::prelude::Context;
|
||||
|
||||
use crate::tts::instance::TTSInstance;
|
||||
|
||||
/// Message trait that can be used to synthesize text to speech.
|
||||
#[async_trait]
|
||||
pub trait TTSMessage {
|
||||
|
||||
/// Parse the message for synthesis.
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// let text = message.parse(instance, ctx).await;
|
||||
/// ```
|
||||
async fn parse(&self, instance: &mut TTSInstance, ctx: &Context) -> String;
|
||||
|
||||
/// Synthesize the message and returns the path to the audio file.
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// let path = message.synthesize(instance, ctx).await;
|
||||
/// ```
|
||||
async fn synthesize(&self, instance: &mut TTSInstance, ctx: &Context) -> String;
|
||||
}
|
6
src/tts/mod.rs
Normal file
6
src/tts/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod gcp_tts;
|
||||
pub mod voicevox;
|
||||
pub mod config;
|
||||
pub mod message;
|
||||
pub mod tts_type;
|
||||
pub mod instance;
|
4
src/tts/tts_type.rs
Normal file
4
src/tts/tts_type.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub enum TTSType {
|
||||
GCP,
|
||||
VOICEVOX
|
||||
}
|
0
src/tts/voicevox/mod.rs
Normal file
0
src/tts/voicevox/mod.rs
Normal file
Reference in New Issue
Block a user