use anyhow::{Context, Result};
use base64::Engine as _;
use reqwest::Client;
use serde::Deserialize;
use super::openai_tts::build_endpoint_url;
pub async fn transcribe_audio(
audio_bytes: Vec<u8>,
api_key: &str,
model: &str,
base_url: &str,
) -> Result<String> {
let transcribe_url = build_endpoint_url(base_url, "v1/audio/transcriptions")?;
let client = Client::new();
let model_owned = model.to_string();
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)
.text("model", model_owned)
.text("response_format", "json");
let response = client
.post(&transcribe_url)
.header("Authorization", format!("Bearer {}", api_key))
.multipart(form)
.send()
.await
.context("Failed to send audio to STT endpoint")?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("STT error ({}): {}", status, error_text);
}
let result: TranscriptionResponse = response
.json()
.await
.context("Failed to parse transcription response")?;
tracing::info!(
"OpenAI-compatible STT: transcribed {} chars (model={}, url={})",
result.text.len(),
model,
transcribe_url,
);
Ok(result.text)
}
pub fn serves_audio_on_chat_route(base_url: &str) -> bool {
crate::brain::provider::opencrabs_install::is_opencrabs_endpoint(Some(base_url))
}
pub async fn transcribe_audio_chat(
audio_bytes: Vec<u8>,
api_key: &str,
model: &str,
base_url: &str,
) -> Result<String> {
let chat_url = build_endpoint_url(base_url, "v1/chat/completions")?;
let audio_b64 = base64::engine::general_purpose::STANDARD.encode(&audio_bytes);
let body = serde_json::json!({
"model": model,
"messages": [{
"role": "user",
"content": [
{
"type": "text",
"text": "Transcribe this audio note verbatim. Reply with the transcription only."
},
{
"type": "input_audio",
"input_audio": { "data": audio_b64, "format": "ogg" }
}
]
}]
});
let response = Client::new()
.post(&chat_url)
.header("Authorization", format!("Bearer {}", api_key))
.json(&body)
.send()
.await
.context("Failed to send audio to the chat transcription route")?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("STT chat-route error ({}): {}", status, error_text);
}
let payload: serde_json::Value = response
.json()
.await
.context("Failed to parse chat transcription response")?;
let message = &payload["choices"][0]["message"];
let text = ["content", "reasoning_content"]
.iter()
.filter_map(|field| message.get(*field).and_then(|value| value.as_str()))
.map(str::trim)
.find(|text| !text.is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"Chat transcription returned no text: content and reasoning_content were both empty"
)
})?;
tracing::info!(
"STT chat route: transcribed {} chars (model={}, url={})",
text.len(),
model,
chat_url,
);
Ok(text.to_string())
}
#[derive(Deserialize)]
struct TranscriptionResponse {
text: String,
}