use anyhow::{Context, Result};
use reqwest::Client;
use serde::Deserialize;
use std::time::Duration;
use super::openai_tts::build_endpoint_url;
const LIVENESS_TIMEOUT: Duration = Duration::from_secs(2);
pub async fn probe_liveness(base_url: &str) -> Result<()> {
let url = build_endpoint_url(base_url, "")?;
let client = Client::builder()
.timeout(LIVENESS_TIMEOUT)
.build()
.context("Failed to build liveness probe client")?;
client
.get(&url)
.send()
.await
.with_context(|| format!("Voicebox liveness probe failed at {url}"))?;
Ok(())
}
pub async fn transcribe(audio_bytes: Vec<u8>, base_url: &str) -> Result<String> {
if let Err(e) = probe_liveness(base_url).await {
anyhow::bail!("Voicebox STT unreachable (liveness probe failed): {e}");
}
let transcribe_url = build_endpoint_url(base_url, "transcribe")?;
let client = Client::new();
let file_part = reqwest::multipart::Part::bytes(audio_bytes)
.file_name("voice.ogg")
.mime_str("audio/ogg")?;
let form = reqwest::multipart::Form::new().part("file", file_part);
let response = client
.post(&transcribe_url)
.multipart(form)
.send()
.await
.context("Failed to send audio to Voicebox STT")?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!(
"Voicebox STT error ({}): {}",
status,
translate_voicebox_error(&error_text),
);
}
let result: TranscribeResponse = response
.json()
.await
.context("Failed to parse Voicebox STT response")?;
tracing::info!(
"Voicebox STT: transcribed {} chars (url={})",
result.text.len(),
transcribe_url,
);
Ok(result.text)
}
#[derive(Deserialize)]
struct TranscribeResponse {
text: String,
}
pub(crate) fn translate_voicebox_error(raw: &str) -> String {
if raw.contains("Cannot load imports from non-existent stub") && raw.contains("__init__.pyi") {
let pkg = extract_package_from_stub_error(raw).unwrap_or_else(|| "<unknown>".to_string());
return format!(
"Voicebox is missing the `{pkg}` runtime stubs (likely a PyInstaller \
bundle issue — lazy_loader needs the `__init__.pyi` files at runtime \
but the build stripped them). Rebuild voicebox with \
`--collect-data {pkg}` and a `hooks/hook-lazy_loader.py` that sets \
`hiddenimports = ['lazy_loader']`. Original detail: {raw}"
);
}
raw.to_string()
}
fn extract_package_from_stub_error(raw: &str) -> Option<String> {
let after_marker = raw.split("__init__.pyi").next()?;
let path_part = after_marker.rsplit(['\'', '"']).next()?;
let mei_split = path_part.split("_MEI").nth(1)?;
let mut segments = mei_split.split('/').filter(|s| !s.is_empty());
let _suffix = segments.next();
segments.next().map(|s| s.to_string())
}