Skip to main content

openai_tools/audio/
request.rs

1//! OpenAI Audio API Request Module
2//!
3//! This module provides the functionality to interact with the OpenAI Audio API.
4//! It supports text-to-speech (TTS), transcription, and translation.
5//!
6//! # Key Features
7//!
8//! - **Text-to-Speech**: Convert text to natural-sounding audio
9//! - **Transcription**: Convert audio to text (speech-to-text)
10//! - **Translation**: Translate audio to English text
11//!
12//! # Quick Start
13//!
14//! ```rust,no_run
15//! use openai_tools::audio::request::{Audio, TtsOptions, Voice};
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!     let audio = Audio::new()?;
20//!
21//!     // Generate speech from text
22//!     let options = TtsOptions::default();
23//!     let audio_bytes = audio.text_to_speech("Hello, world!", options).await?;
24//!     std::fs::write("output.mp3", audio_bytes)?;
25//!
26//!     Ok(())
27//! }
28//! ```
29
30use crate::audio::response::TranscriptionResponse;
31use crate::common::auth::AuthProvider;
32use crate::common::client::create_http_client;
33use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
34use request::multipart::{Form, Part};
35use serde::{Deserialize, Serialize};
36use std::path::Path;
37use std::time::Duration;
38
39/// Default API path for Audio
40const AUDIO_PATH: &str = "audio";
41
42/// Text-to-speech models.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
44#[non_exhaustive]
45pub enum TtsModel {
46    /// Standard quality TTS model
47    #[serde(rename = "tts-1")]
48    #[default]
49    Tts1,
50    /// High definition TTS model
51    #[serde(rename = "tts-1-hd")]
52    Tts1Hd,
53    /// GPT-4o Mini TTS model
54    #[serde(rename = "gpt-4o-mini-tts")]
55    Gpt4oMiniTts,
56    /// tts-1-1106 - dated snapshot of the standard TTS model
57    #[serde(rename = "tts-1-1106")]
58    Tts1_1106,
59    /// tts-1-hd-1106 - dated snapshot of the HD TTS model
60    #[serde(rename = "tts-1-hd-1106")]
61    Tts1Hd1106,
62}
63
64impl TtsModel {
65    /// Returns the model identifier string.
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            Self::Tts1 => "tts-1",
69            Self::Tts1Hd => "tts-1-hd",
70            Self::Gpt4oMiniTts => "gpt-4o-mini-tts",
71            Self::Tts1_1106 => "tts-1-1106",
72            Self::Tts1Hd1106 => "tts-1-hd-1106",
73        }
74    }
75
76    /// Checks if this model supports the `instructions` parameter.
77    ///
78    /// Only `gpt-4o-mini-tts` supports the instructions parameter for
79    /// controlling voice characteristics like tone, emotion, and pacing.
80    ///
81    /// # Example
82    ///
83    /// ```rust
84    /// use openai_tools::audio::request::TtsModel;
85    ///
86    /// assert!(TtsModel::Gpt4oMiniTts.supports_instructions());
87    /// assert!(!TtsModel::Tts1.supports_instructions());
88    /// assert!(!TtsModel::Tts1Hd.supports_instructions());
89    /// ```
90    pub fn supports_instructions(&self) -> bool {
91        matches!(self, Self::Gpt4oMiniTts)
92    }
93}
94
95impl std::fmt::Display for TtsModel {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", self.as_str())
98    }
99}
100
101/// Voice options for text-to-speech.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
103#[serde(rename_all = "lowercase")]
104#[non_exhaustive]
105pub enum Voice {
106    /// Alloy voice
107    #[default]
108    Alloy,
109    /// Ash voice
110    Ash,
111    /// Ballad voice
112    Ballad,
113    /// Cedar voice (recommended for quality)
114    Cedar,
115    /// Coral voice
116    Coral,
117    /// Echo voice
118    Echo,
119    /// Fable voice
120    Fable,
121    /// Marin voice (recommended for quality)
122    Marin,
123    /// Nova voice
124    Nova,
125    /// Onyx voice
126    Onyx,
127    /// Sage voice
128    Sage,
129    /// Shimmer voice
130    Shimmer,
131    /// Verse voice
132    Verse,
133}
134
135impl Voice {
136    /// Returns the voice identifier string.
137    pub fn as_str(&self) -> &'static str {
138        match self {
139            Self::Alloy => "alloy",
140            Self::Ash => "ash",
141            Self::Ballad => "ballad",
142            Self::Cedar => "cedar",
143            Self::Coral => "coral",
144            Self::Echo => "echo",
145            Self::Fable => "fable",
146            Self::Marin => "marin",
147            Self::Nova => "nova",
148            Self::Onyx => "onyx",
149            Self::Sage => "sage",
150            Self::Shimmer => "shimmer",
151            Self::Verse => "verse",
152        }
153    }
154}
155
156impl std::fmt::Display for Voice {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(f, "{}", self.as_str())
159    }
160}
161
162/// Audio output formats for TTS.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
164#[serde(rename_all = "lowercase")]
165#[non_exhaustive]
166pub enum AudioFormat {
167    /// MP3 format (default)
168    #[default]
169    Mp3,
170    /// Opus format
171    Opus,
172    /// AAC format
173    Aac,
174    /// FLAC format
175    Flac,
176    /// WAV format
177    Wav,
178    /// PCM format
179    Pcm,
180}
181
182impl AudioFormat {
183    /// Returns the format string.
184    pub fn as_str(&self) -> &'static str {
185        match self {
186            Self::Mp3 => "mp3",
187            Self::Opus => "opus",
188            Self::Aac => "aac",
189            Self::Flac => "flac",
190            Self::Wav => "wav",
191            Self::Pcm => "pcm",
192        }
193    }
194
195    /// Returns the file extension for this format.
196    pub fn file_extension(&self) -> &'static str {
197        self.as_str()
198    }
199}
200
201impl std::fmt::Display for AudioFormat {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{}", self.as_str())
204    }
205}
206
207/// Speech-to-text models for transcription and translation.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
209#[non_exhaustive]
210pub enum SttModel {
211    /// Whisper v1 model
212    #[serde(rename = "whisper-1")]
213    #[default]
214    Whisper1,
215    /// GPT-4o Transcribe model
216    #[serde(rename = "gpt-4o-transcribe")]
217    Gpt4oTranscribe,
218    /// GPT-4o Mini Transcribe - cheaper GPT-4o Transcribe
219    #[serde(rename = "gpt-4o-mini-transcribe")]
220    Gpt4oMiniTranscribe,
221    /// GPT-4o Transcribe Diarize - transcription with speaker diarization
222    #[serde(rename = "gpt-4o-transcribe-diarize")]
223    Gpt4oTranscribeDiarize,
224    /// GPT Transcribe - high-accuracy speech-to-text for file and realtime input
225    #[serde(rename = "gpt-transcribe")]
226    GptTranscribe,
227    /// GPT Live Transcribe - low-latency streaming transcript deltas
228    ///
229    /// Realtime transcription sessions only; not available on
230    /// `/v1/audio/transcriptions`.
231    #[serde(rename = "gpt-live-transcribe")]
232    GptLiveTranscribe,
233    /// GPT Realtime Whisper - Whisper for realtime transcription sessions
234    ///
235    /// Realtime transcription sessions only; not available on
236    /// `/v1/audio/transcriptions`.
237    #[serde(rename = "gpt-realtime-whisper")]
238    GptRealtimeWhisper,
239}
240
241impl SttModel {
242    /// Returns the model identifier string.
243    pub fn as_str(&self) -> &'static str {
244        match self {
245            Self::Whisper1 => "whisper-1",
246            Self::Gpt4oTranscribe => "gpt-4o-transcribe",
247            Self::Gpt4oMiniTranscribe => "gpt-4o-mini-transcribe",
248            Self::Gpt4oTranscribeDiarize => "gpt-4o-transcribe-diarize",
249            Self::GptTranscribe => "gpt-transcribe",
250            Self::GptLiveTranscribe => "gpt-live-transcribe",
251            Self::GptRealtimeWhisper => "gpt-realtime-whisper",
252        }
253    }
254
255    /// Returns `true` if the model can transcribe uploaded files via
256    /// `/v1/audio/transcriptions`.
257    ///
258    /// [`GptLiveTranscribe`](Self::GptLiveTranscribe) and
259    /// [`GptRealtimeWhisper`](Self::GptRealtimeWhisper) are exposed only on
260    /// `/v1/realtime/transcription_sessions`, so passing them to
261    /// [`Audio::transcribe`] would be rejected by the API.
262    pub fn supports_file_transcription(&self) -> bool {
263        !matches!(self, Self::GptLiveTranscribe | Self::GptRealtimeWhisper)
264    }
265}
266
267impl std::fmt::Display for SttModel {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        write!(f, "{}", self.as_str())
270    }
271}
272
273/// Transcription response formats.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
275#[serde(rename_all = "snake_case")]
276#[non_exhaustive]
277pub enum TranscriptionFormat {
278    /// JSON format
279    #[default]
280    Json,
281    /// Plain text format
282    Text,
283    /// SRT subtitle format
284    Srt,
285    /// Verbose JSON with timestamps
286    VerboseJson,
287    /// VTT subtitle format
288    Vtt,
289}
290
291impl TranscriptionFormat {
292    /// Returns the format string.
293    pub fn as_str(&self) -> &'static str {
294        match self {
295            Self::Json => "json",
296            Self::Text => "text",
297            Self::Srt => "srt",
298            Self::VerboseJson => "verbose_json",
299            Self::Vtt => "vtt",
300        }
301    }
302}
303
304/// Timestamp granularity options.
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "lowercase")]
307#[non_exhaustive]
308pub enum TimestampGranularity {
309    /// Word-level timestamps
310    Word,
311    /// Segment-level timestamps
312    Segment,
313}
314
315impl TimestampGranularity {
316    /// Returns the granularity string.
317    pub fn as_str(&self) -> &'static str {
318        match self {
319            Self::Word => "word",
320            Self::Segment => "segment",
321        }
322    }
323}
324
325/// Options for text-to-speech generation.
326#[derive(Debug, Clone, Default)]
327pub struct TtsOptions {
328    /// The model to use (defaults to tts-1)
329    pub model: TtsModel,
330    /// The voice to use (defaults to alloy)
331    pub voice: Voice,
332    /// The output audio format (defaults to mp3)
333    pub response_format: AudioFormat,
334    /// Speech speed (0.25 to 4.0, defaults to 1.0)
335    pub speed: Option<f32>,
336    /// Instructions for controlling voice characteristics.
337    ///
338    /// Only supported by `gpt-4o-mini-tts` model.
339    /// Use natural language to control tone, emotion, and pacing.
340    ///
341    /// # Examples
342    ///
343    /// - `"Speak in a cheerful and positive tone."`
344    /// - `"Use a calm and soothing voice."`
345    /// - `"Speak with enthusiasm and energy."`
346    ///
347    /// If set with an unsupported model (`tts-1` or `tts-1-hd`),
348    /// this parameter will be ignored and a warning will be logged.
349    pub instructions: Option<String>,
350}
351
352/// Options for audio transcription.
353#[derive(Debug, Clone, Default)]
354pub struct TranscribeOptions {
355    /// The model to use (defaults to whisper-1)
356    pub model: Option<SttModel>,
357    /// The language of the input audio (ISO-639-1 code)
358    pub language: Option<String>,
359    /// Optional prompt to guide the model's style
360    pub prompt: Option<String>,
361    /// Response format (defaults to json)
362    pub response_format: Option<TranscriptionFormat>,
363    /// Temperature for sampling (0.0 to 1.0)
364    pub temperature: Option<f32>,
365    /// Timestamp granularities to include
366    pub timestamp_granularities: Option<Vec<TimestampGranularity>>,
367}
368
369/// Options for audio translation.
370#[derive(Debug, Clone, Default)]
371pub struct TranslateOptions {
372    /// The model to use (only whisper-1 is supported)
373    pub model: Option<SttModel>,
374    /// Optional prompt to guide the model's style
375    pub prompt: Option<String>,
376    /// Response format (defaults to json)
377    pub response_format: Option<TranscriptionFormat>,
378    /// Temperature for sampling (0.0 to 1.0)
379    pub temperature: Option<f32>,
380}
381
382/// Request payload for TTS.
383#[derive(Debug, Clone, Serialize)]
384struct TtsRequest {
385    model: String,
386    input: String,
387    voice: String,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    response_format: Option<String>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    speed: Option<f32>,
392    /// Instructions for voice control (only for gpt-4o-mini-tts).
393    #[serde(skip_serializing_if = "Option::is_none")]
394    instructions: Option<String>,
395}
396
397/// Client for interacting with the OpenAI Audio API.
398///
399/// This struct provides methods for text-to-speech, transcription, and translation.
400/// Use [`Audio::new()`] to create a new instance.
401///
402/// # Example
403///
404/// ```rust,no_run
405/// use openai_tools::audio::request::{Audio, TtsOptions, Voice, AudioFormat};
406///
407/// #[tokio::main]
408/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
409///     let audio = Audio::new()?;
410///
411///     let options = TtsOptions {
412///         voice: Voice::Nova,
413///         response_format: AudioFormat::Mp3,
414///         ..Default::default()
415///     };
416///
417///     let bytes = audio.text_to_speech("Welcome to our app!", options).await?;
418///     std::fs::write("welcome.mp3", bytes)?;
419///
420///     Ok(())
421/// }
422/// ```
423pub struct Audio {
424    /// Authentication provider (OpenAI or Azure)
425    auth: AuthProvider,
426    /// Optional request timeout duration
427    timeout: Option<Duration>,
428}
429
430impl Audio {
431    /// Creates a new Audio client for OpenAI API.
432    ///
433    /// Initializes the client by loading the OpenAI API key from
434    /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
435    /// via dotenvy.
436    ///
437    /// # Returns
438    ///
439    /// * `Ok(Audio)` - A new Audio client ready for use
440    /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
441    ///
442    /// # Example
443    ///
444    /// ```rust,no_run
445    /// use openai_tools::audio::request::Audio;
446    ///
447    /// let audio = Audio::new().expect("API key should be set");
448    /// ```
449    pub fn new() -> Result<Self> {
450        let auth = AuthProvider::openai_from_env()?;
451        Ok(Self { auth, timeout: None })
452    }
453
454    /// Creates a new Audio client with a custom authentication provider
455    pub fn with_auth(auth: AuthProvider) -> Self {
456        Self { auth, timeout: None }
457    }
458
459    /// Creates a new Audio client for Azure OpenAI API
460    pub fn azure() -> Result<Self> {
461        let auth = AuthProvider::azure_from_env()?;
462        Ok(Self { auth, timeout: None })
463    }
464
465    /// Creates a new Audio client by auto-detecting the provider
466    pub fn detect_provider() -> Result<Self> {
467        let auth = AuthProvider::from_env()?;
468        Ok(Self { auth, timeout: None })
469    }
470
471    /// Creates a new Audio client with URL-based provider detection
472    pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
473        let auth = AuthProvider::from_url_with_key(base_url, api_key);
474        Self { auth, timeout: None }
475    }
476
477    /// Creates a new Audio client from URL using environment variables
478    pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
479        let auth = AuthProvider::from_url(url)?;
480        Ok(Self { auth, timeout: None })
481    }
482
483    /// Returns the authentication provider
484    pub fn auth(&self) -> &AuthProvider {
485        &self.auth
486    }
487
488    /// Sets the request timeout duration.
489    ///
490    /// # Arguments
491    ///
492    /// * `timeout` - The maximum time to wait for a response
493    ///
494    /// # Returns
495    ///
496    /// A mutable reference to self for method chaining
497    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
498        self.timeout = Some(timeout);
499        self
500    }
501
502    /// Creates the HTTP client with default headers.
503    fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
504        let client = create_http_client(self.timeout)?;
505        let mut headers = request::header::HeaderMap::new();
506        self.auth.apply_headers(&mut headers)?;
507        headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
508        Ok((client, headers))
509    }
510
511    /// Converts text to speech.
512    ///
513    /// Returns audio bytes in the specified format.
514    ///
515    /// # Arguments
516    ///
517    /// * `text` - The text to convert to speech (max 4096 characters)
518    /// * `options` - TTS options (model, voice, format, speed)
519    ///
520    /// # Returns
521    ///
522    /// * `Ok(Vec<u8>)` - The audio data as bytes
523    /// * `Err(OpenAIToolError)` - If the request fails
524    ///
525    /// # Example
526    ///
527    /// ```rust,no_run
528    /// use openai_tools::audio::request::{Audio, TtsOptions, TtsModel, Voice};
529    ///
530    /// #[tokio::main]
531    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
532    ///     let audio = Audio::new()?;
533    ///
534    ///     let options = TtsOptions {
535    ///         model: TtsModel::Tts1Hd,
536    ///         voice: Voice::Shimmer,
537    ///         speed: Some(1.2),
538    ///         ..Default::default()
539    ///     };
540    ///
541    ///     let bytes = audio.text_to_speech("Hello, this is a test.", options).await?;
542    ///     std::fs::write("speech.mp3", bytes)?;
543    ///
544    ///     Ok(())
545    /// }
546    /// ```
547    pub async fn text_to_speech(&self, text: &str, options: TtsOptions) -> Result<Vec<u8>> {
548        let (client, mut headers) = self.create_client()?;
549        headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
550
551        // Check if instructions parameter is supported by the model
552        let instructions = if options.instructions.is_some() {
553            if options.model.supports_instructions() {
554                options.instructions
555            } else {
556                tracing::warn!("Model '{}' does not support instructions parameter. Ignoring instructions.", options.model);
557                None
558            }
559        } else {
560            None
561        };
562
563        let request_body = TtsRequest {
564            model: options.model.as_str().to_string(),
565            input: text.to_string(),
566            voice: options.voice.as_str().to_string(),
567            response_format: Some(options.response_format.as_str().to_string()),
568            speed: options.speed,
569            instructions,
570        };
571
572        let body = serde_json::to_string(&request_body).map_err(OpenAIToolError::SerdeJsonError)?;
573
574        let url = format!("{}/speech", self.auth.endpoint(AUDIO_PATH));
575
576        let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
577
578        let bytes = response.bytes().await.map_err(OpenAIToolError::RequestError)?;
579
580        Ok(bytes.to_vec())
581    }
582
583    /// Transcribes audio from a file path.
584    ///
585    /// # Arguments
586    ///
587    /// * `audio_path` - Path to the audio file
588    /// * `options` - Transcription options
589    ///
590    /// # Returns
591    ///
592    /// * `Ok(TranscriptionResponse)` - The transcription result
593    /// * `Err(OpenAIToolError)` - If the request fails
594    ///
595    /// # Example
596    ///
597    /// ```rust,no_run
598    /// use openai_tools::audio::request::{Audio, TranscribeOptions};
599    ///
600    /// #[tokio::main]
601    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
602    ///     let audio = Audio::new()?;
603    ///
604    ///     let options = TranscribeOptions {
605    ///         language: Some("en".to_string()),
606    ///         ..Default::default()
607    ///     };
608    ///
609    ///     let response = audio.transcribe("audio.mp3", options).await?;
610    ///     println!("Transcription: {}", response.text);
611    ///
612    ///     Ok(())
613    /// }
614    /// ```
615    pub async fn transcribe(&self, audio_path: &str, options: TranscribeOptions) -> Result<TranscriptionResponse> {
616        let audio_content = tokio::fs::read(audio_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read audio file: {}", e)))?;
617
618        let filename = Path::new(audio_path).file_name().and_then(|n| n.to_str()).unwrap_or("audio.mp3").to_string();
619
620        self.transcribe_bytes(&audio_content, &filename, options).await
621    }
622
623    /// Transcribes audio from bytes.
624    ///
625    /// # Arguments
626    ///
627    /// * `audio_data` - The audio data as bytes
628    /// * `filename` - The filename with extension (e.g., "audio.mp3")
629    /// * `options` - Transcription options
630    ///
631    /// # Returns
632    ///
633    /// * `Ok(TranscriptionResponse)` - The transcription result
634    /// * `Err(OpenAIToolError)` - If the request fails
635    ///
636    /// # Example
637    ///
638    /// ```rust,no_run
639    /// use openai_tools::audio::request::{Audio, TranscribeOptions, SttModel};
640    ///
641    /// #[tokio::main]
642    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
643    ///     let audio = Audio::new()?;
644    ///
645    ///     let audio_data = std::fs::read("recording.mp3")?;
646    ///     let options = TranscribeOptions {
647    ///         model: Some(SttModel::Whisper1),
648    ///         ..Default::default()
649    ///     };
650    ///
651    ///     let response = audio.transcribe_bytes(&audio_data, "recording.mp3", options).await?;
652    ///     println!("Transcription: {}", response.text);
653    ///
654    ///     Ok(())
655    /// }
656    /// ```
657    pub async fn transcribe_bytes(&self, audio_data: &[u8], filename: &str, options: TranscribeOptions) -> Result<TranscriptionResponse> {
658        let (client, headers) = self.create_client()?;
659
660        let audio_part = Part::bytes(audio_data.to_vec())
661            .file_name(filename.to_string())
662            .mime_str("audio/mpeg")
663            .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
664
665        let mut form = Form::new().part("file", audio_part);
666
667        // Add model
668        let model = options.model.unwrap_or_default();
669        form = form.text("model", model.as_str().to_string());
670
671        // Add optional parameters
672        if let Some(language) = options.language {
673            form = form.text("language", language);
674        }
675        if let Some(prompt) = options.prompt {
676            form = form.text("prompt", prompt);
677        }
678        if let Some(response_format) = options.response_format {
679            form = form.text("response_format", response_format.as_str().to_string());
680        }
681        if let Some(temperature) = options.temperature {
682            form = form.text("temperature", temperature.to_string());
683        }
684        if let Some(granularities) = options.timestamp_granularities {
685            for g in granularities {
686                form = form.text("timestamp_granularities[]", g.as_str().to_string());
687            }
688        }
689
690        let url = format!("{}/transcriptions", self.auth.endpoint(AUDIO_PATH));
691
692        let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
693
694        let status = response.status();
695        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
696
697        if cfg!(test) {
698            tracing::info!("Response content: {}", content);
699        }
700
701        if !status.is_success() {
702            if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
703                return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
704            }
705            return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
706        }
707
708        serde_json::from_str::<TranscriptionResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
709    }
710
711    /// Translates audio to English text.
712    ///
713    /// Only supports translation to English using the whisper-1 model.
714    ///
715    /// # Arguments
716    ///
717    /// * `audio_path` - Path to the audio file
718    /// * `options` - Translation options
719    ///
720    /// # Returns
721    ///
722    /// * `Ok(TranscriptionResponse)` - The translation result
723    /// * `Err(OpenAIToolError)` - If the request fails
724    ///
725    /// # Example
726    ///
727    /// ```rust,no_run
728    /// use openai_tools::audio::request::{Audio, TranslateOptions};
729    ///
730    /// #[tokio::main]
731    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
732    ///     let audio = Audio::new()?;
733    ///
734    ///     let options = TranslateOptions::default();
735    ///     let response = audio.translate("french_audio.mp3", options).await?;
736    ///     println!("English translation: {}", response.text);
737    ///
738    ///     Ok(())
739    /// }
740    /// ```
741    pub async fn translate(&self, audio_path: &str, options: TranslateOptions) -> Result<TranscriptionResponse> {
742        let audio_content = tokio::fs::read(audio_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read audio file: {}", e)))?;
743
744        let filename = Path::new(audio_path).file_name().and_then(|n| n.to_str()).unwrap_or("audio.mp3").to_string();
745
746        self.translate_bytes(&audio_content, &filename, options).await
747    }
748
749    /// Translates audio from bytes to English text.
750    ///
751    /// # Arguments
752    ///
753    /// * `audio_data` - The audio data as bytes
754    /// * `filename` - The filename with extension (e.g., "audio.mp3")
755    /// * `options` - Translation options
756    ///
757    /// # Returns
758    ///
759    /// * `Ok(TranscriptionResponse)` - The translation result
760    /// * `Err(OpenAIToolError)` - If the request fails
761    pub async fn translate_bytes(&self, audio_data: &[u8], filename: &str, options: TranslateOptions) -> Result<TranscriptionResponse> {
762        let (client, headers) = self.create_client()?;
763
764        let audio_part = Part::bytes(audio_data.to_vec())
765            .file_name(filename.to_string())
766            .mime_str("audio/mpeg")
767            .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
768
769        let mut form = Form::new().part("file", audio_part);
770
771        // Add model (whisper-1 is the only supported model for translation)
772        let model = options.model.unwrap_or(SttModel::Whisper1);
773        form = form.text("model", model.as_str().to_string());
774
775        // Add optional parameters
776        if let Some(prompt) = options.prompt {
777            form = form.text("prompt", prompt);
778        }
779        if let Some(response_format) = options.response_format {
780            form = form.text("response_format", response_format.as_str().to_string());
781        }
782        if let Some(temperature) = options.temperature {
783            form = form.text("temperature", temperature.to_string());
784        }
785
786        let url = format!("{}/translations", self.auth.endpoint(AUDIO_PATH));
787
788        let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
789
790        let status = response.status();
791        let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
792
793        if cfg!(test) {
794            tracing::info!("Response content: {}", content);
795        }
796
797        if !status.is_success() {
798            if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
799                return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
800            }
801            return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
802        }
803
804        serde_json::from_str::<TranscriptionResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811
812    // =========================================================================
813    // TtsModel Tests
814    // =========================================================================
815
816    #[test]
817    fn test_tts_model_as_str() {
818        assert_eq!(TtsModel::Tts1.as_str(), "tts-1");
819        assert_eq!(TtsModel::Tts1Hd.as_str(), "tts-1-hd");
820        assert_eq!(TtsModel::Gpt4oMiniTts.as_str(), "gpt-4o-mini-tts");
821    }
822
823    #[test]
824    fn test_tts_model_supports_instructions() {
825        // Only gpt-4o-mini-tts supports instructions
826        assert!(TtsModel::Gpt4oMiniTts.supports_instructions());
827        assert!(!TtsModel::Tts1.supports_instructions());
828        assert!(!TtsModel::Tts1Hd.supports_instructions());
829    }
830
831    #[test]
832    fn test_tts_model_default() {
833        let model = TtsModel::default();
834        assert_eq!(model, TtsModel::Tts1);
835    }
836
837    #[test]
838    fn test_tts_model_display() {
839        assert_eq!(format!("{}", TtsModel::Gpt4oMiniTts), "gpt-4o-mini-tts");
840    }
841
842    // =========================================================================
843    // Voice Tests
844    // =========================================================================
845
846    #[test]
847    fn test_voice_as_str_all_voices() {
848        assert_eq!(Voice::Alloy.as_str(), "alloy");
849        assert_eq!(Voice::Ash.as_str(), "ash");
850        assert_eq!(Voice::Ballad.as_str(), "ballad");
851        assert_eq!(Voice::Cedar.as_str(), "cedar");
852        assert_eq!(Voice::Coral.as_str(), "coral");
853        assert_eq!(Voice::Echo.as_str(), "echo");
854        assert_eq!(Voice::Fable.as_str(), "fable");
855        assert_eq!(Voice::Marin.as_str(), "marin");
856        assert_eq!(Voice::Nova.as_str(), "nova");
857        assert_eq!(Voice::Onyx.as_str(), "onyx");
858        assert_eq!(Voice::Sage.as_str(), "sage");
859        assert_eq!(Voice::Shimmer.as_str(), "shimmer");
860        assert_eq!(Voice::Verse.as_str(), "verse");
861    }
862
863    #[test]
864    fn test_voice_new_voices() {
865        // Test the newly added voices
866        assert_eq!(Voice::Ballad.as_str(), "ballad");
867        assert_eq!(Voice::Cedar.as_str(), "cedar");
868        assert_eq!(Voice::Marin.as_str(), "marin");
869        assert_eq!(Voice::Verse.as_str(), "verse");
870    }
871
872    #[test]
873    fn test_voice_default() {
874        let voice = Voice::default();
875        assert_eq!(voice, Voice::Alloy);
876    }
877
878    #[test]
879    fn test_voice_serialization() {
880        let voice = Voice::Coral;
881        let json = serde_json::to_string(&voice).unwrap();
882        assert_eq!(json, "\"coral\"");
883
884        // Test new voices
885        let ballad = Voice::Ballad;
886        let json = serde_json::to_string(&ballad).unwrap();
887        assert_eq!(json, "\"ballad\"");
888    }
889
890    #[test]
891    fn test_voice_deserialization() {
892        let voice: Voice = serde_json::from_str("\"coral\"").unwrap();
893        assert_eq!(voice, Voice::Coral);
894
895        // Test new voices
896        let cedar: Voice = serde_json::from_str("\"cedar\"").unwrap();
897        assert_eq!(cedar, Voice::Cedar);
898
899        let marin: Voice = serde_json::from_str("\"marin\"").unwrap();
900        assert_eq!(marin, Voice::Marin);
901    }
902
903    // =========================================================================
904    // TtsOptions Tests
905    // =========================================================================
906
907    #[test]
908    fn test_tts_options_default() {
909        let options = TtsOptions::default();
910        assert_eq!(options.model, TtsModel::Tts1);
911        assert_eq!(options.voice, Voice::Alloy);
912        assert_eq!(options.response_format, AudioFormat::Mp3);
913        assert!(options.speed.is_none());
914        assert!(options.instructions.is_none());
915    }
916
917    #[test]
918    fn test_tts_options_with_instructions() {
919        let options = TtsOptions {
920            model: TtsModel::Gpt4oMiniTts,
921            voice: Voice::Coral,
922            instructions: Some("Speak in a cheerful tone.".to_string()),
923            ..Default::default()
924        };
925        assert_eq!(options.model, TtsModel::Gpt4oMiniTts);
926        assert_eq!(options.instructions, Some("Speak in a cheerful tone.".to_string()));
927    }
928
929    // =========================================================================
930    // TtsRequest Tests
931    // =========================================================================
932
933    #[test]
934    fn test_tts_request_serialization_with_instructions() {
935        let request = TtsRequest {
936            model: "gpt-4o-mini-tts".to_string(),
937            input: "Hello, world!".to_string(),
938            voice: "coral".to_string(),
939            response_format: Some("mp3".to_string()),
940            speed: None,
941            instructions: Some("Speak cheerfully.".to_string()),
942        };
943        let json = serde_json::to_value(&request).unwrap();
944
945        assert_eq!(json["model"], "gpt-4o-mini-tts");
946        assert_eq!(json["input"], "Hello, world!");
947        assert_eq!(json["voice"], "coral");
948        assert_eq!(json["response_format"], "mp3");
949        assert_eq!(json["instructions"], "Speak cheerfully.");
950        assert!(json.get("speed").is_none());
951    }
952
953    #[test]
954    fn test_tts_request_serialization_without_instructions() {
955        let request = TtsRequest {
956            model: "tts-1".to_string(),
957            input: "Hello".to_string(),
958            voice: "alloy".to_string(),
959            response_format: Some("mp3".to_string()),
960            speed: Some(1.0),
961            instructions: None,
962        };
963        let json = serde_json::to_value(&request).unwrap();
964
965        assert_eq!(json["model"], "tts-1");
966        assert_eq!(json["speed"], 1.0);
967        // instructions should be omitted when None
968        assert!(json.get("instructions").is_none());
969    }
970
971    #[test]
972    fn test_tts_request_skip_serializing_none_fields() {
973        let request = TtsRequest {
974            model: "tts-1".to_string(),
975            input: "Test".to_string(),
976            voice: "echo".to_string(),
977            response_format: None,
978            speed: None,
979            instructions: None,
980        };
981        let json = serde_json::to_value(&request).unwrap();
982
983        // Required fields are present
984        assert!(json.get("model").is_some());
985        assert!(json.get("input").is_some());
986        assert!(json.get("voice").is_some());
987
988        // Optional fields with None are omitted
989        assert!(json.get("response_format").is_none());
990        assert!(json.get("speed").is_none());
991        assert!(json.get("instructions").is_none());
992    }
993
994    // =========================================================================
995    // AudioFormat Tests
996    // =========================================================================
997
998    #[test]
999    fn test_audio_format_as_str() {
1000        assert_eq!(AudioFormat::Mp3.as_str(), "mp3");
1001        assert_eq!(AudioFormat::Opus.as_str(), "opus");
1002        assert_eq!(AudioFormat::Aac.as_str(), "aac");
1003        assert_eq!(AudioFormat::Flac.as_str(), "flac");
1004        assert_eq!(AudioFormat::Wav.as_str(), "wav");
1005        assert_eq!(AudioFormat::Pcm.as_str(), "pcm");
1006    }
1007
1008    #[test]
1009    fn test_audio_format_file_extension() {
1010        assert_eq!(AudioFormat::Mp3.file_extension(), "mp3");
1011        assert_eq!(AudioFormat::Wav.file_extension(), "wav");
1012    }
1013
1014    // =========================================================================
1015    // SttModel Tests
1016    // =========================================================================
1017
1018    #[test]
1019    fn test_stt_model_as_str() {
1020        assert_eq!(SttModel::Whisper1.as_str(), "whisper-1");
1021        assert_eq!(SttModel::Gpt4oTranscribe.as_str(), "gpt-4o-transcribe");
1022    }
1023
1024    // =========================================================================
1025    // TranscriptionFormat Tests
1026    // =========================================================================
1027
1028    #[test]
1029    fn test_transcription_format_as_str() {
1030        assert_eq!(TranscriptionFormat::Json.as_str(), "json");
1031        assert_eq!(TranscriptionFormat::Text.as_str(), "text");
1032        assert_eq!(TranscriptionFormat::Srt.as_str(), "srt");
1033        assert_eq!(TranscriptionFormat::VerboseJson.as_str(), "verbose_json");
1034        assert_eq!(TranscriptionFormat::Vtt.as_str(), "vtt");
1035    }
1036
1037    // =========================================================================
1038    // TimestampGranularity Tests
1039    // =========================================================================
1040
1041    #[test]
1042    fn test_timestamp_granularity_as_str() {
1043        assert_eq!(TimestampGranularity::Word.as_str(), "word");
1044        assert_eq!(TimestampGranularity::Segment.as_str(), "segment");
1045    }
1046
1047    // =========================================================================
1048    // Speech-to-text models added in 2026.
1049    //
1050    // Model IDs verified against the OpenAI API reference
1051    // (https://developers.openai.com/api/docs/models), August 2026.
1052    // =========================================================================
1053
1054    fn new_stt_models() -> Vec<(SttModel, &'static str)> {
1055        vec![
1056            (SttModel::GptTranscribe, "gpt-transcribe"),
1057            (SttModel::GptLiveTranscribe, "gpt-live-transcribe"),
1058            (SttModel::GptRealtimeWhisper, "gpt-realtime-whisper"),
1059            (SttModel::Gpt4oMiniTranscribe, "gpt-4o-mini-transcribe"),
1060        ]
1061    }
1062
1063    #[test]
1064    fn test_new_stt_models_as_str() {
1065        for (model, expected) in new_stt_models() {
1066            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1067        }
1068    }
1069
1070    #[test]
1071    fn test_new_stt_models_serialization() {
1072        for (model, expected) in new_stt_models() {
1073            let json = serde_json::to_string(&model).unwrap();
1074            assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
1075            let deserialized: SttModel = serde_json::from_str(&json).unwrap();
1076            assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
1077        }
1078    }
1079
1080    /// `gpt-live-transcribe` and `gpt-realtime-whisper` are only exposed on
1081    /// `v1/realtime/transcription_sessions`, not on `v1/audio/transcriptions`.
1082    #[test]
1083    fn test_realtime_only_stt_models_are_flagged() {
1084        assert!(!SttModel::GptLiveTranscribe.supports_file_transcription());
1085        assert!(!SttModel::GptRealtimeWhisper.supports_file_transcription());
1086
1087        assert!(SttModel::GptTranscribe.supports_file_transcription());
1088        assert!(SttModel::Whisper1.supports_file_transcription());
1089        assert!(SttModel::Gpt4oTranscribe.supports_file_transcription());
1090        assert!(SttModel::Gpt4oMiniTranscribe.supports_file_transcription());
1091    }
1092
1093    /// Audio models present in the live /v1/models listing but previously
1094    /// missing from the enums. Verified live against the API (August 2026).
1095    #[test]
1096    fn test_previously_missing_tts_models() {
1097        for (model, expected) in [(TtsModel::Tts1_1106, "tts-1-1106"), (TtsModel::Tts1Hd1106, "tts-1-hd-1106")] {
1098            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1099            let json = serde_json::to_string(&model).unwrap();
1100            assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
1101        }
1102    }
1103
1104    #[test]
1105    fn test_diarizing_stt_model() {
1106        assert_eq!(SttModel::Gpt4oTranscribeDiarize.as_str(), "gpt-4o-transcribe-diarize");
1107        assert!(SttModel::Gpt4oTranscribeDiarize.supports_file_transcription());
1108    }
1109}