summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSanto Cariotti <santo@dcariotti.me>2024-09-13 13:04:16 +0200
committerSanto Cariotti <santo@dcariotti.me>2024-09-13 13:04:16 +0200
commit28f4a1be06e29bb3bbb3dbf4f9307308783450ab (patch)
treecf006f2d02e31b02e9636228475095cddfb4c470
parent50f16bb68fb6b28a1049ca25bb4273847c77ea93 (diff)
Text-to-speach API for alert sound generation
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--README.md4
-rw-r--r--assets/sounds/readme.md1
-rw-r--r--docker-compose.yml1
-rw-r--r--k8s/cas-deployment.yaml5
-rw-r--r--k8s/cas-secret.yaml1
-rw-r--r--src/audio.rs86
-rw-r--r--src/config.rs3
-rw-r--r--src/graphql/types/alert.rs40
-rw-r--r--src/main.rs4
12 files changed, 147 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..0fc8cc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+assets/sounds/*.mp3
diff --git a/Cargo.lock b/Cargo.lock
index 2e8d7b9..d0c6ecc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -420,6 +420,7 @@ dependencies = [
"lazy_static",
"once_cell",
"postgis",
+ "reqwest",
"serde",
"serde_json",
"sha256",
diff --git a/Cargo.toml b/Cargo.toml
index 54a15ad..7bca3d3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,3 +31,4 @@ sha256 = "1.5.0"
axum-extra = { version = "0.9.3", features = ["typed-header"] }
bytes = "1"
expo_push_notification_client = "0.4.1"
+reqwest = { version = "0.12.7", features = ["json"] }
diff --git a/README.md b/README.md
index ea71497..50a7acf 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,9 @@ Now you set up some env variables:
- `EXPO_ACCESS_TOKEN`: used by the [Expo](https://expo.dev) API access.
+- `UNREALSPEECH_TOKEN`: used by [Unrealspeech](https://unrealspeech.com) for
+ text-to-speach API.
+
After that you must copy the `schema/init.sql` file into the database.
Now just run the app
@@ -64,6 +67,7 @@ docker run \
-e JWT_SECRET=... \
-e ALLOWED_HOST=... \
-e EXPO_ACCESS_TOKEN ... \
+ -e UNREALSPEECH_TOKEN ... \
cas:latest
```
diff --git a/assets/sounds/readme.md b/assets/sounds/readme.md
new file mode 100644
index 0000000..e4ffb4a
--- /dev/null
+++ b/assets/sounds/readme.md
@@ -0,0 +1 @@
+Here is the sounds list
diff --git a/docker-compose.yml b/docker-compose.yml
index 163890c..36459d3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,6 +27,7 @@ services:
- JWT_SECRET=${JWT_SECRET}
- ALLOWED_HOST=${ALLOWED_HOST}
- EXPO_ACCESS_TOKEN=${EXPO_ACCESS_TOKEN}
+ - UNREALSPEECH_TOKEN=${UNREALSPEECH_TOKEN}
depends_on:
- postgres
diff --git a/k8s/cas-deployment.yaml b/k8s/cas-deployment.yaml
index f070fe4..fc0289a 100644
--- a/k8s/cas-deployment.yaml
+++ b/k8s/cas-deployment.yaml
@@ -37,6 +37,11 @@ spec:
secretKeyRef:
name: cas-secret
key: EXPO_ACCESS_TOKEN
+ - name: UNREALSPEECH_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: cas-secret
+ key: UNREALSPEECH_TOKEN
- name: ALLOWED_HOST
valueFrom:
configMapKeyRef:
diff --git a/k8s/cas-secret.yaml b/k8s/cas-secret.yaml
index 268c119..a2fc3e3 100644
--- a/k8s/cas-secret.yaml
+++ b/k8s/cas-secret.yaml
@@ -6,3 +6,4 @@ type: Opaque
data:
JWT_SECRET: ${JWT_SECRET}
EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN}
+ UNREALSPEECH_TOKEN: ${UNREALSPEECH_TOKEN}
diff --git a/src/audio.rs b/src/audio.rs
new file mode 100644
index 0000000..64ee223
--- /dev/null
+++ b/src/audio.rs
@@ -0,0 +1,86 @@
+use crate::config::CONFIG;
+use axum::{
+ extract::Path,
+ http::{HeaderMap, HeaderName, HeaderValue, StatusCode},
+};
+use reqwest::header::AUTHORIZATION;
+use std::{
+ fs::{self, File},
+ io::Write,
+ path::Path as StdPath,
+};
+
+/// Create a new sound from a text
+pub async fn tts(text: String, filename: String) -> Result<(), String> {
+ let url = "https://api.v7.unrealspeech.com/stream";
+ let api_key = format!("Bearer {}", CONFIG.unrealspeech_token);
+ let filepath = format!("./assets/sounds/{}", filename);
+
+ // Request JSON body
+ let body = serde_json::json!({
+ "Text": text,
+ "VoiceId": "Will",
+ "Bitrate": "192k",
+ "Speed": "0",
+ "Pitch": "0.92",
+ "Codec": "libmp3lame",
+ });
+
+ // Send POST request
+ let client = reqwest::Client::new();
+ let response = client
+ .post(url)
+ .header(AUTHORIZATION, api_key)
+ .json(&body)
+ .send()
+ .await
+ .unwrap();
+
+ // Check for successful response
+ if response.status().is_success() {
+ let mut file = File::create(filepath).unwrap();
+ let content = response.bytes().await.unwrap();
+ let _ = file.write_all(&content);
+ Ok(())
+ } else {
+ Err(format!("Failed to fetch the audio: {}", response.status()))
+ }
+}
+
+/// Axum endpoint which shows files
+pub async fn show_file(
+ Path(id): Path<String>,
+) -> Result<(HeaderMap, Vec<u8>), (StatusCode, String)> {
+ let index = id.find('.').unwrap_or(usize::MAX);
+
+ let mut ext_name = "xxx";
+ if index != usize::MAX {
+ ext_name = &id[index + 1..];
+ }
+
+ let mut headers = HeaderMap::new();
+
+ if ["mp3"].contains(&ext_name) {
+ let content_type = "audio/mpeg";
+ headers.insert(
+ HeaderName::from_static("content-type"),
+ HeaderValue::from_str(content_type).unwrap(),
+ );
+ }
+
+ let file_name = format!("./assets/sounds/{}", id);
+ let file_path = StdPath::new(&file_name);
+
+ if !file_path.exists() {
+ return Err((StatusCode::NOT_FOUND, "File not found".to_string()));
+ }
+
+ // Read the file and return its content
+ match fs::read(file_path) {
+ Ok(file_content) => Ok((headers, file_content)),
+ Err(_) => Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "Failed to read file".to_string(),
+ )),
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index a703010..763b852 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -19,6 +19,9 @@ pub struct Configuration {
/// Token used by Expo API to send a notification
pub expo_access_token: String,
+
+ /// Token used for text-to-speach API
+ pub unrealspeech_token: String,
}
impl Configuration {
diff --git a/src/graphql/types/alert.rs b/src/graphql/types/alert.rs
index 6be723c..2ee0087 100644
--- a/src/graphql/types/alert.rs
+++ b/src/graphql/types/alert.rs
@@ -132,6 +132,8 @@ pub mod query {
}
pub mod mutations {
+ use crate::audio;
+
use super::*;
/// Create a new alert
@@ -330,6 +332,44 @@ pub mod mutations {
.await
.unwrap();
+ if let Err(e) = audio::tts(
+ alert.text1.clone(),
+ format!("alert-{}-text-1.mp3", alert.id),
+ )
+ .await
+ {
+ eprintln!(
+ "Error for `{}`: {}",
+ format!("alert-{}-text-1.mp3", alert.id),
+ e
+ );
+ }
+
+ if let Err(e) = audio::tts(
+ alert.text2.clone(),
+ format!("alert-{}-text-2.mp3", alert.id),
+ )
+ .await
+ {
+ eprintln!(
+ "Error for `{}`: {}",
+ format!("alert-{}-text-2.mp3", alert.id),
+ e
+ );
+ }
+ if let Err(e) = audio::tts(
+ alert.text3.clone(),
+ format!("alert-{}-text-3.mp3", alert.id),
+ )
+ .await
+ {
+ eprintln!(
+ "Error for `{}`: {}",
+ format!("alert-{}-text-3.mp3", alert.id),
+ e
+ );
+ }
+
Ok(alert)
}
}
diff --git a/src/main.rs b/src/main.rs
index 33718c1..940bb5c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")]
+mod audio;
mod config;
mod db;
mod errors;
@@ -13,7 +14,7 @@ use crate::config::CONFIG;
use async_graphql::{EmptySubscription, Schema};
use axum::{
http::{header, Method, Request},
- routing::post,
+ routing::{get, post},
Extension, Router,
};
use tokio::net::TcpListener;
@@ -45,6 +46,7 @@ async fn create_app() -> Router {
.finish();
Router::new()
+ .route("/assets/sounds/:id", get(audio::show_file))
.route(
"/graphql",
post(graphql::routes::graphql_handler).layer(Extension(schema.clone())),