use async_trait::async_trait;
use serde::Serialize;
use tracing::debug;
use crate::WhisrsError;
use super::TtsBackend;
const DEEPGRAM_SPEAK_URL: &str = "https://api.deepgram.com/v1/speak";
pub const DEFAULT_MODEL: &str = "aura-2-thalia-en";
pub struct DeepgramAuraTts {
client: reqwest::Client,
api_key: String,
model: String,
}
impl DeepgramAuraTts {
pub fn new(api_key: String, model: String) -> Self {
let model = if model.trim().is_empty() {
DEFAULT_MODEL.to_string()
} else {
model
};
Self {
client: reqwest::Client::new(),
api_key,
model,
}
}
fn request_body<'a>(&self, text: &'a str) -> SpeechRequest<'a> {
SpeechRequest { text }
}
fn request_url(&self) -> String {
format!(
"{DEEPGRAM_SPEAK_URL}?model={}&encoding=linear16&container=wav",
self.model
)
}
}
#[derive(Debug, Serialize)]
struct SpeechRequest<'a> {
text: &'a str,
}
#[async_trait]
impl TtsBackend for DeepgramAuraTts {
async fn synthesize(&self, text: &str) -> Result<Vec<u8>, WhisrsError> {
if text.trim().is_empty() {
return Err(WhisrsError::Transcription(
"cannot synthesize empty text".to_string(),
));
}
let url = self.request_url();
debug!(
"sending {} chars to Deepgram Aura (model={})",
text.len(),
self.model
);
let response = self
.client
.post(&url)
.header("Authorization", format!("Token {}", self.api_key))
.json(&self.request_body(text))
.send()
.await
.map_err(|e| WhisrsError::Transcription(format!("Deepgram TTS request failed: {e}")))?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(WhisrsError::Transcription(format!(
"Deepgram TTS error ({}): {}",
status.as_u16(),
body
)));
}
let bytes = response.bytes().await.map_err(|e| {
WhisrsError::Transcription(format!("Deepgram TTS read body failed: {e}"))
})?;
Ok(bytes.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_url_has_model_and_wav_params() {
let backend = DeepgramAuraTts::new("test-key".to_string(), "aura-2-thalia-en".to_string());
let url = backend.request_url();
assert!(url.starts_with("https://api.deepgram.com/v1/speak?"));
assert!(url.contains("model=aura-2-thalia-en"));
assert!(url.contains("encoding=linear16"));
assert!(url.contains("container=wav"));
}
#[test]
fn blank_model_falls_back_to_default() {
let backend = DeepgramAuraTts::new("test-key".to_string(), String::new());
assert_eq!(backend.model, DEFAULT_MODEL);
assert!(backend.request_url().contains("model=aura-2-thalia-en"));
}
#[test]
fn request_body_serializes_text_only() {
let backend = DeepgramAuraTts::new("test-key".to_string(), "aura-2-thalia-en".to_string());
let json = serde_json::to_value(backend.request_body("hello world")).unwrap();
assert_eq!(json["text"], "hello world");
assert!(json.get("model").is_none());
}
#[test]
fn auth_header_uses_token_scheme() {
let backend = DeepgramAuraTts::new("secret".to_string(), "aura-2-thalia-en".to_string());
let header = format!("Token {}", backend.api_key);
assert_eq!(header, "Token secret");
}
#[tokio::test]
async fn synthesize_rejects_empty_text() {
let backend = DeepgramAuraTts::new("test-key".to_string(), "aura-2-thalia-en".to_string());
let err = backend.synthesize(" ").await.unwrap_err();
assert!(err.to_string().contains("empty text"));
}
}