mirror of
https://github.com/mii443/ncb-tts-r2.git
synced 2025-09-01 14:59:14 +00:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
52f86a6c16 | |||
b62e81dd66 | |||
60770f65b6 | |||
f93701a591 | |||
2a40e9ee16 | |||
2f2b82857f | |||
8b2574e90b | |||
fb654229b0 | |||
7124930a9d | |||
0bd051e48c | |||
ccf2c63224 | |||
c2a84be3a4 | |||
d525636060 | |||
68a73a4dae | |||
f7b7071a09 | |||
b7a4da7f3e | |||
708b6fc429 | |||
f9fd0686a7 | |||
99d8ef9bef | |||
af01576a99 | |||
ddab474d67 | |||
d10bfcc333 | |||
1470612d8b | |||
065717839b | |||
6dafc66878 | |||
1789bd7c4e | |||
0b93c23e91 | |||
7fe65bc397 | |||
51c39036c6 | |||
c52429bce0 | |||
5ca5325fbd | |||
4161acbd45 | |||
47b11262e2 | |||
b36bee8be8 | |||
6aec4e4ea7 | |||
2bf2fe05f1 | |||
f99d37ea56 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target
|
||||||
|
audio
|
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@ -9,6 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
name: Checkout
|
||||||
- uses: docker/metadata-action@v3
|
- uses: docker/metadata-action@v3
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
@ -21,9 +22,26 @@ jobs:
|
|||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: morioka22
|
username: morioka22
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- uses: docker/build-push-action@v2
|
- uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,4 +3,5 @@ Cargo.lock
|
|||||||
config.toml
|
config.toml
|
||||||
credentials.json
|
credentials.json
|
||||||
/audio
|
/audio
|
||||||
*.mp3
|
*.mp3
|
||||||
|
*.swp
|
||||||
|
@ -14,20 +14,21 @@ reqwest = { version = "0.11", features = ["json"] }
|
|||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
async-trait = "0.1.57"
|
async-trait = "0.1.57"
|
||||||
redis = "*"
|
redis = "*"
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
[dependencies.uuid]
|
[dependencies.uuid]
|
||||||
version = "0.8"
|
version = "0.8"
|
||||||
features = ["serde", "v4"]
|
features = ["serde", "v4"]
|
||||||
|
|
||||||
[dependencies.songbird]
|
[dependencies.songbird]
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
features = ["builtin-queue"]
|
features = ["builtin-queue"]
|
||||||
|
|
||||||
[dependencies.serenity]
|
[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"]
|
features = ["builder", "cache", "client", "gateway", "model", "utils", "unstable_discord_api", "collector", "rustls_backend", "framework", "voice"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
features = ["macros", "rt-multi-thread"]
|
features = ["macros", "rt-multi-thread"]
|
||||||
|
33
Dockerfile
33
Dockerfile
@ -1,16 +1,19 @@
|
|||||||
FROM ubuntu:22.04
|
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||||
RUN apt-get update \
|
WORKDIR app
|
||||||
&& apt-get install -y ffmpeg libssl-dev pkg-config libopus-dev wget curl gcc \
|
|
||||||
&& apt-get -y clean \
|
FROM chef AS planner
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
COPY . .
|
||||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
ENV PATH $PATH:/root/.cargo/bin/
|
|
||||||
RUN rustup install stable
|
FROM chef AS builder
|
||||||
WORKDIR /usr/src/ncb-tts-r2
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
COPY Cargo.toml .
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg libssl-dev pkg-config libopus-dev gcc && apt-get -y clean
|
||||||
COPY src src
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
RUN cargo build --release \
|
COPY . .
|
||||||
&& cp /usr/src/ncb-tts-r2/target/release/ncb-tts-r2 /usr/bin/ncb-tts-r2 \
|
RUN cargo build --release
|
||||||
&& mkdir -p /ncb-tts-r2/audio
|
|
||||||
|
FROM debian:bullseye-slim AS runtime
|
||||||
WORKDIR /ncb-tts-r2
|
WORKDIR /ncb-tts-r2
|
||||||
CMD ["ncb-tts-r2"]
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates ffmpeg libssl-dev libopus-dev && apt-get -y clean && mkdir audio
|
||||||
|
COPY --from=builder /app/target/release/ncb-tts-r2 /usr/local/bin
|
||||||
|
ENTRYPOINT ["/usr/local/bin/ncb-tts-r2"]
|
||||||
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ncb-tts-r2:
|
||||||
|
container_name: ncb-tts-r2
|
||||||
|
image: ghcr.io/morioka22/ncb-tts-r2: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
|
56
manifest/ncb-tts.yaml
Normal file
56
manifest/ncb-tts.yaml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ncb-tts-deployment
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ncb-tts
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ncb-tts
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:7.0.4-alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
name: ncb-redis
|
||||||
|
volumeMounts:
|
||||||
|
- name: ncb-redis-pvc
|
||||||
|
mountPath: /data
|
||||||
|
- name: tts
|
||||||
|
image: ghcr.io/morioka22/ncb-tts-r2
|
||||||
|
volumeMounts:
|
||||||
|
- name: gcp-credentials
|
||||||
|
mountPath: /ncb-tts-r2/credentials.json
|
||||||
|
subPath: credentials.json
|
||||||
|
env:
|
||||||
|
- name: NCB_REDIS_URL
|
||||||
|
value: "redis://localhost:6379/"
|
||||||
|
- name: NCB_PREFIX
|
||||||
|
value: "t2!"
|
||||||
|
- name: NCB_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ncb-secret
|
||||||
|
key: BOT_TOKEN
|
||||||
|
- name: NCB_VOICEVOX_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ncb-secret
|
||||||
|
key: VOICEVOX_KEY
|
||||||
|
- name: NCB_APP_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ncb-secret
|
||||||
|
key: APP_ID
|
||||||
|
volumes:
|
||||||
|
- name: ncb-redis-pvc
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: ncb-redis-pvc
|
||||||
|
- name: gcp-credentials
|
||||||
|
secret:
|
||||||
|
secretName: gcp-credentials
|
12
manifest/pvc.yaml
Normal file
12
manifest/pvc.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: ncb-redis-pvc
|
||||||
|
labels:
|
||||||
|
app: ncb-redis
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 3Gi
|
96
src/commands/config.rs
Normal file
96
src/commands/config.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use serenity::{
|
||||||
|
model::prelude::{
|
||||||
|
component::ButtonStyle,
|
||||||
|
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を指定")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.create_action_row(|a| {
|
||||||
|
a.create_button(|f| {
|
||||||
|
f.label("サーバー設定")
|
||||||
|
.custom_id("TTS_CONFIG_SERVER")
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flags(MessageFlags::EPHEMERAL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
4
src/commands/mod.rs
Normal file
4
src/commands/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod setup;
|
||||||
|
pub mod skip;
|
||||||
|
pub mod stop;
|
146
src/commands/setup.rs
Normal file
146
src/commands/setup.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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 text_channel_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 text_channel_id = {
|
||||||
|
if let Some(mode) = command.data.options.get(0) {
|
||||||
|
let mode = mode.clone();
|
||||||
|
let value = mode.value.unwrap();
|
||||||
|
let value = value.as_str().unwrap();
|
||||||
|
match value {
|
||||||
|
"TEXT_CHANNEL" => command.channel_id,
|
||||||
|
"NEW_THREAD" => {
|
||||||
|
let message = command
|
||||||
|
.channel_id
|
||||||
|
.send_message(&ctx.http, |f| f.content("TTS thread"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
command
|
||||||
|
.channel_id
|
||||||
|
.create_public_thread(&ctx.http, message, |f| {
|
||||||
|
f.name("TTS").auto_archive_duration(60)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
"VOICE_CHANNEL" => channel_id,
|
||||||
|
_ => channel_id,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
storage.insert(
|
||||||
|
guild.id,
|
||||||
|
TTSInstance {
|
||||||
|
before_message: None,
|
||||||
|
guild: guild.id,
|
||||||
|
text_channel: text_channel_id,
|
||||||
|
voice_channel: channel_id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
text_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(format!("TTS Channel: <#{}>{}", text_channel_id, if text_channel_id == channel_id { "\nボイスチャンネルを右クリックし `チャットを開く` を押して開くことが出来ます。" } else { "" }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
text_channel_id.send_message(&ctx.http, |f| f.embed(|e| e.title("読み上げ (Serenity)")
|
||||||
|
.field("クレジット", "```\n四国めたん ずんだもん\n春日部つむぎ 雨晴はう\n波音リツ 玄野武宏\n白上虎太郎 青山龍星\n冥鳴ひまり 九州そら\nモチノ・キョウコ\nナースロボ_タイプT```", false)
|
||||||
|
.field("設定コマンド", "`/config`", false)
|
||||||
|
.field("フィードバック", "https://feedback.mii.codes/", false)
|
||||||
|
)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
90
src/commands/skip.rs
Normal file
90
src/commands/skip.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use serenity::{
|
||||||
|
model::prelude::{
|
||||||
|
interaction::{application_command::ApplicationCommandInteraction, MessageFlags},
|
||||||
|
UserId,
|
||||||
|
},
|
||||||
|
prelude::Context,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::data::TTSData;
|
||||||
|
|
||||||
|
pub async fn skip_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 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.get_mut(&guild.id).unwrap().skip(&ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
command
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.interaction_response_data(|d| d.content("スキップしました"))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
105
src/commands/stop.rs
Normal file
105
src/commands/stop.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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 text_channel_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 text_channel_id = storage.get(&guild.id).unwrap().text_channel;
|
||||||
|
|
||||||
|
storage.remove(&guild.id);
|
||||||
|
|
||||||
|
text_channel_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let _handler = manager.remove(guild.id.0).await;
|
||||||
|
|
||||||
|
command
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.interaction_response_data(|d| d.content("停止しました"))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let _ = text_channel_id
|
||||||
|
.edit_thread(&ctx.http, |f| f.archived(true))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -6,5 +6,5 @@ pub struct Config {
|
|||||||
pub token: String,
|
pub token: String,
|
||||||
pub application_id: u64,
|
pub application_id: u64,
|
||||||
pub redis_url: String,
|
pub redis_url: String,
|
||||||
pub voicevox_key: String
|
pub voicevox_key: String,
|
||||||
}
|
}
|
||||||
|
13
src/data.rs
13
src/data.rs
@ -1,8 +1,15 @@
|
|||||||
use crate::{tts::{gcp_tts::gcp_tts::TTS, voicevox::voicevox::VOICEVOX}, database::database::Database};
|
use crate::{
|
||||||
use serenity::{prelude::{TypeMapKey, RwLock}, model::id::GuildId, futures::lock::Mutex};
|
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 crate::tts::instance::TTSInstance;
|
||||||
use std::{sync::Arc, collections::HashMap};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
/// TTSInstance data
|
/// TTSInstance data
|
||||||
pub struct TTSData;
|
pub struct TTSData;
|
||||||
|
@ -1,29 +1,93 @@
|
|||||||
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 super::{dictionary::Dictionary, server_config::ServerConfig, user_config::UserConfig};
|
||||||
use redis::Commands;
|
use redis::Commands;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub connection: redis::Connection
|
pub client: redis::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn new(connection: redis::Connection) -> Self {
|
pub fn new(client: redis::Client) -> Self {
|
||||||
Self { connection }
|
Self { client }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_config(&mut self, user_id: u64) -> redis::RedisResult<Option<UserConfig>> {
|
pub async fn get_server_config(
|
||||||
let config: String = self.connection.get(format!("discord_user:{}", user_id)).unwrap_or_default();
|
&mut self,
|
||||||
|
server_id: u64,
|
||||||
|
) -> redis::RedisResult<Option<ServerConfig>> {
|
||||||
|
if let Ok(mut connection) = self.client.get_connection() {
|
||||||
|
let config: String = connection
|
||||||
|
.get(format!("discord_server:{}", server_id))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
match serde_json::from_str(&config) {
|
match serde_json::from_str(&config) {
|
||||||
Ok(config) => Ok(Some(config)),
|
Ok(config) => Ok(Some(config)),
|
||||||
Err(_) => Ok(None)
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_user_config(&mut self, user_id: u64, config: UserConfig) -> redis::RedisResult<()> {
|
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),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_server_config(
|
||||||
|
&mut self,
|
||||||
|
server_id: u64,
|
||||||
|
config: ServerConfig,
|
||||||
|
) -> redis::RedisResult<()> {
|
||||||
let config = serde_json::to_string(&config).unwrap();
|
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_server:{}", server_id), config)
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_user_config(
|
||||||
|
&mut self,
|
||||||
|
user_id: u64,
|
||||||
|
config: UserConfig,
|
||||||
|
) -> redis::RedisResult<()> {
|
||||||
|
let config = serde_json::to_string(&config).unwrap();
|
||||||
|
self.client
|
||||||
|
.get_connection()
|
||||||
|
.unwrap()
|
||||||
|
.set::<String, String, ()>(format!("discord_user:{}", user_id), config)
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_default_server_config(&mut self, server_id: u64) -> redis::RedisResult<()> {
|
||||||
|
let config = ServerConfig {
|
||||||
|
dictionary: Dictionary::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.client.get_connection().unwrap().set(
|
||||||
|
format!("discord_server:{}", server_id),
|
||||||
|
serde_json::to_string(&config).unwrap(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +95,7 @@ impl Database {
|
|||||||
let voice_selection = VoiceSelectionParams {
|
let voice_selection = VoiceSelectionParams {
|
||||||
languageCode: String::from("ja-JP"),
|
languageCode: String::from("ja-JP"),
|
||||||
name: String::from("ja-JP-Wavenet-B"),
|
name: String::from("ja-JP-Wavenet-B"),
|
||||||
ssmlGender: String::from("neutral")
|
ssmlGender: String::from("neutral"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let voice_type = TTSType::GCP;
|
let voice_type = TTSType::GCP;
|
||||||
@ -39,15 +103,35 @@ impl Database {
|
|||||||
let config = UserConfig {
|
let config = UserConfig {
|
||||||
tts_type: Some(voice_type),
|
tts_type: Some(voice_type),
|
||||||
gcp_tts_voice: Some(voice_selection),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_config_or_default(&mut self, user_id: u64) -> redis::RedisResult<Option<UserConfig>> {
|
pub async fn get_server_config_or_default(
|
||||||
|
&mut self,
|
||||||
|
server_id: u64,
|
||||||
|
) -> redis::RedisResult<Option<ServerConfig>> {
|
||||||
|
let config = self.get_server_config(server_id).await?;
|
||||||
|
match config {
|
||||||
|
Some(_) => Ok(config),
|
||||||
|
None => {
|
||||||
|
self.set_default_server_config(server_id).await?;
|
||||||
|
self.get_server_config(server_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?;
|
let config = self.get_user_config(user_id).await?;
|
||||||
match config {
|
match config {
|
||||||
Some(_) => Ok(config),
|
Some(_) => Ok(config),
|
||||||
|
34
src/database/dictionary.rs
Normal file
34
src/database/dictionary.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Rule {
|
||||||
|
pub id: String,
|
||||||
|
pub is_regex: bool,
|
||||||
|
pub rule: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Dictionary {
|
||||||
|
pub rules: Vec<Rule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dictionary {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let rules = vec![
|
||||||
|
Rule {
|
||||||
|
id: String::from("url"),
|
||||||
|
is_regex: true,
|
||||||
|
rule: String::from(r"(http://|https://){1}[\w\.\-/:\#\?=\&;%\~\+]+"),
|
||||||
|
to: String::from("URL"),
|
||||||
|
},
|
||||||
|
Rule {
|
||||||
|
id: String::from("code"),
|
||||||
|
is_regex: true,
|
||||||
|
rule: String::from(r"```(.|\n)*```"),
|
||||||
|
to: String::from("code"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
Self { rules }
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
|
pub mod database;
|
||||||
|
pub mod dictionary;
|
||||||
|
pub mod server_config;
|
||||||
pub mod user_config;
|
pub mod user_config;
|
||||||
pub mod database;
|
|
7
src/database/server_config.rs
Normal file
7
src/database/server_config.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use super::dictionary::Dictionary;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub dictionary: Dictionary,
|
||||||
|
}
|
@ -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)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct UserConfig {
|
pub struct UserConfig {
|
||||||
pub tts_type: Option<TTSType>,
|
pub tts_type: Option<TTSType>,
|
||||||
pub gcp_tts_voice: Option<VoiceSelectionParams>,
|
pub gcp_tts_voice: Option<VoiceSelectionParams>,
|
||||||
pub voicevox_speaker: Option<i64>
|
pub voicevox_speaker: Option<i64>,
|
||||||
}
|
}
|
||||||
|
@ -1,302 +1,413 @@
|
|||||||
use serenity::{client::{EventHandler, Context}, async_trait, model::{gateway::Ready, interactions::{Interaction, application_command::ApplicationCommandInteraction, InteractionApplicationCommandCallbackDataFlags}, id::{GuildId, UserId}, channel::Message, prelude::{Member, application_command::{ApplicationCommandOptionType, ApplicationCommandOption}}, voice::VoiceState}, framework::standard::macros::group};
|
use crate::{
|
||||||
use crate::{data::TTSData, tts::{instance::TTSInstance, message::AnnounceMessage}, implement::member_name::ReadName};
|
commands::{
|
||||||
|
config::config_command, setup::setup_command, skip::skip_command, stop::stop_command,
|
||||||
#[group]
|
},
|
||||||
struct Test;
|
data::DatabaseClientData,
|
||||||
|
database::dictionary::Rule,
|
||||||
|
events,
|
||||||
|
tts::tts_type::TTSType,
|
||||||
|
};
|
||||||
|
use serenity::{
|
||||||
|
async_trait,
|
||||||
|
client::{Context, EventHandler},
|
||||||
|
model::{
|
||||||
|
channel::Message,
|
||||||
|
gateway::Ready,
|
||||||
|
prelude::{
|
||||||
|
component::{ActionRowComponent, ButtonStyle, InputTextStyle},
|
||||||
|
interaction::{Interaction, InteractionResponseType, MessageFlags},
|
||||||
|
},
|
||||||
|
voice::VoiceState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Handler;
|
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]
|
#[async_trait]
|
||||||
impl EventHandler for Handler {
|
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) {
|
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;
|
let name = &*command.data.name;
|
||||||
match name {
|
match name {
|
||||||
"setup" => setup_command(&ctx, &command).await.unwrap(),
|
"setup" => setup_command(&ctx, &command).await.unwrap(),
|
||||||
"stop" => stop_command(&ctx, &command).await.unwrap(),
|
"stop" => stop_command(&ctx, &command).await.unwrap(),
|
||||||
|
"config" => config_command(&ctx, &command).await.unwrap(),
|
||||||
|
"skip" => skip_command(&ctx, &command).await.unwrap(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if let Interaction::ModalSubmit(modal) = interaction.clone() {
|
||||||
|
if modal.data.custom_id != "TTS_CONFIG_SERVER_ADD_DICTIONARY" {
|
||||||
async fn voice_state_update(
|
|
||||||
&self,
|
|
||||||
ctx: Context,
|
|
||||||
guild_id: Option<GuildId>,
|
|
||||||
old: Option<VoiceState>,
|
|
||||||
new: VoiceState,
|
|
||||||
) {
|
|
||||||
let guild_id = 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let instance = storage.get_mut(&guild_id).unwrap();
|
let rows = modal.data.components.clone();
|
||||||
|
let rule_name =
|
||||||
|
if let ActionRowComponent::InputText(text) = rows[0].components[0].clone() {
|
||||||
|
text.value
|
||||||
|
} else {
|
||||||
|
panic!("Cannot get rule name");
|
||||||
|
};
|
||||||
|
|
||||||
let mut message: Option<String> = None;
|
let from = if let ActionRowComponent::InputText(text) = rows[1].components[0].clone() {
|
||||||
|
text.value
|
||||||
|
} else {
|
||||||
|
panic!("Cannot get from");
|
||||||
|
};
|
||||||
|
|
||||||
match old {
|
let to = if let ActionRowComponent::InputText(text) = rows[2].components[0].clone() {
|
||||||
Some(old) => {
|
text.value
|
||||||
match (old.channel_id, new.channel_id) {
|
} else {
|
||||||
(Some(old_channel_id), Some(new_channel_id)) => {
|
panic!("Cannot get to");
|
||||||
if old_channel_id == new_channel_id {
|
};
|
||||||
return;
|
|
||||||
}
|
let rule = Rule {
|
||||||
if old_channel_id != new_channel_id {
|
id: rule_name.clone(),
|
||||||
if instance.voice_channel == new_channel_id {
|
is_regex: true,
|
||||||
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
|
rule: from.clone(),
|
||||||
}
|
to: to.clone(),
|
||||||
} else if old_channel_id == instance.voice_channel && new_channel_id != instance.voice_channel {
|
};
|
||||||
message = Some(format!("{} さんが通話から退出しました", new.member.unwrap().read_name()));
|
|
||||||
} else {
|
let data_read = ctx.data.read().await;
|
||||||
return;
|
|
||||||
}
|
let mut config = {
|
||||||
}
|
let database = data_read
|
||||||
(Some(old_channel_id), None) => {
|
.get::<DatabaseClientData>()
|
||||||
if old_channel_id == instance.voice_channel {
|
.expect("Cannot get DatabaseClientData")
|
||||||
message = Some(format!("{} さんが通話から退出しました", new.member.unwrap().read_name()));
|
.clone();
|
||||||
} else {
|
let mut database = database.lock().await;
|
||||||
return;
|
database
|
||||||
}
|
.get_server_config_or_default(modal.guild_id.unwrap().0)
|
||||||
}
|
.await
|
||||||
(None, Some(new_channel_id)) => {
|
.unwrap()
|
||||||
if new_channel_id == instance.voice_channel {
|
.unwrap()
|
||||||
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
|
};
|
||||||
} else {
|
config.dictionary.rules.push(rule);
|
||||||
return;
|
|
||||||
}
|
{
|
||||||
}
|
let database = data_read
|
||||||
_ => {
|
.get::<DatabaseClientData>()
|
||||||
return;
|
.expect("Cannot get DatabaseClientData")
|
||||||
|
.clone();
|
||||||
|
let mut database = database.lock().await;
|
||||||
|
database
|
||||||
|
.set_server_config(modal.guild_id.unwrap().0, config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
modal
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.custom_id("TTS_CONFIG_SERVER_ADD_DICTIONARY_RESPONSE")
|
||||||
|
.content(format!(
|
||||||
|
"辞書を追加しました\n名前: {}\n変換元: {}\n変換後: {}",
|
||||||
|
rule_name, from, to
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(message_component) = interaction.message_component() {
|
||||||
|
match &*message_component.data.custom_id {
|
||||||
|
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU" => {
|
||||||
|
let i = usize::from_str_radix(&message_component.data.values[0], 10).unwrap();
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
|
||||||
|
let mut config = {
|
||||||
|
let database = data_read
|
||||||
|
.get::<DatabaseClientData>()
|
||||||
|
.expect("Cannot get DatabaseClientData")
|
||||||
|
.clone();
|
||||||
|
let mut database = database.lock().await;
|
||||||
|
database
|
||||||
|
.get_server_config_or_default(message_component.guild_id.unwrap().0)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
config.dictionary.rules.remove(i);
|
||||||
|
{
|
||||||
|
let database = data_read
|
||||||
|
.get::<DatabaseClientData>()
|
||||||
|
.expect("Cannot get DatabaseClientData")
|
||||||
|
.clone();
|
||||||
|
let mut database = database.lock().await;
|
||||||
|
database
|
||||||
|
.set_server_config(message_component.guild_id.unwrap().0, config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
message_component
|
||||||
|
.create_interaction_response(&ctx, |f| {
|
||||||
|
f.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.custom_id("DICTIONARY_REMOVED")
|
||||||
|
.content("辞書を削除しました")
|
||||||
|
.components(|c| c)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_BUTTON" => {
|
||||||
|
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_server_config_or_default(message_component.guild_id.unwrap().0)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
message_component
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.custom_id("TTS_CONFIG_SERVER_REMOVE_DICTIONARY")
|
||||||
|
.content("削除する辞書内容を選択してください")
|
||||||
|
.components(|c| {
|
||||||
|
c.create_action_row(|a| {
|
||||||
|
a.create_select_menu(|s| {
|
||||||
|
s.custom_id(
|
||||||
|
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU",
|
||||||
|
)
|
||||||
|
.options(|o| {
|
||||||
|
let mut o = o;
|
||||||
|
for (i, rule) in config
|
||||||
|
.dictionary
|
||||||
|
.rules
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
o = o.create_option(|c| {
|
||||||
|
c.label(rule.id.clone())
|
||||||
|
.value(i)
|
||||||
|
.description(format!(
|
||||||
|
"{} -> {}",
|
||||||
|
rule.rule.clone(),
|
||||||
|
rule.to.clone()
|
||||||
|
))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
o
|
||||||
|
})
|
||||||
|
.max_values(1)
|
||||||
|
.min_values(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"TTS_CONFIG_SERVER_SHOW_DICTIONARY_BUTTON" => {
|
||||||
|
let config = {
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
let database = data_read
|
||||||
|
.get::<DatabaseClientData>()
|
||||||
|
.expect("Cannot get DatabaseClientData")
|
||||||
|
.clone();
|
||||||
|
let mut database = database.lock().await;
|
||||||
|
database
|
||||||
|
.get_server_config_or_default(message_component.guild_id.unwrap().0)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
message_component
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.custom_id("DICTIONARY_LIST").content("").embed(|e| {
|
||||||
|
e.title("辞書一覧");
|
||||||
|
for rule in config.dictionary.rules {
|
||||||
|
e.field(
|
||||||
|
rule.id,
|
||||||
|
format!("{} -> {}", rule.rule, rule.to),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
e
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON" => {
|
||||||
|
message_component
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.kind(InteractionResponseType::Modal)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.custom_id("TTS_CONFIG_SERVER_ADD_DICTIONARY")
|
||||||
|
.title("辞書追加")
|
||||||
|
.components(|c| {
|
||||||
|
c.create_action_row(|a| {
|
||||||
|
a.create_input_text(|i| {
|
||||||
|
i.style(InputTextStyle::Short)
|
||||||
|
.label("Rule name")
|
||||||
|
.custom_id("rule_name")
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.create_action_row(|a| {
|
||||||
|
a.create_input_text(|i| {
|
||||||
|
i.style(InputTextStyle::Paragraph)
|
||||||
|
.label("From")
|
||||||
|
.custom_id("from")
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.create_action_row(|a| {
|
||||||
|
a.create_input_text(|i| {
|
||||||
|
i.style(InputTextStyle::Short)
|
||||||
|
.label("To")
|
||||||
|
.custom_id("to")
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"TTS_CONFIG_SERVER" => {
|
||||||
|
message_component
|
||||||
|
.create_interaction_response(&ctx.http, |f| {
|
||||||
|
f.kind(InteractionResponseType::UpdateMessage)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.content("サーバー設定")
|
||||||
|
.custom_id("TTS_CONFIG_SERVER")
|
||||||
|
.components(|c| {
|
||||||
|
c.create_action_row(|a| {
|
||||||
|
a.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
"TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON",
|
||||||
|
)
|
||||||
|
.label("辞書を追加")
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_BUTTON",
|
||||||
|
)
|
||||||
|
.label("辞書を削除")
|
||||||
|
.style(ButtonStyle::Danger)
|
||||||
|
})
|
||||||
|
.create_button(|b| {
|
||||||
|
b.custom_id(
|
||||||
|
"TTS_CONFIG_SERVER_SHOW_DICTIONARY_BUTTON",
|
||||||
|
)
|
||||||
|
.label("辞書一覧")
|
||||||
|
.style(ButtonStyle::Primary)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(v) = message_component.data.values.get(0) {
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
|
||||||
|
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 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 {
|
if config_changed {
|
||||||
Some(channel_id) => {
|
let database = data_read
|
||||||
if instance.voice_channel == channel_id {
|
.get::<DatabaseClientData>()
|
||||||
message = Some(format!("{} さんが通話に参加しました", new.member.unwrap().read_name()));
|
.expect("Cannot get DatabaseClientData")
|
||||||
}
|
.clone();
|
||||||
}
|
let mut database = database.lock().await;
|
||||||
None => {
|
database
|
||||||
return;
|
.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) {
|
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
||||||
|
events::voice_state_update::voice_state_update(ctx, old, new).await
|
||||||
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")
|
|
||||||
});
|
|
||||||
commands.create_application_command(|command| {
|
|
||||||
command.name("config")
|
|
||||||
})
|
|
||||||
}).await;
|
|
||||||
println!("{:?}", commands);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
src/events/message_receive.rs
Normal file
44
src/events/message_receive.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.content.starts_with(";") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.read(message, &ctx).await;
|
||||||
|
}
|
||||||
|
}
|
3
src/events/mod.rs
Normal file
3
src/events/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod message_receive;
|
||||||
|
pub mod ready;
|
||||||
|
pub mod voice_state_update;
|
32
src/events/ready.rs
Normal file
32
src/events/ready.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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_option(|o| {
|
||||||
|
o.name("mode")
|
||||||
|
.description("TTS channel")
|
||||||
|
.add_string_choice("Text Channel", "TEXT_CHANNEL")
|
||||||
|
.add_string_choice("New Thread", "NEW_THREAD")
|
||||||
|
.add_string_choice("Voice Channel", "VOICE_CHANNEL")
|
||||||
|
.kind(serenity::model::prelude::command::CommandOptionType::String)
|
||||||
|
.required(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.create_application_command(|command| command.name("config").description("Config"))
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command.name("skip").description("skip tts message")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
86
src/events/voice_state_update.rs
Normal file
86
src/events/voice_state_update.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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 {
|
||||||
|
let _ = storage
|
||||||
|
.get(&guild_id)
|
||||||
|
.unwrap()
|
||||||
|
.text_channel
|
||||||
|
.edit_thread(&ctx.http, |f| f.archived(true))
|
||||||
|
.await;
|
||||||
|
storage.remove(&guild_id);
|
||||||
|
|
||||||
|
let manager = songbird::get(&ctx)
|
||||||
|
.await
|
||||||
|
.expect("Cannot get songbird client.")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
manager.remove(guild_id.0).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,4 +8,4 @@ impl ReadName for Member {
|
|||||||
fn read_name(&self) -> String {
|
fn read_name(&self) -> String {
|
||||||
self.nick.clone().unwrap_or(self.user.name.clone())
|
self.nick.clone().unwrap_or(self.user.name.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,51 @@
|
|||||||
use std::{path::Path, fs::File, io::Write, env};
|
use std::{env, fs::File, io::Write};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serenity::{prelude::Context, model::prelude::Message};
|
use regex::Regex;
|
||||||
|
use serenity::{model::prelude::Message, prelude::Context};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{TTSClientData, DatabaseClientData},
|
data::{DatabaseClientData, TTSClientData},
|
||||||
tts::{
|
tts::{
|
||||||
|
gcp_tts::structs::{
|
||||||
|
audio_config::AudioConfig, synthesis_input::SynthesisInput,
|
||||||
|
synthesize_request::SynthesizeRequest,
|
||||||
|
},
|
||||||
instance::TTSInstance,
|
instance::TTSInstance,
|
||||||
message::TTSMessage,
|
message::TTSMessage,
|
||||||
gcp_tts::structs::{
|
tts_type::TTSType,
|
||||||
audio_config::AudioConfig, synthesis_input::SynthesisInput, synthesize_request::SynthesizeRequest
|
|
||||||
}, tts_type::{self, TTSType}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TTSMessage for Message {
|
impl TTSMessage for Message {
|
||||||
async fn parse(&self, instance: &mut TTSInstance, _: &Context) -> String {
|
async fn parse(&self, instance: &mut TTSInstance, ctx: &Context) -> String {
|
||||||
let res = if let Some(before_message) = &instance.before_message {
|
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_server_config_or_default(instance.guild.0)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let mut text = self.content.clone();
|
||||||
|
for rule in config.dictionary.rules {
|
||||||
|
if rule.is_regex {
|
||||||
|
let regex = Regex::new(&rule.rule).unwrap();
|
||||||
|
text = regex.replace_all(&text, rule.to).to_string();
|
||||||
|
} else {
|
||||||
|
text = text.replace(&rule.rule, &rule.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut res = if let Some(before_message) = &instance.before_message {
|
||||||
if before_message.author.id == self.author.id {
|
if before_message.author.id == self.author.id {
|
||||||
self.content.clone()
|
text.clone()
|
||||||
} else {
|
} else {
|
||||||
let member = self.member.clone();
|
let member = self.member.clone();
|
||||||
let name = if let Some(member) = member {
|
let name = if let Some(member) = member {
|
||||||
@ -27,7 +53,7 @@ impl TTSMessage for Message {
|
|||||||
} else {
|
} else {
|
||||||
self.author.name.clone()
|
self.author.name.clone()
|
||||||
};
|
};
|
||||||
format!("{} さんの発言<break time=\"200ms\"/>{}", name, self.content)
|
format!("{}さんの発言<break time=\"200ms\"/>{}", name, text)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let member = self.member.clone();
|
let member = self.member.clone();
|
||||||
@ -36,9 +62,17 @@ impl TTSMessage for Message {
|
|||||||
} else {
|
} else {
|
||||||
self.author.name.clone()
|
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());
|
instance.before_message = Some(self.clone());
|
||||||
|
|
||||||
res
|
res
|
||||||
@ -48,34 +82,51 @@ impl TTSMessage for Message {
|
|||||||
let text = self.parse(instance, ctx).await;
|
let text = self.parse(instance, ctx).await;
|
||||||
|
|
||||||
let data_read = ctx.data.read().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 mut tts = storage.lock().await;
|
||||||
|
|
||||||
let config = {
|
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;
|
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) {
|
let audio = match config.tts_type.unwrap_or(TTSType::GCP) {
|
||||||
TTSType::GCP => {
|
TTSType::GCP => tts
|
||||||
tts.0.synthesize(SynthesizeRequest {
|
.0
|
||||||
|
.synthesize(SynthesizeRequest {
|
||||||
input: SynthesisInput {
|
input: SynthesisInput {
|
||||||
text: None,
|
text: None,
|
||||||
ssml: Some(format!("<speak>{}</speak>", text))
|
ssml: Some(format!("<speak>{}</speak>", text)),
|
||||||
},
|
},
|
||||||
voice: config.gcp_tts_voice.unwrap(),
|
voice: config.gcp_tts_voice.unwrap(),
|
||||||
audioConfig: AudioConfig {
|
audioConfig: AudioConfig {
|
||||||
audioEncoding: String::from("mp3"),
|
audioEncoding: String::from("mp3"),
|
||||||
speakingRate: 1.2f32,
|
speakingRate: 1.2f32,
|
||||||
pitch: 1.0f32
|
pitch: 1.0f32,
|
||||||
}
|
},
|
||||||
}).await.unwrap()
|
})
|
||||||
}
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
|
||||||
TTSType::VOICEVOX => {
|
TTSType::VOICEVOX => tts
|
||||||
tts.1.synthesize(text.replace("<break time=\"200ms\"/>", "、"), config.voicevox_speaker.unwrap_or(1)).await.unwrap()
|
.1
|
||||||
}
|
.synthesize(
|
||||||
|
text.replace("<break time=\"200ms\"/>", "、"),
|
||||||
|
config.voicevox_speaker.unwrap_or(1),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
pub mod member_name;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod member_name;
|
pub mod voice_move_state;
|
||||||
|
52
src/implement/voice_move_state.rs
Normal file
52
src/implement/voice_move_state.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
src/main.rs
77
src/main.rs
@ -1,23 +1,27 @@
|
|||||||
use std::{sync::Arc, collections::HashMap};
|
mod commands;
|
||||||
|
|
||||||
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 config;
|
mod config;
|
||||||
mod event_handler;
|
|
||||||
mod tts;
|
|
||||||
mod implement;
|
|
||||||
mod data;
|
mod data;
|
||||||
mod database;
|
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
|
/// Create discord client
|
||||||
///
|
///
|
||||||
@ -28,16 +32,12 @@ mod database;
|
|||||||
/// client.start().await;
|
/// client.start().await;
|
||||||
/// ```
|
/// ```
|
||||||
async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, serenity::Error> {
|
async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, serenity::Error> {
|
||||||
let framework = StandardFramework::new()
|
let framework = StandardFramework::new().configure(|c| c.with_whitespace(true).prefix(prefix));
|
||||||
.configure(|c| c
|
|
||||||
.with_whitespace(true)
|
|
||||||
.prefix(prefix));
|
|
||||||
|
|
||||||
Client::builder(token)
|
Client::builder(token, GatewayIntents::all())
|
||||||
.event_handler(Handler)
|
.event_handler(Handler)
|
||||||
.application_id(id)
|
.application_id(id)
|
||||||
.framework(framework)
|
.framework(framework)
|
||||||
.intents(GatewayIntents::all())
|
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -45,24 +45,43 @@ async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, ser
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Load config
|
// Load config
|
||||||
let config = std::fs::read_to_string("./config.toml").expect("Cannot read config file.");
|
let config = {
|
||||||
let config: Config = toml::from_str(&config).expect("Cannot load config file.");
|
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
|
// 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
|
// Create GCP TTS client
|
||||||
let tts = match TTS::new("./credentials.json".to_string()).await {
|
let tts = match TTS::new("./credentials.json".to_string()).await {
|
||||||
Ok(tts) => tts,
|
Ok(tts) => tts,
|
||||||
Err(err) => panic!("{}", err)
|
Err(err) => panic!("GCP init error: {}", err),
|
||||||
};
|
};
|
||||||
|
|
||||||
let voicevox = VOICEVOX::new(config.voicevox_key);
|
let voicevox = VOICEVOX::new(config.voicevox_key);
|
||||||
|
|
||||||
let database_client = {
|
let database_client = {
|
||||||
let redis_client = redis::Client::open(config.redis_url).unwrap();
|
let redis_client = redis::Client::open(config.redis_url).unwrap();
|
||||||
let con = redis_client.get_connection().unwrap();
|
Database::new(redis_client)
|
||||||
Database::new(con)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create TTS storage
|
// Create TTS storage
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
use gcp_auth::Token;
|
|
||||||
use crate::tts::gcp_tts::structs::{
|
use crate::tts::gcp_tts::structs::{
|
||||||
synthesize_request::SynthesizeRequest,
|
synthesize_request::SynthesizeRequest, synthesize_response::SynthesizeResponse,
|
||||||
synthesize_response::SynthesizeResponse,
|
|
||||||
};
|
};
|
||||||
|
use gcp_auth::Token;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TTS {
|
pub struct TTS {
|
||||||
pub token: Token,
|
pub token: Token,
|
||||||
pub credentials_path: String
|
pub credentials_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TTS {
|
impl TTS {
|
||||||
|
|
||||||
pub async fn update_token(&mut self) -> Result<(), gcp_auth::Error> {
|
pub async fn update_token(&mut self) -> Result<(), gcp_auth::Error> {
|
||||||
if self.token.has_expired() {
|
if self.token.has_expired() {
|
||||||
let authenticator = gcp_auth::from_credentials_file(self.credentials_path.clone()).await?;
|
let authenticator =
|
||||||
let token = authenticator.get_token(&["https://www.googleapis.com/auth/cloud-platform"]).await?;
|
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;
|
self.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,11 +25,13 @@ impl TTS {
|
|||||||
|
|
||||||
pub async fn new(credentials_path: String) -> Result<TTS, gcp_auth::Error> {
|
pub async fn new(credentials_path: String) -> Result<TTS, gcp_auth::Error> {
|
||||||
let authenticator = gcp_auth::from_credentials_file(credentials_path.clone()).await?;
|
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 {
|
Ok(TTS {
|
||||||
token,
|
token,
|
||||||
credentials_path
|
credentials_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,19 +56,29 @@ impl TTS {
|
|||||||
/// }
|
/// }
|
||||||
/// }).await.unwrap();
|
/// }).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();
|
self.update_token().await.unwrap();
|
||||||
let client = reqwest::Client::new();
|
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::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())
|
.body(serde_json::to_string(&request).unwrap())
|
||||||
.send().await {
|
.send()
|
||||||
Ok(ok) => {
|
.await
|
||||||
let response: SynthesizeResponse = serde_json::from_str(&ok.text().await.expect("")).unwrap();
|
{
|
||||||
Ok(base64::decode(response.audioContent).unwrap()[..].to_vec())
|
Ok(ok) => {
|
||||||
},
|
let response: SynthesizeResponse =
|
||||||
Err(err) => Err(Box::new(err))
|
serde_json::from_str(&ok.text().await.expect("")).unwrap();
|
||||||
|
Ok(base64::decode(response.audioContent).unwrap()[..].to_vec())
|
||||||
|
}
|
||||||
|
Err(err) => Err(Box::new(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
pub mod gcp_tts;
|
pub mod gcp_tts;
|
||||||
pub mod structs;
|
pub mod structs;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -13,5 +13,5 @@ use serde::{Serialize, Deserialize};
|
|||||||
pub struct AudioConfig {
|
pub struct AudioConfig {
|
||||||
pub audioEncoding: String,
|
pub audioEncoding: String,
|
||||||
pub speakingRate: f32,
|
pub speakingRate: f32,
|
||||||
pub pitch: f32
|
pub pitch: f32,
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub mod audio_config;
|
pub mod audio_config;
|
||||||
pub mod synthesis_input;
|
pub mod synthesis_input;
|
||||||
pub mod synthesize_request;
|
pub mod synthesize_request;
|
||||||
|
pub mod synthesize_response;
|
||||||
pub mod voice_selection_params;
|
pub mod voice_selection_params;
|
||||||
pub mod synthesize_response;
|
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -10,5 +10,5 @@ use serde::{Serialize, Deserialize};
|
|||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct SynthesisInput {
|
pub struct SynthesisInput {
|
||||||
pub text: Option<String>,
|
pub text: Option<String>,
|
||||||
pub ssml: Option<String>
|
pub ssml: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use crate::tts::gcp_tts::structs::{
|
use crate::tts::gcp_tts::structs::{
|
||||||
synthesis_input::SynthesisInput,
|
audio_config::AudioConfig, synthesis_input::SynthesisInput,
|
||||||
audio_config::AudioConfig,
|
|
||||||
voice_selection_params::VoiceSelectionParams,
|
voice_selection_params::VoiceSelectionParams,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -30,4 +29,4 @@ pub struct SynthesizeRequest {
|
|||||||
pub input: SynthesisInput,
|
pub input: SynthesisInput,
|
||||||
pub voice: VoiceSelectionParams,
|
pub voice: VoiceSelectionParams,
|
||||||
pub audioConfig: AudioConfig,
|
pub audioConfig: AudioConfig,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct SynthesizeResponse {
|
pub struct SynthesizeResponse {
|
||||||
pub audioContent: String
|
pub audioContent: String,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -13,5 +13,5 @@ use serde::{Serialize, Deserialize};
|
|||||||
pub struct VoiceSelectionParams {
|
pub struct VoiceSelectionParams {
|
||||||
pub languageCode: String,
|
pub languageCode: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub ssmlGender: String
|
pub ssmlGender: String,
|
||||||
}
|
}
|
||||||
|
@ -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 struct TTSInstance {
|
||||||
pub before_message: Option<Message>,
|
pub before_message: Option<Message>,
|
||||||
pub text_channel: ChannelId,
|
pub text_channel: ChannelId,
|
||||||
pub voice_channel: ChannelId,
|
pub voice_channel: ChannelId,
|
||||||
pub guild: GuildId
|
pub guild: GuildId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TTSInstance {
|
impl TTSInstance {
|
||||||
@ -17,7 +23,8 @@ impl TTSInstance {
|
|||||||
/// instance.read(message, &ctx).await;
|
/// instance.read(message, &ctx).await;
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn read<T>(&mut self, message: T, ctx: &Context)
|
pub async fn read<T>(&mut self, message: T, ctx: &Context)
|
||||||
where T: TTSMessage
|
where
|
||||||
|
T: TTSMessage,
|
||||||
{
|
{
|
||||||
let path = message.synthesize(self, ctx).await;
|
let path = message.synthesize(self, ctx).await;
|
||||||
|
|
||||||
@ -25,8 +32,18 @@ impl TTSInstance {
|
|||||||
let manager = songbird::get(&ctx).await.unwrap();
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let call = manager.get(self.guild).unwrap();
|
let call = manager.get(self.guild).unwrap();
|
||||||
let mut call = call.lock().await;
|
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);
|
call.enqueue_source(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn skip(&mut self, ctx: &Context) {
|
||||||
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
|
let call = manager.get(self.guild).unwrap();
|
||||||
|
let call = call.lock().await;
|
||||||
|
let queue = call.queue();
|
||||||
|
let _ = queue.skip();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
use std::{path::Path, fs::File, io::Write, env};
|
use std::{env, fs::File, io::Write};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serenity::prelude::Context;
|
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.
|
/// Message trait that can be used to synthesize text to speech.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TTSMessage {
|
pub trait TTSMessage {
|
||||||
|
|
||||||
/// Parse the message for synthesis.
|
/// Parse the message for synthesis.
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
@ -34,33 +36,43 @@ pub struct AnnounceMessage {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TTSMessage for AnnounceMessage {
|
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;
|
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 {
|
async fn synthesize(&self, instance: &mut TTSInstance, ctx: &Context) -> String {
|
||||||
let text = self.parse(instance, ctx).await;
|
let text = self.parse(instance, ctx).await;
|
||||||
let data_read = ctx.data.read().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 mut storage = storage.lock().await;
|
||||||
|
|
||||||
let audio = storage.0.synthesize(SynthesizeRequest {
|
let audio = storage
|
||||||
input: SynthesisInput {
|
.0
|
||||||
text: None,
|
.synthesize(SynthesizeRequest {
|
||||||
ssml: Some(text)
|
input: SynthesisInput {
|
||||||
},
|
text: None,
|
||||||
voice: VoiceSelectionParams {
|
ssml: Some(text),
|
||||||
languageCode: String::from("ja-JP"),
|
},
|
||||||
name: String::from("ja-JP-Wavenet-B"),
|
voice: VoiceSelectionParams {
|
||||||
ssmlGender: String::from("neutral")
|
languageCode: String::from("ja-JP"),
|
||||||
},
|
name: String::from("ja-JP-Wavenet-B"),
|
||||||
audioConfig: AudioConfig {
|
ssmlGender: String::from("neutral"),
|
||||||
audioEncoding: String::from("mp3"),
|
},
|
||||||
speakingRate: 1.2f32,
|
audioConfig: AudioConfig {
|
||||||
pitch: 1.0f32
|
audioEncoding: String::from("mp3"),
|
||||||
}
|
speakingRate: 1.2f32,
|
||||||
}).await.unwrap();
|
pitch: 1.0f32,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
let uuid = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
@ -72,4 +84,4 @@ impl TTSMessage for AnnounceMessage {
|
|||||||
|
|
||||||
file_path.into_os_string().into_string().unwrap()
|
file_path.into_os_string().into_string().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub mod gcp_tts;
|
pub mod gcp_tts;
|
||||||
pub mod voicevox;
|
pub mod instance;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod tts_type;
|
pub mod tts_type;
|
||||||
pub mod instance;
|
pub mod voicevox;
|
||||||
|
@ -3,5 +3,5 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum TTSType {
|
pub enum TTSType {
|
||||||
GCP,
|
GCP,
|
||||||
VOICEVOX
|
VOICEVOX,
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
pub mod structs;
|
pub mod structs;
|
||||||
pub mod voicevox;
|
pub mod voicevox;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::mora::Mora;
|
use super::mora::Mora;
|
||||||
|
|
||||||
@ -7,5 +7,5 @@ pub struct AccentPhrase {
|
|||||||
pub moras: Vec<Mora>,
|
pub moras: Vec<Mora>,
|
||||||
pub accent: f64,
|
pub accent: f64,
|
||||||
pub pause_mora: Option<Mora>,
|
pub pause_mora: Option<Mora>,
|
||||||
pub is_interrogative: bool
|
pub is_interrogative: bool,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::accent_phrase::AccentPhrase;
|
use super::accent_phrase::AccentPhrase;
|
||||||
|
|
||||||
@ -14,5 +14,5 @@ pub struct AudioQuery {
|
|||||||
pub postPhonemeLength: f64,
|
pub postPhonemeLength: f64,
|
||||||
pub outputSamplingRate: f64,
|
pub outputSamplingRate: f64,
|
||||||
pub outputStereo: bool,
|
pub outputStereo: bool,
|
||||||
pub kana: Option<String>
|
pub kana: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
pub mod mora;
|
pub mod accent_phrase;
|
||||||
pub mod audio_query;
|
pub mod audio_query;
|
||||||
pub mod accent_phrase;
|
pub mod mora;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Mora {
|
pub struct Mora {
|
||||||
@ -7,5 +7,5 @@ pub struct Mora {
|
|||||||
pub consonant_length: Option<f64>,
|
pub consonant_length: Option<f64>,
|
||||||
pub vowel: String,
|
pub vowel: String,
|
||||||
pub vowel_length: f64,
|
pub vowel_length: f64,
|
||||||
pub pitch: f64
|
pub pitch: f64,
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,65 @@ const API_URL: &str = "https://api.su-shiki.com/v2/voicevox/audio";
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VOICEVOX {
|
pub struct VOICEVOX {
|
||||||
pub key: String
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VOICEVOX {
|
impl VOICEVOX {
|
||||||
pub fn new(key: String) -> Self {
|
pub fn get_speakers() -> Vec<(String, i64)> {
|
||||||
Self {
|
vec![
|
||||||
key
|
("四国めたん - ノーマル".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),
|
||||||
|
("ナースロボ_タイプT - ノーマル".to_string(), 47),
|
||||||
|
("ナースロボ_タイプT - 楽々".to_string(), 48),
|
||||||
|
("ナースロボ_タイプT - 恐怖".to_string(), 49),
|
||||||
|
("ナースロボ_タイプT - 内緒話".to_string(), 50),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
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) => {
|
Ok(response) => {
|
||||||
let body = response.bytes().await?;
|
let body = response.bytes().await?;
|
||||||
Ok(body.to_vec())
|
Ok(body.to_vec())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => Err(Box::new(err)),
|
||||||
Err(Box::new(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user