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::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 building the transcription request
14        #[error("RequestError: {0}")]
15        RequestError(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
16
17        #[cfg(target_family = "wasm")]
18        /// Error building the transcription request
19        #[error("RequestError: {0}")]
20        RequestError(#[from] Box<dyn std::error::Error + 'static>),
21    }
22);
23
24/// General transcription response struct that contains the transcription text
25/// and the raw response.
26pub struct TranscriptionResponse<T> {
27    pub text: String,
28    pub response: T,
29}
30
31/// Trait defining a transcription model that can be used to generate transcription requests.
32/// This trait is meant to be implemented by the user to define a custom transcription model,
33/// either from a third-party provider (e.g: OpenAI) or a local model.
34pub trait TranscriptionModel: Clone + WasmCompatSend + WasmCompatSync {
35    /// The raw response type returned by the underlying model.
36    type Response: WasmCompatSend + WasmCompatSync;
37    type Client;
38
39    fn make(client: &Self::Client, model: impl Into<String>) -> Self;
40
41    /// Generates a completion response for the given transcription model
42    fn transcription(
43        &self,
44        request: TranscriptionRequest,
45    ) -> impl std::future::Future<
46        Output = Result<TranscriptionResponse<Self::Response>, TranscriptionError>,
47    > + WasmCompatSend;
48
49    /// Generates a transcription request builder for the given `file`
50    fn transcription_request(&self) -> TranscriptionRequestBuilder<Self, Missing> {
51        TranscriptionRequestBuilder::new(self.clone())
52    }
53}
54/// Struct representing a general transcription request that can be sent to a transcription model provider.
55pub struct TranscriptionRequest {
56    /// The file data to be sent to the transcription model provider
57    pub data: Vec<u8>,
58    /// The file name to be used in the request
59    pub filename: String,
60    /// The language used in the response from the transcription model provider
61    pub language: Option<String>,
62    /// The prompt to be sent to the transcription model provider
63    pub prompt: Option<String>,
64    /// The temperature sent to the transcription model provider
65    pub temperature: Option<f64>,
66    /// Additional parameters to be sent to the transcription model provider
67    pub additional_params: Option<serde_json::Value>,
68}
69
70/// Builder struct for a transcription request
71///
72/// Example usage:
73/// ```no_run
74/// use rig_core::{
75///     prelude::TranscriptionClient,
76///     providers::openai::{Client, self},
77///     transcription::{TranscriptionModel, TranscriptionRequestBuilder},
78/// };
79///
80/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
81/// let openai = Client::new("your-openai-api-key")?;
82/// let model = openai.transcription_model(openai::WHISPER_1);
83///
84/// // Create the transcription request and execute it separately.
85/// let request = TranscriptionRequestBuilder::new(model.clone())
86///     .data(vec![0; 16])
87///     .filename(Some("audio.mp3".to_string()))
88///     .temperature(0.5)
89///     .build();
90///
91/// let response = model.transcription(request).await?;
92/// # Ok(())
93/// # }
94/// ```
95///
96/// Alternatively, you can execute the transcription request directly from the builder:
97/// ```no_run
98/// use rig_core::{
99///     prelude::TranscriptionClient,
100///     providers::openai::{Client, self},
101///     transcription::TranscriptionRequestBuilder,
102/// };
103///
104/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
105/// let openai = Client::new("your-openai-api-key")?;
106/// let model = openai.transcription_model(openai::WHISPER_1);
107///
108/// // Create the transcription request and execute it directly.
109/// let response = TranscriptionRequestBuilder::new(model)
110///     .data(vec![0; 16])
111///     .filename(Some("audio.mp3".to_string()))
112///     .temperature(0.5)
113///     .send()
114///     .await?;
115/// # Ok(())
116/// # }
117/// ```
118///
119/// Note: It is usually unnecessary to create a completion request builder directly.
120/// Instead, use the [TranscriptionModel::transcription_request] method.
121pub struct TranscriptionRequestBuilder<M, D>
122where
123    M: TranscriptionModel,
124{
125    model: M,
126    data: D, // starts Missing, becomes Provided<Vec<u8>> after data is set or load_file is called
127    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    /// Sets the data for the request and transitions the builder to the next state where data is provided.
161    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    /// Load the specified file into data and transitions the builder to the next state where data is provided.
174    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    /// Sets the output language for the transcription request
198    pub fn language(mut self, language: String) -> Self {
199        self.language = Some(language);
200        self
201    }
202
203    /// Sets the prompt to be sent in the transcription request
204    pub fn prompt(mut self, prompt: String) -> Self {
205        self.prompt = Some(prompt);
206        self
207    }
208
209    /// Set the temperature to be sent in the transcription request
210    pub fn temperature(mut self, temperature: f64) -> Self {
211        self.temperature = Some(temperature);
212        self
213    }
214
215    /// Adds additional parameters to the transcription request.
216    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    /// Sets the additional parameters for the transcription request.
229    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
235/// The build and send methods are only available when data is provided, ensuring that the request cannot be sent without the required data.
236impl<M> TranscriptionRequestBuilder<M, Provided<Vec<u8>>>
237where
238    M: TranscriptionModel,
239{
240    /// Builds the transcription request
241    /// Panics if data is empty.
242    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    /// Sends the transcription request to the transcription model provider and returns the transcription response
254    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}