mod arabic;
mod endpoint;
mod error;
mod output;
mod provider;
pub use arabic::normalize_ar;
pub use endpoint::{Endpoint, COHERE_URL, DEFAULT_COHERE_MODEL};
pub use error::{Error, Result};
pub use output::OutputFormat;
pub use provider::Provider;
use std::path::Path;
use std::time::Duration;
pub const DEFAULT_LANGUAGE: &str = "ar";
pub const DEFAULT_TIMEOUT_SECS: u64 = 300;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Transcript {
pub text: String,
pub text_normalized: String,
pub provider: String,
pub model: String,
pub language: String,
}
pub struct Transcriber {
endpoint: Endpoint,
language: String,
client: reqwest::blocking::Client,
}
impl Transcriber {
pub fn new(endpoint: Endpoint) -> Self {
Self::with_timeout(endpoint, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
}
pub fn with_timeout(endpoint: Endpoint, timeout: Duration) -> Self {
let client = reqwest::blocking::Client::builder()
.timeout(timeout)
.build()
.unwrap_or_else(|_| reqwest::blocking::Client::new());
Transcriber {
endpoint,
language: DEFAULT_LANGUAGE.to_string(),
client,
}
}
pub fn language(mut self, lang: impl Into<String>) -> Self {
self.language = lang.into();
self
}
pub fn transcribe(&self, audio: &Path) -> Result<Transcript> {
let bytes = std::fs::read(audio).map_err(|source| Error::ReadFile {
path: audio.to_path_buf(),
source,
})?;
let filename = audio
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("audio")
.to_string();
let file_part = reqwest::blocking::multipart::Part::bytes(bytes).file_name(filename);
let form = reqwest::blocking::multipart::Form::new()
.text("model", self.endpoint.model.clone())
.text("language", self.language.clone())
.part("file", file_part);
let mut req = self.client.post(&self.endpoint.url).multipart(form);
if let Some(key) = &self.endpoint.api_key {
req = req.bearer_auth(key);
}
let resp = req.send().map_err(|source| Error::Http {
url: self.endpoint.url.clone(),
source,
})?;
let status = resp.status();
let body = resp.text().map_err(|source| Error::Http {
url: self.endpoint.url.clone(),
source,
})?;
if !status.is_success() {
return Err(Error::Api {
status: status.as_u16(),
body,
});
}
let text = extract_text(&body).ok_or_else(|| Error::BadResponse { body: body.clone() })?;
Ok(Transcript {
text_normalized: normalize_ar(&text),
text,
provider: self.endpoint.provider.as_str().to_string(),
model: self.endpoint.model.clone(),
language: self.language.clone(),
})
}
}
fn extract_text(body: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(body).ok()?;
v.get("text")?.as_str().map(str::to_string)
}