openai_interface/audio/
speech.rs1use serde::Serialize;
14use url::Url;
15
16use crate::{
17 errors::OapiError,
18 rest::post::{Post, PostBinary},
19};
20
21#[derive(Debug, Serialize, Default, Clone)]
23pub struct SpeechRequest {
24 pub input: String,
27 pub model: String,
30 pub voice: SpeechVoice,
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub instructions: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
44 pub response_format: Option<SpeechFormat>,
45 #[serde(skip_serializing_if = "Option::is_none")]
48 pub speed: Option<f32>,
49}
50
51#[derive(Debug, Serialize, Clone)]
53#[serde(untagged)]
54pub enum SpeechVoice {
55 BuiltIn(String),
57 Custom {
59 id: String,
61 },
62}
63
64impl Default for SpeechVoice {
65 fn default() -> Self {
66 Self::BuiltIn(String::new())
67 }
68}
69
70#[derive(Debug, Serialize, Clone, Copy)]
72#[serde(rename_all = "snake_case")]
73pub enum SpeechFormat {
74 Mp3,
75 Opus,
76 Aac,
77 Flac,
78 Wav,
79 Pcm,
80}
81
82impl Post for SpeechRequest {
83 #[inline]
84 fn is_streaming(&self) -> bool {
85 false
86 }
87
88 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
92 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
93 url.path_segments_mut()
94 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
95 .push("audio")
96 .push("speech");
97
98 Ok(url.to_string())
99 }
100}
101
102impl PostBinary for SpeechRequest {}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
110 fn request_serialization() {
111 let request = SpeechRequest {
112 input: "The quick brown fox jumped over the lazy dog.".to_string(),
113 model: "gpt-4o-mini-tts".to_string(),
114 voice: SpeechVoice::BuiltIn("alloy".to_string()),
115 instructions: Some("Voice: cheerful.".to_string()),
116 response_format: Some(SpeechFormat::Wav),
117 speed: Some(1.5),
118 };
119
120 let json = serde_json::to_string(&request).unwrap();
121 assert!(
122 json.contains(r#""input":"The quick brown fox jumped over the lazy dog.""#),
123 "json: {json}"
124 );
125 assert!(
126 json.contains(r#""model":"gpt-4o-mini-tts""#),
127 "json: {json}"
128 );
129 assert!(json.contains(r#""voice":"alloy""#), "json: {json}");
130 assert!(
131 json.contains(r#""instructions":"Voice: cheerful.""#),
132 "json: {json}"
133 );
134 assert!(json.contains(r#""response_format":"wav""#), "json: {json}");
135 assert!(json.contains(r#""speed":1.5"#), "json: {json}");
136 }
137
138 #[test]
140 fn custom_voice_serialization() {
141 let request = SpeechRequest {
142 input: "Hello".to_string(),
143 model: "gpt-4o-mini-tts".to_string(),
144 voice: SpeechVoice::Custom {
145 id: "voice_1234".to_string(),
146 },
147 ..Default::default()
148 };
149
150 let json = serde_json::to_string(&request).unwrap();
151 assert!(
152 json.contains(r#""voice":{"id":"voice_1234"}"#),
153 "json: {json}"
154 );
155 }
156
157 #[test]
158 fn test_build_url() {
159 let request = SpeechRequest::default();
160 let url = request.build_url("https://api.openai.com/v1/").unwrap();
161 assert_eq!(url, "https://api.openai.com/v1/audio/speech");
162 }
163}