openai_interface/audio/
speech.rs1use serde::{Deserialize, Serialize};
19use url::Url;
20
21use crate::{
22 errors::OapiError,
23 rest::post::{Post, PostBinary},
24};
25
26#[derive(Debug, Serialize, Deserialize, Default, Clone)]
28pub struct SpeechRequest {
29 pub input: String,
32 pub model: String,
35 pub voice: SpeechVoice,
42 #[serde(skip_serializing_if = "Option::is_none")]
45 pub instructions: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
49 pub response_format: Option<SpeechFormat>,
50 #[serde(skip_serializing_if = "Option::is_none")]
53 pub speed: Option<f32>,
54 #[serde(skip_serializing_if = "Option::is_none")]
61 pub stream_format: Option<SpeechStreamFormat>,
62}
63
64#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
66#[serde(rename_all = "lowercase")]
67pub enum SpeechStreamFormat {
68 Sse,
71 Audio,
73}
74
75#[derive(Debug, Serialize, Deserialize, Clone)]
77#[serde(untagged)]
78pub enum SpeechVoice {
79 BuiltIn(String),
81 Custom {
83 id: String,
85 },
86}
87
88impl Default for SpeechVoice {
89 fn default() -> Self {
90 Self::BuiltIn(String::new())
91 }
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
96#[serde(rename_all = "snake_case")]
97pub enum SpeechFormat {
98 Mp3,
99 Opus,
100 Aac,
101 Flac,
102 Wav,
103 Pcm,
104}
105
106impl Post for SpeechRequest {
107 #[inline]
108 fn is_streaming(&self) -> bool {
109 false
110 }
111
112 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
116 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
117 url.path_segments_mut()
118 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
119 .push("audio")
120 .push("speech");
121
122 Ok(url.to_string())
123 }
124}
125
126impl PostBinary for SpeechRequest {}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
134 fn request_serialization() {
135 let request = SpeechRequest {
136 input: "The quick brown fox jumped over the lazy dog.".to_string(),
137 model: "gpt-4o-mini-tts".to_string(),
138 voice: SpeechVoice::BuiltIn("alloy".to_string()),
139 instructions: Some("Voice: cheerful.".to_string()),
140 response_format: Some(SpeechFormat::Wav),
141 speed: Some(1.5),
142 stream_format: None,
143 };
144
145 let json = serde_json::to_string(&request).unwrap();
146 assert!(
147 json.contains(r#""input":"The quick brown fox jumped over the lazy dog.""#),
148 "json: {json}"
149 );
150 assert!(
151 json.contains(r#""model":"gpt-4o-mini-tts""#),
152 "json: {json}"
153 );
154 assert!(json.contains(r#""voice":"alloy""#), "json: {json}");
155 assert!(
156 json.contains(r#""instructions":"Voice: cheerful.""#),
157 "json: {json}"
158 );
159 assert!(json.contains(r#""response_format":"wav""#), "json: {json}");
160 assert!(json.contains(r#""speed":1.5"#), "json: {json}");
161 }
162
163 #[test]
165 fn custom_voice_serialization() {
166 let request = SpeechRequest {
167 input: "Hello".to_string(),
168 model: "gpt-4o-mini-tts".to_string(),
169 voice: SpeechVoice::Custom {
170 id: "voice_1234".to_string(),
171 },
172 ..Default::default()
173 };
174
175 let json = serde_json::to_string(&request).unwrap();
176 assert!(
177 json.contains(r#""voice":{"id":"voice_1234"}"#),
178 "json: {json}"
179 );
180 }
181
182 #[test]
185 fn stream_format_serialization() {
186 let request = SpeechRequest {
187 input: "Hello".to_string(),
188 model: "gpt-4o-mini-tts".to_string(),
189 voice: SpeechVoice::BuiltIn("alloy".to_string()),
190 stream_format: Some(SpeechStreamFormat::Sse),
191 ..Default::default()
192 };
193 let json = serde_json::to_string(&request).unwrap();
194 assert!(json.contains(r#""stream_format":"sse""#), "json: {json}");
195
196 let request = SpeechRequest {
197 stream_format: Some(SpeechStreamFormat::Audio),
198 ..request
199 };
200 let json = serde_json::to_string(&request).unwrap();
201 assert!(json.contains(r#""stream_format":"audio""#), "json: {json}");
202
203 let request = SpeechRequest {
204 stream_format: None,
205 ..request
206 };
207 let json = serde_json::to_string(&request).unwrap();
208 assert!(!json.contains("stream_format"), "json: {json}");
209 }
210
211 #[test]
212 fn test_build_url() {
213 let request = SpeechRequest::default();
214 let url = request.build_url("https://api.openai.com/v1/").unwrap();
215 assert_eq!(url, "https://api.openai.com/v1/audio/speech");
216 }
217}