use crate::auth::{current_date_string, random_hex};
use crate::connection::Ws;
use crate::error::TtsError;
use crate::event::TtsEvent;
use crate::protocol;
use crate::ssml;
use crate::tts::Engine;
use std::future::Future;
use std::time::Duration;
use tokio_tungstenite::tungstenite::Message;
const OUTPUT_FORMAT: &str = "audio-24khz-48kbitrate-mono-mp3";
const RECEIVE_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
const REQ_ID_BYTES: usize = 16;
const PATH_TURN_END: &str = "turn.end";
const PATH_AUDIO_METADATA: &str = "audio.metadata";
pub struct EdgeTts;
impl Engine for EdgeTts {
fn synthesize(
&self,
text: &str,
voice: &str,
rate: &str,
lang: &str,
) -> impl Future<Output = Result<Vec<TtsEvent>, TtsError>> + Send {
let text = text.to_string();
let voice = voice.to_string();
let rate = rate.to_string();
let lang = lang.to_string();
async move { Self::run_synthesis(&text, &voice, &rate, &lang).await }
}
}
impl EdgeTts {
async fn run_synthesis(
text: &str,
voice: &str,
rate: &str,
lang: &str,
) -> Result<Vec<TtsEvent>, TtsError> {
let mut ws = Ws::connect().await?;
Self::send_config(&mut ws).await?;
Self::send_ssml(&mut ws, text, voice, rate, lang).await?;
Self::receive_events(&mut ws).await
}
async fn send_config(ws: &mut Ws) -> Result<(), TtsError> {
let timestamp = current_date_string();
let config = format!(
"X-Timestamp:{timestamp}\r\n\
Content-Type:application/json; charset=utf-8\r\n\
Path:speech.config\r\n\r\n\
{{\"context\":{{\"synthesis\":{{\"audio\":{{\
\"metadataoptions\":{{\
\"sentenceBoundaryEnabled\":\"false\",\
\"wordBoundaryEnabled\":\"true\"}},\
\"outputFormat\":\"{OUTPUT_FORMAT}\"}}}}}}}}\r\n"
);
ws.send_text(config).await
}
async fn send_ssml(
ws: &mut Ws,
text: &str,
voice: &str,
rate: &str,
lang: &str,
) -> Result<(), TtsError> {
let request_id = random_hex(REQ_ID_BYTES);
let timestamp = current_date_string();
let ssml = ssml::build_ssml(text, voice, rate, lang);
let message = format!(
"X-RequestId:{request_id}\r\n\
Content-Type:application/ssml+xml\r\n\
X-Timestamp:{timestamp}Z\r\n\
Path:ssml\r\n\r\n{ssml}"
);
ws.send_text(message).await
}
async fn receive_events(ws: &mut Ws) -> Result<Vec<TtsEvent>, TtsError> {
let mut events = Vec::new();
let mut got_audio = false;
while let Some(msg) = ws.recv_timeout(RECEIVE_IDLE_TIMEOUT).await? {
match msg {
Message::Binary(bin) => {
log::debug!("edge-tts BIN len={}", bin.len());
if let Some(audio) = protocol::parse_binary_audio(&bin) {
got_audio = true;
events.push(TtsEvent::Audio(audio));
}
}
Message::Text(text) => {
if Self::handle_text_message(&text, &mut events) {
break;
}
}
Message::Close(_) => break,
Message::Ping(data) => ws.send_pong(data).await,
ref other => log::debug!("edge-tts OTHER {other:?}"),
}
}
if !got_audio {
return Err(TtsError::NoAudio);
}
Ok(events)
}
fn handle_text_message(text: &str, events: &mut Vec<TtsEvent>) -> bool {
let (headers, body) = protocol::split_msg(text);
let path = headers.get("Path").map(String::as_str).unwrap_or("");
log::debug!(
"edge-tts TXT path={path} body[..160]={}",
body.chars().take(160).collect::<String>()
);
match path {
PATH_TURN_END => {
events.push(TtsEvent::TurnEnd);
true
}
PATH_AUDIO_METADATA => {
events.extend(protocol::parse_word_boundaries(body));
false
}
_ => false,
}
}
}