use std::{io::Write, path::Path};
use cached::proc_macro::cached;
use reqwest::Url;
use sha2::Digest;
use songbird::input::Input;
use tracing::instrument;
use crate::Error;
#[instrument(skip(client))]
pub(crate) async fn tts(
cache_dir: &Path,
client: &reqwest::Client,
endpoint: Url,
text: String,
voice: String,
) -> Result<Input, Error> {
let mut hasher = sha2::Sha256::new();
hasher.update(text.as_bytes());
hasher.update(voice.as_bytes());
let key = hex::encode(hasher.finalize());
let dest_path = cache_dir.join(key);
if dest_path.is_file() {
tracing::info!(file = ?dest_path, "Playing existing TTS file");
Ok(songbird::ffmpeg(dest_path).await?)
} else {
let tts_endpoint = endpoint.join("tts")?;
let response = client
.post(tts_endpoint)
.query(&[
("voice", voice.as_str()),
("noiseScale", "0.667"),
("noiseW", "0.8"),
("lengthScale", "1.0"),
("ssml", "false"),
])
.body(text)
.send()
.await?
.error_for_status()?;
let body = response.bytes().await?;
let mut dest_file = std::fs::File::create(&dest_path)?;
dest_file.write_all(&body)?;
dest_file.sync_all()?;
drop(dest_file);
tracing::info!(file = ?dest_path, "Caching new TTS file");
let source = songbird::ffmpeg(dest_path).await?;
Ok(source)
}
}
#[instrument(skip_all)]
#[cached(
result = true,
key = "String",
convert = r#"{ format!("{}", endpoint) }"#
)]
pub(crate) async fn voices(client: &reqwest::Client, endpoint: Url) -> Result<Vec<String>, Error> {
let voice_endpoint = endpoint.join("voices")?;
tracing::info!(url = %voice_endpoint, "Querying Mimic for available voices");
let response = client
.get(voice_endpoint)
.send()
.await?
.error_for_status()?;
let voices = response
.json::<Vec<serde_json::Value>>()
.await?
.into_iter()
.filter_map(|value| {
let key = &value["key"];
if key.is_null() {
None
} else {
let key = key.to_string().replace('"', "");
if key.starts_with("en") {
tracing::info!(key = %key, "Adding TTS language key");
Some(key)
} else {
tracing::debug!(key = %key, "Ignoring non-english key");
None
}
}
});
Ok(voices.collect())
}