use crate::markers::{Missing, Provided};
use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
use crate::{http_client, json_utils, provider_response};
use std::io;
use std::{fs, path::Path};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TranscriptionError {
#[error("HttpError: {0}")]
HttpError(#[from] http_client::Error),
#[error("JsonError: {0}")]
JsonError(#[from] serde_json::Error),
#[cfg(not(target_family = "wasm"))]
#[error("RequestError: {0}")]
RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[cfg(target_family = "wasm")]
#[error("RequestError: {0}")]
RequestError(#[from] Box<dyn std::error::Error + 'static>),
#[error("ResponseError: {0}")]
ResponseError(String),
#[error("ProviderError: {0}")]
ProviderError(String),
#[error("ProviderResponseError: {0}")]
ProviderResponse(provider_response::ProviderResponseError),
}
crate::provider_response::impl_provider_response_helpers!(TranscriptionError);
pub struct TranscriptionResponse<T> {
pub text: String,
pub response: T,
}
pub trait TranscriptionModel: Clone + WasmCompatSend + WasmCompatSync {
type Response: WasmCompatSend + WasmCompatSync;
type Client;
fn make(client: &Self::Client, model: impl Into<String>) -> Self;
fn transcription(
&self,
request: TranscriptionRequest,
) -> impl std::future::Future<
Output = Result<TranscriptionResponse<Self::Response>, TranscriptionError>,
> + WasmCompatSend;
fn transcription_request(&self) -> TranscriptionRequestBuilder<Self, Missing> {
TranscriptionRequestBuilder::new(self.clone())
}
}
pub struct TranscriptionRequest {
pub data: Vec<u8>,
pub filename: String,
pub language: Option<String>,
pub prompt: Option<String>,
pub temperature: Option<f64>,
pub additional_params: Option<serde_json::Value>,
}
pub struct TranscriptionRequestBuilder<M, D>
where
M: TranscriptionModel,
{
model: M,
data: D, filename: Option<String>,
language: Option<String>,
prompt: Option<String>,
temperature: Option<f64>,
additional_params: Option<serde_json::Value>,
}
impl<M> TranscriptionRequestBuilder<M, Missing>
where
M: TranscriptionModel,
{
pub fn new(model: M) -> Self {
TranscriptionRequestBuilder {
model,
data: Missing,
filename: None,
language: None,
prompt: None,
temperature: None,
additional_params: None,
}
}
}
impl<M, D> TranscriptionRequestBuilder<M, D>
where
M: TranscriptionModel,
{
pub fn filename(mut self, filename: Option<String>) -> Self {
self.filename = filename;
self
}
pub fn data(self, data: Vec<u8>) -> TranscriptionRequestBuilder<M, Provided<Vec<u8>>> {
TranscriptionRequestBuilder {
model: self.model,
data: Provided(data),
filename: self.filename,
language: self.language,
prompt: self.prompt,
temperature: self.temperature,
additional_params: self.additional_params,
}
}
pub fn load_file<P>(
self,
path: P,
) -> io::Result<TranscriptionRequestBuilder<M, Provided<Vec<u8>>>>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let data = fs::read(path)?;
let filename = path.file_name().map(|n| n.to_string_lossy().into_owned());
Ok(TranscriptionRequestBuilder {
model: self.model,
data: Provided(data),
filename: filename.or(self.filename),
language: self.language,
prompt: self.prompt,
temperature: self.temperature,
additional_params: self.additional_params,
})
}
pub fn language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
pub fn prompt(mut self, prompt: String) -> Self {
self.prompt = Some(prompt);
self
}
pub fn temperature(mut self, temperature: f64) -> Self {
self.temperature = Some(temperature);
self
}
pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
match self.additional_params {
Some(params) => {
self.additional_params = Some(json_utils::merge(params, additional_params));
}
None => {
self.additional_params = Some(additional_params);
}
}
self
}
pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
self.additional_params = additional_params;
self
}
}
impl<M> TranscriptionRequestBuilder<M, Provided<Vec<u8>>>
where
M: TranscriptionModel,
{
pub fn build(self) -> TranscriptionRequest {
TranscriptionRequest {
data: self.data.0,
filename: self.filename.unwrap_or("file".to_string()),
language: self.language,
prompt: self.prompt,
temperature: self.temperature,
additional_params: self.additional_params,
}
}
pub async fn send(self) -> Result<TranscriptionResponse<M::Response>, TranscriptionError> {
let model = self.model.clone();
model.transcription(self.build()).await
}
}
#[cfg(test)]
mod provider_response_tests {
use super::*;
use http::StatusCode;
#[test]
fn transcription_error_provider_response_helpers_with_preserved_json_body() {
let body = r#"{"error":{"message":"rate limited"}}"#;
let error =
TranscriptionError::ProviderResponse(provider_response::ProviderResponseError {
status: None,
body: body.to_string(),
});
assert_eq!(error.provider_response_body(), Some(body));
assert_eq!(error.provider_response_status(), None);
assert_eq!(
error.provider_response_json().expect("valid JSON"),
Some(serde_json::json!({ "error": { "message": "rate limited" } }))
);
}
#[test]
fn transcription_error_provider_response_helpers_with_http_non_success() {
let body = r#"{"error":{"message":"bad request"}}"#;
let error =
TranscriptionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
StatusCode::BAD_REQUEST,
body.to_string(),
));
assert_eq!(error.provider_response_body(), Some(body));
assert_eq!(
error.provider_response_status(),
Some(StatusCode::BAD_REQUEST)
);
assert_eq!(
error.provider_response_json().expect("valid JSON"),
Some(serde_json::json!({ "error": { "message": "bad request" } }))
);
}
#[test]
fn transcription_error_provider_response_helpers_with_preserved_plain_text_body() {
let error =
TranscriptionError::ProviderResponse(provider_response::ProviderResponseError {
status: None,
body: "not json".to_string(),
});
assert_eq!(error.provider_response_body(), Some("not json"));
assert!(error.provider_response_json().is_err());
}
#[test]
fn transcription_error_provider_error_is_not_a_provider_response() {
let error = TranscriptionError::ProviderError("internal diagnostic".to_string());
assert_eq!(error.provider_response_body(), None);
assert_eq!(error.provider_response_status(), None);
assert_eq!(error.provider_response_json().expect("no body"), None);
}
#[test]
fn transcription_error_provider_response_helpers_with_unrelated_variant() {
let error = TranscriptionError::ResponseError("parse failed".to_string());
assert_eq!(error.provider_response_body(), None);
assert_eq!(error.provider_response_status(), None);
assert_eq!(error.provider_response_json().expect("no body"), None);
}
}