Skip to main content

deepinfra_client_rs/
audio_transcription.rs

1use crate::client::DeepinfraClient;
2use bon::Builder;
3use reqwest::multipart;
4use serde::Deserialize;
5use std::path::Path;
6use tracing::instrument;
7
8const AUDIO_TRANSCRIPTION_API_URL: &str =
9    "https://api.deepinfra.com/v1/openai/audio/transcriptions";
10
11#[derive(Debug, Deserialize)]
12pub struct AudioTranscriptionResponse {
13    pub text: String,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct ErrorDetail {
18    loc: Vec<String>,
19    msg: String,
20    #[serde(rename = "type")]
21    error_type: String,
22}
23
24#[derive(Debug, Deserialize)]
25#[serde(untagged)]
26pub enum ErrorResponse {
27    Simple { detail: String },
28    Detailed { detail: Vec<ErrorDetail> },
29}
30
31#[derive(Debug, Deserialize)]
32#[serde(untagged)]
33enum AudioTranscriptionApiResponse {
34    TranscriptionResponse(AudioTranscriptionResponse),
35    ErrorResponse(ErrorResponse),
36}
37
38#[derive(Debug, thiserror::Error)]
39pub enum AudioTranscriptionError {
40    #[error("Request error: {0}")]
41    ReqwestError(#[from] reqwest::Error),
42    #[error("File not found: {0}")]
43    FileNotFoundError(String),
44    #[error("IO error: {0}")]
45    IoError(#[from] std::io::Error),
46    #[error("Error response: {0}")]
47    ErrorResponse(String),
48}
49
50#[derive(Debug)]
51pub enum FileSource {
52    Filepath(Box<Path>),
53    Bytes { buffer: Vec<u8>, file_name: String },
54}
55
56#[derive(Builder)]
57/// Represents a request to transcribe an audio file.
58///
59/// # Fields
60/// - `language`: Optional language of the input audio (ISO-639-1 format).
61/// - `model`: The transcription model to use (default: "openai/whisper-large-v3-turbo").
62/// - `prompt`: Optional prompt to guide the transcription output.
63/// - `response_format`: The desired format of the transcription response (e.g., "json", "text").
64/// - `source`: The audio source; can be either a file path or a byte buffer.
65/// - `temperature`: Optional sampling temperature (between 0 and 1).
66/// - `timestamp_granularities`: Optional list specifying timestamp granularities.
67pub struct AudioTranscriptionRequest {
68    /// Optional language of the input audio in ISO-639-1 format.
69    #[builder(into)]
70    language: Option<String>,
71    /// The transcription model to use (default: "openai/whisper-large-v3-turbo").
72    #[builder(default = "openai/whisper-large-v3-turbo".to_string(), into)]
73    model: String,
74    /// Optional prompt to guide the transcription style.
75    #[builder(into)]
76    prompt: Option<String>,
77    /// The desired response format (default: "json").
78    #[builder(default = "json".to_string())]
79    response_format: String,
80    /// The audio source: either a file path or a buffer with file name.
81    #[builder(into)]
82    source: FileSource,
83    /// Optional temperature controlling sampling; must be between 0 and 1.
84    #[builder(into)]
85    temperature: Option<f32>,
86    /// Optional timestamp granularities for transcription.
87    #[builder(into)]
88    timestamp_granularities: Option<Vec<String>>,
89}
90
91impl DeepinfraClient {
92    /// Transcribes an audio file using the Deepinfra API.
93    ///
94    /// This function builds and sends a multipart/form request containing the audio file and additional
95    /// parameters specified in `AudioTranscriptionRequest`. The audio source can be provided as a file path
96    /// or as raw bytes with a file name.
97    ///
98    /// # Parameters
99    ///
100    /// - `request`: An `AudioTranscriptionRequest` containing details such as the audio source,
101    ///   language, model, prompt, and other parameters.
102    ///
103    /// # Returns
104    ///
105    /// Returns an `AudioTranscriptionResponse` with the transcribed text if successful,
106    /// or an `AudioTranscriptionError` in case of a failure.
107    #[instrument(skip(self, request))]
108    pub async fn audio_transcription(
109        &self,
110        request: AudioTranscriptionRequest,
111    ) -> Result<AudioTranscriptionResponse, AudioTranscriptionError> {
112        let mut form = multipart::Form::new()
113            .text("model", request.model.to_string())
114            .text("response_format", request.response_format.to_string());
115
116        match request.source {
117            FileSource::Filepath(file_path) => {
118                let file_path = file_path.as_ref();
119
120                if !file_path.exists() {
121                    return Err(AudioTranscriptionError::FileNotFoundError(
122                        file_path.to_string_lossy().into_owned(),
123                    ));
124                }
125
126                form = form.file("file", file_path).await?;
127            }
128            FileSource::Bytes { buffer, file_name } => {
129                let part = multipart::Part::bytes(buffer).file_name(file_name);
130                form = form.part("file", part);
131            }
132        }
133
134        if let Some(language) = request.language {
135            form = form.text("language", language.to_string());
136        }
137        if let Some(prompt) = request.prompt {
138            form = form.text("prompt", prompt.to_string());
139        }
140        if let Some(temperature) = request.temperature {
141            form = form.text("temperature", temperature.to_string());
142        }
143        if let Some(timestamp_granularities) = request.timestamp_granularities {
144            for granularity in timestamp_granularities {
145                form = form.text("timestamp_granularities[]", granularity.to_string());
146            }
147        }
148
149        let response = self
150            .client
151            .post(AUDIO_TRANSCRIPTION_API_URL)
152            .multipart(form)
153            .send()
154            .await?
155            .json::<AudioTranscriptionApiResponse>()
156            .await?;
157
158        match response {
159            AudioTranscriptionApiResponse::TranscriptionResponse(response) => Ok(response),
160            AudioTranscriptionApiResponse::ErrorResponse(error) => match error {
161                ErrorResponse::Simple { detail } => {
162                    Err(AudioTranscriptionError::ErrorResponse(detail))
163                }
164                ErrorResponse::Detailed { detail } => {
165                    let error_details: Vec<String> = detail
166                        .iter()
167                        .map(|d| format!("{}: {}", d.loc.join("."), d.msg))
168                        .collect();
169                    Err(AudioTranscriptionError::ErrorResponse(
170                        error_details.join(", "),
171                    ))
172                }
173            },
174        }
175    }
176}