1use crate::json_utils;
5use crate::markers::{Missing, Provided};
6use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
7use std::io;
8use std::{fs, path::Path};
9
10crate::provider_response::provider_error_enum!(
11 TranscriptionError, "transcription" {
12 #[cfg(not(target_family = "wasm"))]
13 #[error("RequestError: {0}")]
15 RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
16
17 #[cfg(target_family = "wasm")]
18 #[error("RequestError: {0}")]
20 RequestError(#[from] Box<dyn std::error::Error + 'static>),
21 }
22);
23
24pub struct TranscriptionResponse<T> {
27 pub text: String,
28 pub response: T,
29}
30
31pub trait TranscriptionModel: Clone + WasmCompatSend + WasmCompatSync {
35 type Response: WasmCompatSend + WasmCompatSync;
37 type Client;
38
39 fn make(client: &Self::Client, model: impl Into<String>) -> Self;
40
41 fn transcription(
43 &self,
44 request: TranscriptionRequest,
45 ) -> impl std::future::Future<
46 Output = Result<TranscriptionResponse<Self::Response>, TranscriptionError>,
47 > + WasmCompatSend;
48
49 fn transcription_request(&self) -> TranscriptionRequestBuilder<Self, Missing> {
51 TranscriptionRequestBuilder::new(self.clone())
52 }
53}
54pub struct TranscriptionRequest {
56 pub data: Vec<u8>,
58 pub filename: String,
60 pub language: Option<String>,
62 pub prompt: Option<String>,
64 pub temperature: Option<f64>,
66 pub additional_params: Option<serde_json::Value>,
68}
69
70pub struct TranscriptionRequestBuilder<M, D>
122where
123 M: TranscriptionModel,
124{
125 model: M,
126 data: D, filename: Option<String>,
128 language: Option<String>,
129 prompt: Option<String>,
130 temperature: Option<f64>,
131 additional_params: Option<serde_json::Value>,
132}
133
134impl<M> TranscriptionRequestBuilder<M, Missing>
135where
136 M: TranscriptionModel,
137{
138 pub fn new(model: M) -> Self {
139 TranscriptionRequestBuilder {
140 model,
141 data: Missing,
142 filename: None,
143 language: None,
144 prompt: None,
145 temperature: None,
146 additional_params: None,
147 }
148 }
149}
150
151impl<M, D> TranscriptionRequestBuilder<M, D>
152where
153 M: TranscriptionModel,
154{
155 pub fn filename(mut self, filename: Option<String>) -> Self {
156 self.filename = filename;
157 self
158 }
159
160 pub fn data(self, data: Vec<u8>) -> TranscriptionRequestBuilder<M, Provided<Vec<u8>>> {
162 TranscriptionRequestBuilder {
163 model: self.model,
164 data: Provided(data),
165 filename: self.filename,
166 language: self.language,
167 prompt: self.prompt,
168 temperature: self.temperature,
169 additional_params: self.additional_params,
170 }
171 }
172
173 pub fn load_file<P>(
175 self,
176 path: P,
177 ) -> io::Result<TranscriptionRequestBuilder<M, Provided<Vec<u8>>>>
178 where
179 P: AsRef<Path>,
180 {
181 let path = path.as_ref();
182 let data = fs::read(path)?;
183
184 let filename = path.file_name().map(|n| n.to_string_lossy().into_owned());
185
186 Ok(TranscriptionRequestBuilder {
187 model: self.model,
188 data: Provided(data),
189 filename: filename.or(self.filename),
190 language: self.language,
191 prompt: self.prompt,
192 temperature: self.temperature,
193 additional_params: self.additional_params,
194 })
195 }
196
197 pub fn language(mut self, language: String) -> Self {
199 self.language = Some(language);
200 self
201 }
202
203 pub fn prompt(mut self, prompt: String) -> Self {
205 self.prompt = Some(prompt);
206 self
207 }
208
209 pub fn temperature(mut self, temperature: f64) -> Self {
211 self.temperature = Some(temperature);
212 self
213 }
214
215 pub fn additional_params(mut self, additional_params: serde_json::Value) -> Self {
217 match self.additional_params {
218 Some(params) => {
219 self.additional_params = Some(json_utils::merge(params, additional_params));
220 }
221 None => {
222 self.additional_params = Some(additional_params);
223 }
224 }
225 self
226 }
227
228 pub fn additional_params_opt(mut self, additional_params: Option<serde_json::Value>) -> Self {
230 self.additional_params = additional_params;
231 self
232 }
233}
234
235impl<M> TranscriptionRequestBuilder<M, Provided<Vec<u8>>>
237where
238 M: TranscriptionModel,
239{
240 pub fn build(self) -> TranscriptionRequest {
243 TranscriptionRequest {
244 data: self.data.0,
245 filename: self.filename.unwrap_or("file".to_string()),
246 language: self.language,
247 prompt: self.prompt,
248 temperature: self.temperature,
249 additional_params: self.additional_params,
250 }
251 }
252
253 pub async fn send(self) -> Result<TranscriptionResponse<M::Response>, TranscriptionError> {
255 let model = self.model.clone();
256 model.transcription(self.build()).await
257 }
258}
259
260#[cfg(test)]
261mod provider_response_tests {
262 use super::*;
263 use crate::{http_client, provider_response};
264 use http::StatusCode;
265
266 #[test]
267 fn transcription_error_provider_response_helpers_with_preserved_json_body() {
268 let body = r#"{"error":{"message":"rate limited"}}"#;
269 let error = TranscriptionError::ProviderResponse(
270 provider_response::ProviderResponseError::without_status(body.to_string()),
271 );
272
273 assert_eq!(error.provider_response_body(), Some(body));
274 assert_eq!(error.provider_response_status(), None);
275 assert_eq!(
276 error.provider_response_json().expect("valid JSON"),
277 Some(serde_json::json!({ "error": { "message": "rate limited" } }))
278 );
279 }
280
281 #[test]
282 fn transcription_error_provider_response_helpers_with_http_non_success() {
283 let body = r#"{"error":{"message":"bad request"}}"#;
284 let error =
285 TranscriptionError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
286 StatusCode::BAD_REQUEST,
287 body.to_string(),
288 ));
289
290 assert_eq!(error.provider_response_body(), Some(body));
291 assert_eq!(
292 error.provider_response_status(),
293 Some(StatusCode::BAD_REQUEST)
294 );
295 assert_eq!(
296 error.provider_response_json().expect("valid JSON"),
297 Some(serde_json::json!({ "error": { "message": "bad request" } }))
298 );
299 }
300
301 #[test]
302 fn transcription_error_provider_response_helpers_with_preserved_plain_text_body() {
303 let error = TranscriptionError::ProviderResponse(
304 provider_response::ProviderResponseError::without_status("not json".to_string()),
305 );
306
307 assert_eq!(error.provider_response_body(), Some("not json"));
308 assert!(error.provider_response_json().is_err());
309 }
310
311 #[test]
312 fn transcription_error_provider_error_is_not_a_provider_response() {
313 let error = TranscriptionError::ProviderError("internal diagnostic".to_string());
314
315 assert_eq!(error.provider_response_body(), None);
316 assert_eq!(error.provider_response_status(), None);
317 assert_eq!(error.provider_response_json().expect("no body"), None);
318 }
319
320 #[test]
321 fn transcription_error_provider_response_helpers_with_unrelated_variant() {
322 let error = TranscriptionError::ResponseError("parse failed".to_string());
323
324 assert_eq!(error.provider_response_body(), None);
325 assert_eq!(error.provider_response_status(), None);
326 assert_eq!(error.provider_response_json().expect("no body"), None);
327 }
328}