use anyhow::Context;
use base64::Engine;
use std::path::Path;
#[derive(Clone)]
pub(crate) struct MediaTranscriber {
api_url: String,
model: String,
provider_route: Option<String>,
}
impl MediaTranscriber {
pub(crate) fn new(api_url: String, model: String, provider_route: Option<String>) -> Self {
Self {
api_url,
model,
provider_route,
}
}
fn chat_url(&self) -> String {
crate::providers::ensure_chat_completions_url(&self.api_url)
}
}
#[derive(Clone)]
pub struct ImageTranscriber {
inner: MediaTranscriber,
}
impl ImageTranscriber {
#[must_use]
pub(crate) const fn from_inner(inner: MediaTranscriber) -> Self {
Self { inner }
}
pub async fn transcribe(&self, image_data_uri: &str) -> anyhow::Result<String> {
let mut body = serde_json::json!({
"model": self.inner.model,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image concisely."},
{"type": "image_url", "image_url": {"url": image_data_uri}}
]
}
],
"max_tokens": 512,
});
if let Some(route) = &self.inner.provider_route
&& let Some(routing) = crate::providers::provider_routing_json(route, false)
{
body["provider"] = routing;
}
let result = crate::util::http::post_json_to_provider(
&self.inner.chat_url(),
&body,
"transcription",
)
.await?;
let text = result["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("")
.trim()
.to_string();
Ok(text)
}
}
#[derive(Clone)]
pub struct AudioTranscriber {
inner: MediaTranscriber,
}
impl AudioTranscriber {
#[must_use]
pub(crate) const fn from_inner(inner: MediaTranscriber) -> Self {
Self { inner }
}
pub async fn transcribe(&self, file_path: &Path) -> anyhow::Result<String> {
let file_bytes = tokio::fs::read(file_path)
.await
.context("failed to read audio file")?;
let format = match file_path.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("oga") => "ogg",
Some(e) => e,
None => "wav",
}
.to_lowercase();
let encoded = base64::engine::general_purpose::STANDARD.encode(&file_bytes);
let mut body = serde_json::json!({
"model": self.inner.model,
"input_audio": {
"data": encoded,
"format": format,
},
});
if let Some(route) = &self.inner.provider_route
&& let Some(routing) = crate::providers::provider_routing_json(route, false)
{
body["provider"] = routing;
}
let base = crate::providers::ensure_base_url(&self.inner.api_url);
let url = format!("{base}/audio/transcriptions");
let json =
crate::util::http::post_json_to_provider(&url, &body, "audio transcription").await?;
json.get("text")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow::anyhow!("empty transcription response"))
}
}