Skip to main content

rig_core/
transcription.rs

1//! This module provides functionality for working with audio transcription models.
2//! It provides traits, structs, and enums for generating audio transcription requests,
3//! handling transcription responses, and defining transcription models.
4use 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// Errors
12/// Errors returned by transcription models.
13///
14/// Inspect provider failures with [`Self::provider_response_body`],
15/// [`Self::provider_response_json`], and [`Self::provider_response_status`].
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum TranscriptionError {
19    /// Http error (e.g.: connection error, timeout, etc.)
20    #[error("HttpError: {0}")]
21    HttpError(#[from] http_client::Error),
22
23    /// Json error (e.g.: serialization, deserialization)
24    #[error("JsonError: {0}")]
25    JsonError(#[from] serde_json::Error),
26
27    #[cfg(not(target_family = "wasm"))]
28    /// Error building the transcription request
29    #[error("RequestError: {0}")]
30    RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
31
32    #[cfg(target_family = "wasm")]
33    /// Error building the transcription request
34    #[error("RequestError: {0}")]
35    RequestError(#[from] Box<dyn std::error::Error + 'static>),
36
37    /// Error parsing the transcription response
38    #[error("ResponseError: {0}")]
39    ResponseError(String),
40
41    /// Error returned by the transcription model provider
42    #[error("ProviderError: {0}")]
43    ProviderError(String),
44
45    /// Raw error response preserved from the transcription model provider
46    #[error("ProviderResponseError: {0}")]
47    ProviderResponse(provider_response::ProviderResponseError),
48}
49
50crate::provider_response::impl_provider_response_helpers!(TranscriptionError);
51
52/// General transcription response struct that contains the transcription text
53/// and the raw response.
54pub struct TranscriptionResponse<T> {
55    pub text: String,
56    pub response: T,
57}
58
59/// Trait defining a transcription model that can be used to generate transcription requests.
60/// This trait is meant to be implemented by the user to define a custom transcription model,
61/// either from a third-party provider (e.g: OpenAI) or a local model.
62pub trait TranscriptionModel: Clone + WasmCompatSend + WasmCompatSync {
63    /// The raw response type returned by the underlying model.
64    type Response: WasmCompatSend + WasmCompatSync;
65    type Client;
66
67    fn make(client: &Self::Client, model: impl Into<String>) -> Self;
68
69    /// Generates a completion response for the given transcription model
70    fn transcription(
71        &self,
72        request: TranscriptionRequest,
73    ) -> impl std::future::Future<
74        Output = Result<TranscriptionResponse<Self::Response>, TranscriptionError>,
75    > + WasmCompatSend;
76
77    /// Generates a transcription request builder for the given `file`
78    fn transcription_request(&self) -> TranscriptionRequestBuilder<Self, Missing> {
79        TranscriptionRequestBuilder::new(self.clone())
80    }
81}
82/// Struct representing a general transcription request that can be sent to a transcription model provider.
83pub struct TranscriptionRequest {
84    /// The file data to be sent to the transcription model provider
85    pub data: Vec<u8>,
86    /// The file name to be used in the request
87    pub filename: String,
88    /// The language used in the response from the transcription model provider
89    pub language: Option<String>,
90    /// The prompt to be sent to the transcription model provider
91    pub prompt: Option<String>,
92    /// The temperature sent to the transcription model provider
93    pub temperature: Option<f64>,
94    /// Additional parameters to be sent to the transcription model provider
95    pub additional_params: Option<serde_json::Value>,
96}
97
98/// Builder struct for a transcription request
99///
100/// Example usage:
101/// ```no_run
102/// use rig_core::{
103///     prelude::TranscriptionClient,
104///     providers::openai::{Client, self},
105///     transcription::{TranscriptionModel, TranscriptionRequestBuilder},
106/// };
107///
108/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
109/// let openai = Client::new("your-openai-api-key")?;
110/// let model = openai.transcription_model(openai::WHISPER_1);
111///
112/// // Create the transcription request and execute it separately.
113/// let request = TranscriptionRequestBuilder::new(model.clone())
114///     .data(vec![0; 16])
115///     .filename(Some("audio.mp3".to_string()))
116///     .temperature(0.5)
117///     .build();
118///
119/// let response = model.transcription(request).await?;
120/// # Ok(())
121/// # }
122/// ```
123///
124/// Alternatively, you can execute the transcription request directly from the builder:
125/// ```no_run
126/// use rig_core::{
127///     prelude::TranscriptionClient,
128///     providers::openai::{Client, self},
129///     transcription::TranscriptionRequestBuilder,
130/// };
131///
132/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
133/// let openai = Client::new("your-openai-api-key")?;
134/// let model = openai.transcription_model(openai::WHISPER_1);
135///
136/// // Create the transcription request and execute it directly.
137/// let response = TranscriptionRequestBuilder::new(model)
138///     .data(vec![0; 16])
139///     .filename(Some("audio.mp3".to_string()))
140///     .temperature(0.5)
141///     .send()
142///     .await?;
143/// # Ok(())
144/// # }
145/// ```
146///
147/// Note: It is usually unnecessary to create a completion request builder directly.
148/// Instead, use the [TranscriptionModel::transcription_request] method.
149pub struct TranscriptionRequestBuilder<M, D>
150where
151    M: TranscriptionModel,
152{
153    model: M,
154    data: D, // starts Missing, becomes Provided<Vec<u8>> after data is set or load_file is called
155    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    /// Sets the data for the request and transitions the builder to the next state where data is provided.
189    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    /// Load the specified file into data and transitions the builder to the next state where data is provided.
202    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    /// Sets the output language for the transcription request
226    pub fn language(mut self, language: String) -> Self {
227        self.language = Some(language);
228        self
229    }
230
231    /// Sets the prompt to be sent in the transcription request
232    pub fn prompt(mut self, prompt: String) -> Self {
233        self.prompt = Some(prompt);
234        self
235    }
236
237    /// Set the temperature to be sent in the transcription request
238    pub fn temperature(mut self, temperature: f64) -> Self {
239        self.temperature = Some(temperature);
240        self
241    }
242
243    /// Adds additional parameters to the transcription request.
244    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    /// Sets the additional parameters for the transcription request.
257    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
263/// The build and send methods are only available when data is provided, ensuring that the request cannot be sent without the required data.
264impl<M> TranscriptionRequestBuilder<M, Provided<Vec<u8>>>
265where
266    M: TranscriptionModel,
267{
268    /// Builds the transcription request
269    /// Panics if data is empty.
270    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    /// Sends the transcription request to the transcription model provider and returns the transcription response
282    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}