1use crate::markers::{Missing, Provided};
5use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
6use crate::{http_client, json_utils, provider_response};
7use std::io;
8use std::{fs, path::Path};
9use thiserror::Error;
10
11#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum TranscriptionError {
19 #[error("HttpError: {0}")]
21 HttpError(#[from] http_client::Error),
22
23 #[error("JsonError: {0}")]
25 JsonError(#[from] serde_json::Error),
26
27 #[cfg(not(target_family = "wasm"))]
28 #[error("RequestError: {0}")]
30 RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
31
32 #[cfg(target_family = "wasm")]
33 #[error("RequestError: {0}")]
35 RequestError(#[from] Box<dyn std::error::Error + 'static>),
36
37 #[error("ResponseError: {0}")]
39 ResponseError(String),
40
41 #[error("ProviderError: {0}")]
43 ProviderError(String),
44
45 #[error("ProviderResponseError: {0}")]
47 ProviderResponse(provider_response::ProviderResponseError),
48}
49
50crate::provider_response::impl_provider_response_helpers!(TranscriptionError);
51
52pub struct TranscriptionResponse<T> {
55 pub text: String,
56 pub response: T,
57}
58
59pub trait TranscriptionModel: Clone + WasmCompatSend + WasmCompatSync {
63 type Response: WasmCompatSend + WasmCompatSync;
65 type Client;
66
67 fn make(client: &Self::Client, model: impl Into<String>) -> Self;
68
69 fn transcription(
71 &self,
72 request: TranscriptionRequest,
73 ) -> impl std::future::Future<
74 Output = Result<TranscriptionResponse<Self::Response>, TranscriptionError>,
75 > + WasmCompatSend;
76
77 fn transcription_request(&self) -> TranscriptionRequestBuilder<Self, Missing> {
79 TranscriptionRequestBuilder::new(self.clone())
80 }
81}
82pub struct TranscriptionRequest {
84 pub data: Vec<u8>,
86 pub filename: String,
88 pub language: Option<String>,
90 pub prompt: Option<String>,
92 pub temperature: Option<f64>,
94 pub additional_params: Option<serde_json::Value>,
96}
97
98pub struct TranscriptionRequestBuilder<M, D>
150where
151 M: TranscriptionModel,
152{
153 model: M,
154 data: D, filename: Option<String>,
156 language: Option<String>,
157 prompt: Option<String>,
158 temperature: Option<f64>,
159 additional_params: Option<serde_json::Value>,
160}
161
162impl<M> TranscriptionRequestBuilder<M, Missing>
163where
164 M: TranscriptionModel,
165{
166 pub fn new(model: M) -> Self {
167 TranscriptionRequestBuilder {
168 model,
169 data: Missing,
170 filename: None,
171 language: None,
172 prompt: None,
173 temperature: None,
174 additional_params: None,
175 }
176 }
177}
178
179impl<M, D> TranscriptionRequestBuilder<M, D>
180where
181 M: TranscriptionModel,
182{
183 pub fn filename(mut self, filename: Option<String>) -> Self {
184 self.filename = filename;
185 self
186 }
187
188 pub fn data(self, data: Vec<u8>) -> TranscriptionRequestBuilder<M, Provided<Vec<u8>>> {
190 TranscriptionRequestBuilder {
191 model: self.model,
192 data: Provided(data),
193 filename: self.filename,
194 language: self.language,
195 prompt: self.prompt,
196 temperature: self.temperature,
197 additional_params: self.additional_params,
198 }
199 }
200
201 pub fn load_file<P>(
203 self,
204 path: P,
205 ) -> io::Result<TranscriptionRequestBuilder<M, Provided<Vec<u8>>>>
206 where
207 P: AsRef<Path>,
208 {
209 let path = path.as_ref();
210 let data = fs::read(path)?;
211
212 let filename = path.file_name().map(|n| n.to_string_lossy().into_owned());
213
214 Ok(TranscriptionRequestBuilder {
215 model: self.model,
216 data: Provided(data),
217 filename: filename.or(self.filename),
218 language: self.language,
219 prompt: self.prompt,
220 temperature: self.temperature,
221 additional_params: self.additional_params,
222 })
223 }
224
225 pub fn language(mut self, language: String) -> Self {
227 self.language = Some(language);
228 self
229 }
230
231 pub fn prompt(mut self, prompt: String) -> Self {
233 self.prompt = Some(prompt);
234 self
235 }
236
237 pub fn temperature(mut self, temperature: f64) -> Self {
239 self.temperature = Some(temperature);
240 self
241 }
242
243 pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
245 match self.additional_params {
246 Some(params) => {
247 self.additional_params = Some(json_utils::merge(params, additional_params));
248 }
249 None => {
250 self.additional_params = Some(additional_params);
251 }
252 }
253 self
254 }
255
256 pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
258 self.additional_params = additional_params;
259 self
260 }
261}
262
263impl<M> TranscriptionRequestBuilder<M, Provided<Vec<u8>>>
265where
266 M: TranscriptionModel,
267{
268 pub fn build(self) -> TranscriptionRequest {
271 TranscriptionRequest {
272 data: self.data.0,
273 filename: self.filename.unwrap_or("file".to_string()),
274 language: self.language,
275 prompt: self.prompt,
276 temperature: self.temperature,
277 additional_params: self.additional_params,
278 }
279 }
280
281 pub async fn send(self) -> Result<TranscriptionResponse<M::Response>, TranscriptionError> {
283 let model = self.model.clone();
284 model.transcription(self.build()).await
285 }
286}
287
288#[cfg(test)]
289mod provider_response_tests {
290 use super::*;
291 use http::StatusCode;
292
293 #[test]
294 fn transcription_error_provider_response_helpers_with_preserved_json_body() {
295 let body = r#"{"error":{"message":"rate limited"}}"#;
296 let error =
297 TranscriptionError::ProviderResponse(provider_response::ProviderResponseError {
298 status: None,
299 body: body.to_string(),
300 });
301
302 assert_eq!(error.provider_response_body(), Some(body));
303 assert_eq!(error.provider_response_status(), None);
304 assert_eq!(
305 error.provider_response_json().expect("valid JSON"),
306 Some(serde_json::json!({ "error": { "message": "rate limited" } }))
307 );
308 }
309
310 #[test]
311 fn transcription_error_provider_response_helpers_with_http_non_success() {
312 let body = r#"{"error":{"message":"bad request"}}"#;
313 let error =
314 TranscriptionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
315 StatusCode::BAD_REQUEST,
316 body.to_string(),
317 ));
318
319 assert_eq!(error.provider_response_body(), Some(body));
320 assert_eq!(
321 error.provider_response_status(),
322 Some(StatusCode::BAD_REQUEST)
323 );
324 assert_eq!(
325 error.provider_response_json().expect("valid JSON"),
326 Some(serde_json::json!({ "error": { "message": "bad request" } }))
327 );
328 }
329
330 #[test]
331 fn transcription_error_provider_response_helpers_with_preserved_plain_text_body() {
332 let error =
333 TranscriptionError::ProviderResponse(provider_response::ProviderResponseError {
334 status: None,
335 body: "not json".to_string(),
336 });
337
338 assert_eq!(error.provider_response_body(), Some("not json"));
339 assert!(error.provider_response_json().is_err());
340 }
341
342 #[test]
343 fn transcription_error_provider_error_is_not_a_provider_response() {
344 let error = TranscriptionError::ProviderError("internal diagnostic".to_string());
345
346 assert_eq!(error.provider_response_body(), None);
347 assert_eq!(error.provider_response_status(), None);
348 assert_eq!(error.provider_response_json().expect("no body"), None);
349 }
350
351 #[test]
352 fn transcription_error_provider_response_helpers_with_unrelated_variant() {
353 let error = TranscriptionError::ResponseError("parse failed".to_string());
354
355 assert_eq!(error.provider_response_body(), None);
356 assert_eq!(error.provider_response_status(), None);
357 assert_eq!(error.provider_response_json().expect("no body"), None);
358 }
359}