24 Commits

Author SHA1 Message Date
mii
b7a4da7f3e thread tts 2022-11-18 09:08:27 +00:00
mii
708b6fc429 fix getting guild id 2022-11-03 05:13:05 +00:00
mii
f9fd0686a7 add sample docker-compose.yml 2022-11-02 15:07:36 +00:00
mii
99d8ef9bef voice state bugfix 2022-11-02 15:04:18 +00:00
mii
af01576a99 update serenity and songbird 2022-11-02 14:38:56 +00:00
mii
ddab474d67 support env variable 2022-10-31 13:02:20 +00:00
mii
d10bfcc333 fix database, borrow 2022-10-31 12:46:59 +00:00
mii
1470612d8b clippy 2022-10-31 12:40:55 +00:00
mii
065717839b reading attachment files, fix database 2022-10-31 21:04:11 +09:00
mii
6dafc66878 . 2022-08-14 12:02:52 +09:00
mii
1789bd7c4e fix config change message, auto disconnect 2022-08-13 16:48:47 +09:00
mii
0b93c23e91 auto leave 2022-08-12 23:42:23 +09:00
mii
7fe65bc397 add validator 2022-08-12 23:07:20 +09:00
mii
51c39036c6 delete debug register 2022-08-12 22:43:21 +09:00
mii
c52429bce0 add config command 2022-08-12 22:39:12 +09:00
mii
5ca5325fbd refactoring 2022-08-12 20:25:39 +09:00
mii
4161acbd45 fix warn 2022-08-12 20:05:34 +09:00
mii
47b11262e2 refactoring 2022-08-12 19:05:02 +09:00
mii
b36bee8be8 fix database connection bug 2022-08-12 17:57:28 +09:00
mii
6aec4e4ea7 fix dockerfile 2022-08-12 00:50:12 +09:00
mii
2bf2fe05f1 dockerfile 2022-08-12 00:40:14 +09:00
mii
f99d37ea56 fix slash commands register 2022-08-12 00:25:52 +09:00
mii
3652079bab fix audio path 2022-08-12 00:00:03 +09:00
mii
8a4de65a8a fix audio path 2022-08-11 23:59:52 +09:00
42 changed files with 958 additions and 458 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ Cargo.lock
config.toml
credentials.json
/audio
*.mp3
*.mp3
*.swp

View File

@ -14,20 +14,21 @@ reqwest = { version = "0.11", features = ["json"] }
base64 = "0.13"
async-trait = "0.1.57"
redis = "*"
regex = "1"
[dependencies.uuid]
version = "0.8"
features = ["serde", "v4"]
[dependencies.songbird]
version = "0.2.0"
version = "0.3.0"
features = ["builtin-queue"]
[dependencies.serenity]
version = "0.10.9"
version = "0.11.5"
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"]
features = ["macros", "rt-multi-thread"]

View File

@ -1,16 +1,18 @@
FROM ubuntu:22.04
WORKDIR /usr/src/ncb-tts-r2
ENV PATH $PATH:/root/.cargo/bin/
RUN apt-get update \
&& apt-get install -y ffmpeg libssl-dev pkg-config libopus-dev wget curl gcc \
&& apt-get -y clean \
&& rm -rf /var/lib/apt/lists/*
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
ENV PATH $PATH:/root/.cargo/bin/
RUN rustup install stable
WORKDIR /usr/src/ncb-tts-r2
&& rm -rf /var/lib/apt/lists/* \
&& curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable \
&& rustup install stable
COPY Cargo.toml .
COPY src src
RUN cargo build --release \
&& cp /usr/src/ncb-tts-r2/target/release/ncb-tts-r2 /usr/bin/ncb-tts-r2 \
&& mkdir -p /ncb-tts-r2/audio
&& mkdir -p /ncb-tts-r2/audio \
&& apt-get purge -y pkg-config wget curl gcc \
&& rustup self uninstall -y
WORKDIR /ncb-tts-r2
CMD ["ncb-tts-r2"]
CMD ["ncb-tts-r2"]

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
version: '3'
services:
ncb-tts-r2:
container_name: ncb-tts-r2
image: ghcr.io/morioka22/ncb-tts-r2:1.1.2
environment:
- NCB_TOKEN=YOUR_BOT_TOKEN
- NCB_APP_ID=YOUR_BOT_ID
- NCB_PREFIX=BOT_PREFIX
- NCB_REDIS_URL=redis://<REDIS_IP>/
- NCB_VOICEVOX_KEY=VOICEVOX_KEY
volumes:
- ./credentials.json:/ncb-tts-r2/credentials.json:ro

88
src/commands/config.rs Normal file
View File

@ -0,0 +1,88 @@
use serenity::{
model::prelude::interaction::{
application_command::ApplicationCommandInteraction, MessageFlags,
},
prelude::Context,
};
use crate::{
data::DatabaseClientData,
tts::{tts_type::TTSType, voicevox::voicevox::VOICEVOX},
};
pub async fn config_command(
ctx: &Context,
command: &ApplicationCommandInteraction,
) -> Result<(), Box<dyn std::error::Error>> {
let data_read = ctx.data.read().await;
let config = {
let database = data_read
.get::<DatabaseClientData>()
.expect("Cannot get DatabaseClientData")
.clone();
let mut database = database.lock().await;
database
.get_user_config_or_default(command.user.id.0)
.await
.unwrap()
.unwrap()
};
let voicevox_speaker = config.voicevox_speaker.unwrap_or(1);
let tts_type = config.tts_type.unwrap_or(TTSType::GCP);
command
.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("読み上げ設定")
.components(|c| {
c.create_action_row(|a| {
a.create_select_menu(|m| {
m.custom_id("TTS_CONFIG_ENGINE")
.options(|o| {
o.create_option(|co| {
co.label("Google TTS")
.value("TTS_CONFIG_ENGINE_SELECTED_GOOGLE")
.default_selection(tts_type == TTSType::GCP)
})
.create_option(
|co| {
co.label("VOICEVOX")
.value("TTS_CONFIG_ENGINE_SELECTED_VOICEVOX")
.default_selection(
tts_type == TTSType::VOICEVOX,
)
},
)
})
.placeholder("読み上げAPIを選択")
})
})
.create_action_row(|a| {
a.create_select_menu(|m| {
m.custom_id("TTS_CONFIG_VOICEVOX_SPEAKER")
.options(|o| {
let mut o = o;
for (name, value) in VOICEVOX::get_speakers() {
o = o.create_option(|co| {
co.label(name)
.value(format!(
"TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_{}",
value
))
.default_selection(value == voicevox_speaker)
})
}
o
})
.placeholder("VOICEVOX Speakerを指定")
})
})
})
.flags(MessageFlags::EPHEMERAL)
})
})
.await?;
Ok(())
}

3
src/commands/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod config;
pub mod setup;
pub mod stop;

126
src/commands/setup.rs Normal file
View File

@ -0,0 +1,126 @@
use serenity::{
model::prelude::{
interaction::{application_command::ApplicationCommandInteraction, MessageFlags},
UserId,
},
prelude::Context,
};
use crate::{data::TTSData, tts::instance::TTSInstance};
pub 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(MessageFlags::EPHEMERAL)
})
})
.await?;
return Ok(());
}
let guild = command.guild_id.unwrap().to_guild_cached(&ctx.cache);
if let None = guild {
command
.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("ギルドキャッシュを取得できませんでした.")
.flags(MessageFlags::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(MessageFlags::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 thread_id = {
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(MessageFlags::EPHEMERAL)
})
})
.await?;
return Ok(());
}
let message = command
.channel_id
.send_message(&ctx.http, |f| f.content("TTS thread"))
.await
.unwrap();
let thread_id = command
.channel_id
.create_public_thread(&ctx.http, message, |f| {
f.name("TTS").auto_archive_duration(60)
})
.await
.unwrap();
storage.insert(
guild.id,
TTSInstance {
before_message: None,
guild: guild.id,
text_channel: thread_id.id,
voice_channel: channel_id,
},
);
thread_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("."))
})
.await?;
thread_id.send_message(&ctx.http, |f| f.embed(|e| e.title("読み上げ (Serenity)")
.field("クレジット", "```\n四国めたん  ずんだもん\n春日部つむぎ 雨晴はう\n波音リツ   玄野武宏\n白上虎太郎  青山龍星\n冥鳴ひまり  九州そら\nモチノ・キョウコ```", false)
.field("設定コマンド", "`/config`", false)
)).await?;
Ok(())
}

97
src/commands/stop.rs Normal file
View File

@ -0,0 +1,97 @@
use serenity::{
model::prelude::{
interaction::{application_command::ApplicationCommandInteraction, MessageFlags},
UserId,
},
prelude::Context,
};
use crate::data::TTSData;
pub async fn stop_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(MessageFlags::EPHEMERAL)
})
})
.await?;
return Ok(());
}
let guild = command.guild_id.unwrap().to_guild_cached(&ctx.cache);
if let None = guild {
command
.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("ギルドキャッシュを取得できませんでした.")
.flags(MessageFlags::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(MessageFlags::EPHEMERAL)
})
})
.await?;
return Ok(());
}
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(MessageFlags::EPHEMERAL)
})
})
.await?;
return Ok(());
}
storage.remove(&guild.id);
}
let _handler = manager.remove(guild.id.0).await;
command
.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| d.content("停止しました"))
})
.await?;
Ok(())
}

View File

@ -6,5 +6,5 @@ pub struct Config {
pub token: String,
pub application_id: u64,
pub redis_url: String,
pub voicevox_key: String
}
pub voicevox_key: String,
}

View File

@ -1,8 +1,15 @@
use crate::{tts::{gcp_tts::gcp_tts::TTS, voicevox::voicevox::VOICEVOX}, database::database::Database};
use serenity::{prelude::{TypeMapKey, RwLock}, model::id::GuildId, futures::lock::Mutex};
use crate::{
database::database::Database,
tts::{gcp_tts::gcp_tts::TTS, voicevox::voicevox::VOICEVOX},
};
use serenity::{
futures::lock::Mutex,
model::id::GuildId,
prelude::{RwLock, TypeMapKey},
};
use crate::tts::instance::TTSInstance;
use std::{sync::Arc, collections::HashMap};
use std::{collections::HashMap, sync::Arc};
/// TTSInstance data
pub struct TTSData;

View File

@ -1,29 +1,48 @@
use crate::tts::{gcp_tts::structs::voice_selection_params::VoiceSelectionParams, tts_type::TTSType};
use crate::tts::{
gcp_tts::structs::voice_selection_params::VoiceSelectionParams, tts_type::TTSType,
};
use super::user_config::UserConfig;
use redis::Commands;
pub struct Database {
pub connection: redis::Connection
pub client: redis::Client,
}
impl Database {
pub fn new(connection: redis::Connection) -> Self {
Self { connection }
pub fn new(client: redis::Client) -> Self {
Self { client }
}
pub async fn get_user_config(&mut self, user_id: u64) -> redis::RedisResult<Option<UserConfig>> {
let config: String = self.connection.get(format!("discord_user:{}", user_id)).unwrap_or_default();
pub async fn get_user_config(
&mut self,
user_id: u64,
) -> redis::RedisResult<Option<UserConfig>> {
if let Ok(mut connection) = self.client.get_connection() {
let config: String = connection
.get(format!("discord_user:{}", user_id))
.unwrap_or_default();
match serde_json::from_str(&config) {
Ok(config) => Ok(Some(config)),
Err(_) => Ok(None)
match serde_json::from_str(&config) {
Ok(config) => Ok(Some(config)),
Err(_) => Ok(None),
}
} else {
Ok(None)
}
}
pub async fn set_user_config(&mut self, user_id: u64, config: UserConfig) -> redis::RedisResult<()> {
pub async fn set_user_config(
&mut self,
user_id: u64,
config: UserConfig,
) -> redis::RedisResult<()> {
let config = serde_json::to_string(&config).unwrap();
self.connection.set::<String, String, ()>(format!("discord_user:{}", user_id), config).unwrap();
self.client
.get_connection()
.unwrap()
.set::<String, String, ()>(format!("discord_user:{}", user_id), config)
.unwrap();
Ok(())
}
@ -31,7 +50,7 @@ impl Database {
let voice_selection = VoiceSelectionParams {
languageCode: String::from("ja-JP"),
name: String::from("ja-JP-Wavenet-B"),
ssmlGender: String::from("neutral")
ssmlGender: String::from("neutral"),
};
let voice_type = TTSType::GCP;
@ -39,15 +58,21 @@ impl Database {
let config = UserConfig {
tts_type: Some(voice_type),
gcp_tts_voice: Some(voice_selection),
voicevox_speaker: Some(1)
voicevox_speaker: Some(1),
};
self.connection.set(format!("discord_user:{}", user_id), serde_json::to_string(&config).unwrap())?;
self.client.get_connection().unwrap().set(
format!("discord_user:{}", user_id),
serde_json::to_string(&config).unwrap(),
)?;
Ok(())
}
pub async fn get_user_config_or_default(&mut self, user_id: u64) -> redis::RedisResult<Option<UserConfig>> {
pub async fn get_user_config_or_default(
&mut self,
user_id: u64,
) -> redis::RedisResult<Option<UserConfig>> {
let config = self.get_user_config(user_id).await?;
match config {
Some(_) => Ok(config),

View File

@ -1,2 +1,2 @@
pub mod database;
pub mod user_config;
pub mod database;

View File

@ -1,10 +1,12 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use crate::tts::{gcp_tts::structs::voice_selection_params::VoiceSelectionParams, tts_type::TTSType};
use crate::tts::{
gcp_tts::structs::voice_selection_params::VoiceSelectionParams, tts_type::TTSType,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserConfig {
pub tts_type: Option<TTSType>,
pub gcp_tts_voice: Option<VoiceSelectionParams>,
pub voicevox_speaker: Option<i64>
pub voicevox_speaker: Option<i64>,
}

View File

@ -1,299 +1,120 @@
use serenity::{client::{EventHandler, Context}, async_trait, model::{gateway::Ready, interactions::{Interaction, application_command::ApplicationCommandInteraction, InteractionApplicationCommandCallbackDataFlags}, id::{GuildId, UserId}, channel::Message, prelude::Member, voice::VoiceState}, framework::standard::macros::group};
use crate::{data::TTSData, tts::{instance::TTSInstance, message::AnnounceMessage}, implement::member_name::ReadName};
#[group]
struct Test;
use crate::{
commands::{config::config_command, setup::setup_command, stop::stop_command},
data::DatabaseClientData,
events,
tts::tts_type::TTSType,
};
use serenity::{
async_trait,
client::{Context, EventHandler},
model::{
channel::Message,
gateway::Ready,
prelude::interaction::{Interaction, MessageFlags},
voice::VoiceState,
},
};
pub struct Handler;
async fn stop_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.remove(&guild.id);
}
let _handler = manager.leave(guild.id.0).await;
command.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("停止しました").flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
})
}).await?;
Ok(())
}
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 message(&self, ctx: Context, message: Message) {
events::message_receive::message(ctx, message).await
}
async fn ready(&self, ctx: Context, ready: Ready) {
events::ready::ready(ctx, ready).await
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::ApplicationCommand(command) = interaction {
if let Interaction::ApplicationCommand(command) = interaction.clone() {
let name = &*command.data.name;
match name {
"setup" => setup_command(&ctx, &command).await.unwrap(),
"stop" => stop_command(&ctx, &command).await.unwrap(),
"config" => config_command(&ctx, &command).await.unwrap(),
_ => {}
}
}
}
if let Some(message_component) = interaction.message_component() {
if let Some(v) = message_component.data.values.get(0) {
let data_read = ctx.data.read().await;
async fn voice_state_update(
&self,
ctx: Context,
guild_id: Option<GuildId>,
old: Option<VoiceState>,
new: VoiceState,
) {
let guild_id = guild_id.unwrap();
let mut config = {
let database = data_read
.get::<DatabaseClientData>()
.expect("Cannot get DatabaseClientData")
.clone();
let mut database = database.lock().await;
database
.get_user_config_or_default(message_component.user.id.0)
.await
.unwrap()
.unwrap()
};
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();
let mut message: Option<String> = None;
match old {
Some(old) => {
match (old.channel_id, new.channel_id) {
(Some(old_channel_id), Some(new_channel_id)) => {
if old_channel_id == new_channel_id {
return;
}
if old_channel_id != new_channel_id {
if instance.voice_channel == new_channel_id {
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
}
} else if old_channel_id == instance.voice_channel && new_channel_id != instance.voice_channel {
message = Some(format!("{} さんが通話から退出しました", new.member.unwrap().read_name()));
} else {
return;
}
}
(Some(old_channel_id), None) => {
if old_channel_id == instance.voice_channel {
message = Some(format!("{} さんが通話から退出しました", new.member.unwrap().read_name()));
} else {
return;
}
}
(None, Some(new_channel_id)) => {
if new_channel_id == instance.voice_channel {
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
} else {
return;
}
}
_ => {
return;
let res = (*v).clone();
let mut config_changed = false;
let mut voicevox_changed = false;
match &*res {
"TTS_CONFIG_ENGINE_SELECTED_GOOGLE" => {
config.tts_type = Some(TTSType::GCP);
config_changed = true;
}
"TTS_CONFIG_ENGINE_SELECTED_VOICEVOX" => {
config.tts_type = Some(TTSType::VOICEVOX);
config_changed = true;
}
_ => {
if res.starts_with("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_") {
config.voicevox_speaker = Some(
i64::from_str_radix(
&res.replace("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_", ""),
10,
)
.unwrap(),
);
config_changed = true;
voicevox_changed = true;
}
}
}
None => {
match new.channel_id {
Some(channel_id) => {
if instance.voice_channel == channel_id {
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
}
}
None => {
return;
}
if config_changed {
let database = data_read
.get::<DatabaseClientData>()
.expect("Cannot get DatabaseClientData")
.clone();
let mut database = database.lock().await;
database
.set_user_config(message_component.user.id.0, config.clone())
.await
.unwrap();
if voicevox_changed && config.tts_type.unwrap_or(TTSType::GCP) == TTSType::GCP {
message_component.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("設定しました\nこの音声を使うにはAPIをGoogleからVOICEVOXに変更する必要があります。")
.flags(MessageFlags::EPHEMERAL)
})
}).await.unwrap();
} else {
message_component
.create_interaction_response(&ctx.http, |f| {
f.interaction_response_data(|d| {
d.content("設定しました").flags(MessageFlags::EPHEMERAL)
})
})
.await
.unwrap();
}
}
}
if let Some(message) = message {
instance.read(AnnounceMessage {
message
}, &ctx).await;
}
}
}
async fn message(&self, ctx: Context, message: Message) {
if message.author.bot {
return;
}
let guild_id = message.guild(&ctx.cache).await;
if let None = guild_id {
return;
}
let guild_id = guild_id.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();
if instance.text_channel.0 != message.channel_id.0 {
return;
}
instance.read(message, &ctx).await;
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
let guild_id = GuildId(660046656934248460);
let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| {
commands.create_application_command(|command| {
command.name("stop")
.description("Stop tts")
});
commands.create_application_command(|command| {
command.name("setup")
.description("Setup tts")
})
}).await;
println!("{:?}", commands);
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
events::voice_state_update::voice_state_update(ctx, old, new).await
}
}

View File

@ -0,0 +1,40 @@
use serenity::{model::prelude::Message, prelude::Context};
use crate::data::TTSData;
pub async fn message(ctx: Context, message: Message) {
if message.author.bot {
return;
}
let guild_id = message.guild(&ctx.cache);
if let None = guild_id {
return;
}
let guild_id = guild_id.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();
if instance.text_channel.0 != message.channel_id.0 {
return;
}
instance.read(message, &ctx).await;
}
}

3
src/events/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod message_receive;
pub mod ready;
pub mod voice_state_update;

16
src/events/ready.rs Normal file
View File

@ -0,0 +1,16 @@
use serenity::{
model::prelude::{command::Command, Ready},
prelude::Context,
};
pub async fn ready(ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
let _ = Command::set_global_application_commands(&ctx.http, |commands| {
commands
.create_application_command(|command| command.name("stop").description("Stop tts"))
.create_application_command(|command| command.name("setup").description("Setup tts"))
.create_application_command(|command| command.name("config").description("Config"))
})
.await;
}

View File

@ -0,0 +1,80 @@
use crate::{
data::TTSData,
implement::{
member_name::ReadName,
voice_move_state::{VoiceMoveState, VoiceMoveStateTrait},
},
tts::message::AnnounceMessage,
};
use serenity::{model::voice::VoiceState, prelude::Context};
pub async fn voice_state_update(ctx: Context, old: Option<VoiceState>, new: VoiceState) {
if new.member.clone().unwrap().user.bot {
return;
}
if old.is_none() && new.guild_id.is_none() {
return;
}
let guild_id = if let Some(guild_id) = new.guild_id {
guild_id
} else {
old.clone().unwrap().guild_id.unwrap()
};
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();
let voice_move_state = new.move_state(&old, instance.voice_channel);
let message: Option<String> = match voice_move_state {
VoiceMoveState::JOIN => Some(format!(
"{} さんが通話に参加しました",
new.member.unwrap().read_name()
)),
VoiceMoveState::LEAVE => Some(format!(
"{} さんが通話から退出しました",
new.member.unwrap().read_name()
)),
_ => None,
};
if let Some(message) = message {
instance.read(AnnounceMessage { message }, &ctx).await;
}
if voice_move_state == VoiceMoveState::LEAVE {
let mut del_flag = false;
for channel in guild_id.channels(&ctx.http).await.unwrap() {
if channel.0 == instance.voice_channel {
del_flag = channel.1.members(&ctx.cache).await.unwrap().len() <= 1;
}
}
if del_flag {
storage.remove(&guild_id);
let manager = songbird::get(&ctx)
.await
.expect("Cannot get songbird client.")
.clone();
manager.remove(guild_id.0).await.unwrap();
}
}
}
}

View File

@ -8,4 +8,4 @@ impl ReadName for Member {
fn read_name(&self) -> String {
self.nick.clone().unwrap_or(self.user.name.clone())
}
}
}

View File

@ -1,25 +1,29 @@
use std::{path::Path, fs::File, io::Write};
use std::{env, fs::File, io::Write};
use async_trait::async_trait;
use serenity::{prelude::Context, model::prelude::Message};
use serenity::{model::prelude::Message, prelude::Context};
use crate::{
data::{TTSClientData, DatabaseClientData},
data::{DatabaseClientData, TTSClientData},
tts::{
gcp_tts::structs::{
audio_config::AudioConfig, synthesis_input::SynthesisInput,
synthesize_request::SynthesizeRequest,
},
instance::TTSInstance,
message::TTSMessage,
gcp_tts::structs::{
audio_config::AudioConfig, synthesis_input::SynthesisInput, synthesize_request::SynthesizeRequest
}, tts_type::{self, TTSType}
tts_type::TTSType,
validator,
},
};
#[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 {
let text = validator::remove_url(self.content.clone());
let mut res = if let Some(before_message) = &instance.before_message {
if before_message.author.id == self.author.id {
self.content.clone()
text.clone()
} else {
let member = self.member.clone();
let name = if let Some(member) = member {
@ -27,7 +31,7 @@ impl TTSMessage for Message {
} else {
self.author.name.clone()
};
format!("{} さんの発言<break time=\"200ms\"/>{}", name, self.content)
format!("{}さんの発言<break time=\"200ms\"/>{}", name, text)
}
} else {
let member = self.member.clone();
@ -36,9 +40,17 @@ impl TTSMessage for Message {
} else {
self.author.name.clone()
};
format!("{} さんの発言<break time=\"200ms\"/>{}", name, self.content)
format!("{}さんの発言<break time=\"200ms\"/>{}", name, text)
};
if self.attachments.len() > 0 {
res = format!(
"{}<break time=\"200ms\"/>{}個の添付ファイル",
res,
self.attachments.len()
);
}
instance.before_message = Some(self.clone());
res
@ -48,40 +60,56 @@ impl TTSMessage for Message {
let text = self.parse(instance, ctx).await;
let data_read = ctx.data.read().await;
let storage = data_read.get::<TTSClientData>().expect("Cannot get GCP TTSClientStorage").clone();
let storage = data_read
.get::<TTSClientData>()
.expect("Cannot get GCP TTSClientStorage")
.clone();
let mut tts = storage.lock().await;
let config = {
let database = data_read.get::<DatabaseClientData>().expect("Cannot get DatabaseClientData").clone();
let database = data_read
.get::<DatabaseClientData>()
.expect("Cannot get DatabaseClientData")
.clone();
let mut database = database.lock().await;
database.get_user_config_or_default(self.author.id.0).await.unwrap().unwrap()
database
.get_user_config_or_default(self.author.id.0)
.await
.unwrap()
.unwrap()
};
let audio = match config.tts_type.unwrap_or(TTSType::GCP) {
TTSType::GCP => {
tts.0.synthesize(SynthesizeRequest {
TTSType::GCP => tts
.0
.synthesize(SynthesizeRequest {
input: SynthesisInput {
text: None,
ssml: Some(format!("<speak>{}</speak>", text))
ssml: Some(format!("<speak>{}</speak>", text)),
},
voice: config.gcp_tts_voice.unwrap(),
audioConfig: AudioConfig {
audioEncoding: String::from("mp3"),
speakingRate: 1.2f32,
pitch: 1.0f32
}
}).await.unwrap()
}
pitch: 1.0f32,
},
})
.await
.unwrap(),
TTSType::VOICEVOX => {
tts.1.synthesize(text.replace("<break time=\"200ms\"/>", ""), config.voicevox_speaker.unwrap_or(1)).await.unwrap()
}
TTSType::VOICEVOX => tts
.1
.synthesize(
text.replace("<break time=\"200ms\"/>", ""),
config.voicevox_speaker.unwrap_or(1),
)
.await
.unwrap(),
};
let uuid = uuid::Uuid::new_v4().to_string();
let root = option_env!("CARGO_MANIFEST_DIR").unwrap();
let path = Path::new(root);
let path = env::current_dir().unwrap();
let file_path = path.join("audio").join(format!("{}.mp3", uuid));
let mut file = File::create(file_path.clone()).unwrap();

View File

@ -1,2 +1,3 @@
pub mod member_name;
pub mod message;
pub mod member_name;
pub mod voice_move_state;

View File

@ -0,0 +1,52 @@
use serenity::model::{prelude::ChannelId, voice::VoiceState};
pub trait VoiceMoveStateTrait {
fn move_state(&self, old: &Option<VoiceState>, target_channel: ChannelId) -> VoiceMoveState;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VoiceMoveState {
JOIN,
LEAVE,
NONE,
}
impl VoiceMoveStateTrait for VoiceState {
fn move_state(&self, old: &Option<VoiceState>, target_channel: ChannelId) -> VoiceMoveState {
let new = self;
if let None = old.clone() {
return if target_channel == new.channel_id.unwrap() {
VoiceMoveState::JOIN
} else {
VoiceMoveState::NONE
};
}
let old = (*old).clone().unwrap();
match (old.channel_id, new.channel_id) {
(Some(old_channel_id), Some(new_channel_id)) => {
if old_channel_id == new_channel_id {
VoiceMoveState::NONE
} else if old_channel_id != new_channel_id {
if target_channel == new_channel_id {
VoiceMoveState::JOIN
} else {
VoiceMoveState::NONE
}
} else {
VoiceMoveState::NONE
}
}
(Some(old_channel_id), None) => {
if old_channel_id == target_channel {
VoiceMoveState::LEAVE
} else {
VoiceMoveState::NONE
}
}
_ => VoiceMoveState::NONE,
}
}
}

View File

@ -1,23 +1,27 @@
use std::{sync::Arc, collections::HashMap};
use config::Config;
use data::{TTSData, TTSClientData, DatabaseClientData};
use database::database::Database;
use event_handler::Handler;
use tts::{gcp_tts::gcp_tts::TTS, voicevox::voicevox::VOICEVOX};
use serenity::{
client::{Client, bridge::gateway::GatewayIntents},
framework::StandardFramework, prelude::RwLock, futures::lock::Mutex
};
use songbird::SerenityInit;
mod commands;
mod config;
mod event_handler;
mod tts;
mod implement;
mod data;
mod database;
mod event_handler;
mod events;
mod implement;
mod tts;
use std::{collections::HashMap, env, sync::Arc};
use config::Config;
use data::{DatabaseClientData, TTSClientData, TTSData};
use database::database::Database;
use event_handler::Handler;
use serenity::{
client::Client,
framework::StandardFramework,
futures::lock::Mutex,
prelude::{GatewayIntents, RwLock},
};
use tts::{gcp_tts::gcp_tts::TTS, voicevox::voicevox::VOICEVOX};
use songbird::SerenityInit;
/// Create discord client
///
@ -28,16 +32,12 @@ mod database;
/// 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));
let framework = StandardFramework::new().configure(|c| c.with_whitespace(true).prefix(prefix));
Client::builder(token)
Client::builder(token, GatewayIntents::all())
.event_handler(Handler)
.application_id(id)
.framework(framework)
.intents(GatewayIntents::all())
.register_songbird()
.await
}
@ -45,24 +45,43 @@ async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, ser
#[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.");
let config = {
let config = std::fs::read_to_string("./config.toml");
if let Ok(config) = config {
toml::from_str::<Config>(&config).expect("Cannot load config file.")
} else {
let token = env::var("NCB_TOKEN").unwrap();
let application_id = env::var("NCB_APP_ID").unwrap();
let prefix = env::var("NCB_PREFIX").unwrap();
let redis_url = env::var("NCB_REDIS_URL").unwrap();
let voicevox_key = env::var("NCB_VOICEVOX_KEY").unwrap();
Config {
token,
application_id: u64::from_str_radix(&application_id, 10).unwrap(),
prefix,
redis_url,
voicevox_key,
}
}
};
// Create discord client
let mut client = create_client(&config.prefix, &config.token, config.application_id).await.expect("Err creating client");
let mut client = create_client(&config.prefix, &config.token, config.application_id)
.await
.expect("Err creating client");
// Create GCP TTS client
let tts = match TTS::new("./credentials.json".to_string()).await {
Ok(tts) => tts,
Err(err) => panic!("{}", err)
Err(err) => panic!("GCP init error: {}", err),
};
let voicevox = VOICEVOX::new(config.voicevox_key);
let database_client = {
let redis_client = redis::Client::open(config.redis_url).unwrap();
let con = redis_client.get_connection().unwrap();
Database::new(con)
Database::new(redis_client)
};
// Create TTS storage

View File

@ -1,21 +1,22 @@
use gcp_auth::Token;
use crate::tts::gcp_tts::structs::{
synthesize_request::SynthesizeRequest,
synthesize_response::SynthesizeResponse,
synthesize_request::SynthesizeRequest, synthesize_response::SynthesizeResponse,
};
use gcp_auth::Token;
#[derive(Clone)]
pub struct TTS {
pub token: Token,
pub credentials_path: String
pub credentials_path: String,
}
impl TTS {
pub async fn update_token(&mut self) -> Result<(), gcp_auth::Error> {
if self.token.has_expired() {
let authenticator = gcp_auth::from_credentials_file(self.credentials_path.clone()).await?;
let token = authenticator.get_token(&["https://www.googleapis.com/auth/cloud-platform"]).await?;
let authenticator =
gcp_auth::from_credentials_file(self.credentials_path.clone()).await?;
let token = authenticator
.get_token(&["https://www.googleapis.com/auth/cloud-platform"])
.await?;
self.token = token;
}
@ -24,11 +25,13 @@ impl TTS {
pub async fn new(credentials_path: String) -> Result<TTS, gcp_auth::Error> {
let authenticator = gcp_auth::from_credentials_file(credentials_path.clone()).await?;
let token = authenticator.get_token(&["https://www.googleapis.com/auth/cloud-platform"]).await?;
let token = authenticator
.get_token(&["https://www.googleapis.com/auth/cloud-platform"])
.await?;
Ok(TTS {
token,
credentials_path
credentials_path,
})
}
@ -53,19 +56,29 @@ impl TTS {
/// }
/// }).await.unwrap();
/// ```
pub async fn synthesize(&mut self, request: SynthesizeRequest) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
pub async fn synthesize(
&mut self,
request: SynthesizeRequest,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
self.update_token().await.unwrap();
let client = reqwest::Client::new();
match client.post("https://texttospeech.googleapis.com/v1/text:synthesize")
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()))
.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))
.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)),
}
}
}

View File

@ -1,2 +1,2 @@
pub mod gcp_tts;
pub mod structs;
pub mod structs;

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
/// Example:
/// ```rust
@ -13,5 +13,5 @@ use serde::{Serialize, Deserialize};
pub struct AudioConfig {
pub audioEncoding: String,
pub speakingRate: f32,
pub pitch: f32
}
pub pitch: f32,
}

View File

@ -1,5 +1,5 @@
pub mod audio_config;
pub mod synthesis_input;
pub mod synthesize_request;
pub mod synthesize_response;
pub mod voice_selection_params;
pub mod synthesize_response;

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
/// Example:
/// ```rust
@ -10,5 +10,5 @@ use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct SynthesisInput {
pub text: Option<String>,
pub ssml: Option<String>
}
pub ssml: Option<String>,
}

View File

@ -1,9 +1,8 @@
use serde::{Serialize, Deserialize};
use crate::tts::gcp_tts::structs::{
synthesis_input::SynthesisInput,
audio_config::AudioConfig,
audio_config::AudioConfig, synthesis_input::SynthesisInput,
voice_selection_params::VoiceSelectionParams,
};
use serde::{Deserialize, Serialize};
/// Example:
/// ```rust
@ -30,4 +29,4 @@ pub struct SynthesizeRequest {
pub input: SynthesisInput,
pub voice: VoiceSelectionParams,
pub audioConfig: AudioConfig,
}
}

View File

@ -1,7 +1,7 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct SynthesizeResponse {
pub audioContent: String
}
pub audioContent: String,
}

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
/// Example:
/// ```rust
@ -13,5 +13,5 @@ use serde::{Serialize, Deserialize};
pub struct VoiceSelectionParams {
pub languageCode: String,
pub name: String,
pub ssmlGender: String
}
pub ssmlGender: String,
}

View File

@ -1,12 +1,18 @@
use serenity::{model::{channel::Message, id::{ChannelId, GuildId}}, prelude::Context};
use serenity::{
model::{
channel::Message,
id::{ChannelId, GuildId},
},
prelude::Context,
};
use crate::{tts::message::TTSMessage};
use crate::tts::message::TTSMessage;
pub struct TTSInstance {
pub before_message: Option<Message>,
pub text_channel: ChannelId,
pub voice_channel: ChannelId,
pub guild: GuildId
pub guild: GuildId,
}
impl TTSInstance {
@ -17,7 +23,8 @@ impl TTSInstance {
/// instance.read(message, &ctx).await;
/// ```
pub async fn read<T>(&mut self, message: T, ctx: &Context)
where T: TTSMessage
where
T: TTSMessage,
{
let path = message.synthesize(self, ctx).await;
@ -25,7 +32,9 @@ impl TTSInstance {
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.");
let input = songbird::input::ffmpeg(path)
.await
.expect("File not found.");
call.enqueue_source(input);
}
}

View File

@ -1,16 +1,18 @@
use std::{path::Path, fs::File, io::Write};
use std::{env, fs::File, io::Write};
use async_trait::async_trait;
use serenity::prelude::Context;
use crate::{tts::instance::TTSInstance, data::TTSClientData};
use crate::{data::TTSClientData, tts::instance::TTSInstance};
use super::gcp_tts::structs::{synthesize_request::SynthesizeRequest, synthesis_input::SynthesisInput, audio_config::AudioConfig, voice_selection_params::VoiceSelectionParams};
use super::gcp_tts::structs::{
audio_config::AudioConfig, synthesis_input::SynthesisInput,
synthesize_request::SynthesizeRequest, voice_selection_params::VoiceSelectionParams,
};
/// Message trait that can be used to synthesize text to speech.
#[async_trait]
pub trait TTSMessage {
/// Parse the message for synthesis.
///
/// Example:
@ -34,38 +36,47 @@ pub struct AnnounceMessage {
#[async_trait]
impl TTSMessage for AnnounceMessage {
async fn parse(&self, instance: &mut TTSInstance, ctx: &Context) -> String {
async fn parse(&self, instance: &mut TTSInstance, _ctx: &Context) -> String {
instance.before_message = None;
format!(r#"<speak>アナウンス<break time="200ms"/>{}</speak>"#, self.message)
format!(
r#"<speak>アナウンス<break time="200ms"/>{}</speak>"#,
self.message
)
}
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 = data_read
.get::<TTSClientData>()
.expect("Cannot get TTSClientStorage")
.clone();
let mut storage = storage.lock().await;
let audio = storage.0.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 audio = storage
.0
.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 path = env::current_dir().unwrap();
let file_path = path.join("audio").join(format!("{}.mp3", uuid));
let mut file = File::create(file_path.clone()).unwrap();
@ -73,4 +84,4 @@ impl TTSMessage for AnnounceMessage {
file_path.into_os_string().into_string().unwrap()
}
}
}

View File

@ -1,5 +1,6 @@
pub mod gcp_tts;
pub mod voicevox;
pub mod instance;
pub mod message;
pub mod tts_type;
pub mod instance;
pub mod validator;
pub mod voicevox;

View File

@ -3,5 +3,5 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TTSType {
GCP,
VOICEVOX
}
VOICEVOX,
}

6
src/tts/validator.rs Normal file
View File

@ -0,0 +1,6 @@
use regex::Regex;
pub fn remove_url(text: String) -> String {
let url_regex = Regex::new(r"(http://|https://){1}[\w\.\-/:\#\?=\&;%\~\+]+").unwrap();
url_regex.replace_all(&text, " URL ").to_string()
}

View File

@ -1,2 +1,2 @@
pub mod structs;
pub mod voicevox;
pub mod voicevox;

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use super::mora::Mora;
@ -7,5 +7,5 @@ pub struct AccentPhrase {
pub moras: Vec<Mora>,
pub accent: f64,
pub pause_mora: Option<Mora>,
pub is_interrogative: bool
}
pub is_interrogative: bool,
}

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use super::accent_phrase::AccentPhrase;
@ -14,5 +14,5 @@ pub struct AudioQuery {
pub postPhonemeLength: f64,
pub outputSamplingRate: f64,
pub outputStereo: bool,
pub kana: Option<String>
}
pub kana: Option<String>,
}

View File

@ -1,3 +1,3 @@
pub mod mora;
pub mod accent_phrase;
pub mod audio_query;
pub mod accent_phrase;
pub mod mora;

View File

@ -1,4 +1,4 @@
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Mora {
@ -7,5 +7,5 @@ pub struct Mora {
pub consonant_length: Option<f64>,
pub vowel: String,
pub vowel_length: f64,
pub pitch: f64
}
pub pitch: f64,
}

View File

@ -2,26 +2,61 @@ const API_URL: &str = "https://api.su-shiki.com/v2/voicevox/audio";
#[derive(Clone)]
pub struct VOICEVOX {
pub key: String
pub key: String,
}
impl VOICEVOX {
pub fn new(key: String) -> Self {
Self {
key
}
pub fn get_speakers() -> Vec<(String, i64)> {
vec![
("四国めたん - ノーマル".to_string(), 2),
("四国めたん - あまあま".to_string(), 0),
("四国めたん - ツンツン".to_string(), 6),
("四国めたん - セクシー".to_string(), 4),
("ずんだもん - ノーマル".to_string(), 3),
("ずんだもん - あまあま".to_string(), 1),
("ずんだもん - ツンツン".to_string(), 7),
("ずんだもん - セクシー".to_string(), 5),
("春日部つむぎ - ノーマル".to_string(), 8),
("雨晴はう - ノーマル".to_string(), 10),
("波音リツ - ノーマル".to_string(), 9),
("玄野武宏 - ノーマル".to_string(), 11),
("白上虎太郎 - ノーマル".to_string(), 12),
("青山龍星 - ノーマル".to_string(), 13),
("冥鳴ひまり - ノーマル".to_string(), 14),
("九州そら - ノーマル".to_string(), 16),
("九州そら - あまあま".to_string(), 15),
("九州そら - ツンツン".to_string(), 18),
("九州そら - セクシー".to_string(), 17),
("九州そら - ささやき".to_string(), 19),
("モチノ・キョウコ - ノーマル".to_string(), 20),
]
}
pub async fn synthesize(&self, text: String, speaker: i64) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
pub fn new(key: String) -> Self {
Self { key }
}
pub async fn synthesize(
&self,
text: String,
speaker: i64,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
match client.post(API_URL).query(&[("speaker", speaker.to_string()), ("text", text), ("key", self.key.clone())]).send().await {
match client
.post(API_URL)
.query(&[
("speaker", speaker.to_string()),
("text", text),
("key", self.key.clone()),
])
.send()
.await
{
Ok(response) => {
let body = response.bytes().await?;
Ok(body.to_vec())
}
Err(err) => {
Err(Box::new(err))
}
Err(err) => Err(Box::new(err)),
}
}
}
}