use anyhow::{Context, Result};
use reqwest::Client;
use url::Url;
pub async fn synthesize_speech(
text: &str,
api_key: &str,
voice: &str,
model: &str,
base_url: &str,
) -> Result<Vec<u8>> {
if text.is_empty() {
anyhow::bail!("Cannot synthesize empty text");
}
let speech_url = build_endpoint_url(base_url, "v1/audio/speech")?;
let client = Client::new();
let body = serde_json::json!({
"model": model,
"input": text,
"voice": voice,
"response_format": "opus",
});
let response = client
.post(&speech_url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.context("Failed to send TTS request")?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("TTS error ({}): {}", status, error_text);
}
let audio_bytes = response
.bytes()
.await
.context("Failed to read TTS audio bytes")?
.to_vec();
tracing::info!(
"OpenAI-compatible TTS: generated {} bytes of audio (voice={}, model={}, url={})",
audio_bytes.len(),
voice,
model,
speech_url,
);
Ok(audio_bytes)
}
pub fn build_endpoint_url(base_url: &str, path: &str) -> Result<String> {
let mut url = Url::parse(base_url).context("Invalid base URL")?;
if !url.path().ends_with('/') {
url.set_path(&format!("{}/", url.path()));
}
let joined = url.join(path).context("Failed to join URL path")?;
Ok(joined.to_string())
}