openai_interface/audio/
translations.rs1use std::path::PathBuf;
18
19use serde::{Deserialize, Serialize};
20use url::Url;
21
22use crate::{
23 audio::{AudioResponseFormat, transcriptions::TranscriptionVerbose},
24 errors::OapiError,
25 rest::RequestOptions,
26 rest::post::{Post, PostNoStream},
27};
28
29#[derive(Debug, Serialize, Default, Clone)]
32pub struct TranslationRequest {
33 #[serde(skip_serializing)]
38 pub file: PathBuf,
39 pub model: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
45 pub prompt: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
52 pub response_format: Option<AudioResponseFormat>,
53 #[serde(skip_serializing_if = "Option::is_none")]
57 pub temperature: Option<f32>,
58 pub extra_body_map: Option<serde_json::Map<String, serde_json::Value>>,
61}
62
63#[derive(Debug, Deserialize, Serialize, Clone)]
66#[serde(untagged)]
67pub enum TranslationResponse {
68 Verbose(TranscriptionVerbose),
71 Plain(Translation),
73}
74
75#[derive(Debug, Deserialize, Serialize, Clone)]
77pub struct Translation {
78 pub text: String,
80}
81
82crate::impl_from_str!(TranslationResponse);
83
84impl Post for TranslationRequest {
85 #[inline]
86 fn is_streaming(&self) -> bool {
87 false
88 }
89
90 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
94 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
95 url.path_segments_mut()
96 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
97 .push("audio")
98 .push("translations");
99
100 Ok(url.to_string())
101 }
102}
103
104impl PostNoStream for TranslationRequest {
105 type Response = TranslationResponse;
106
107 async fn get_response_string(
110 &self,
111 client: &reqwest::Client,
112 base_url: &str,
113 options: &RequestOptions,
114 ) -> Result<String, OapiError> {
115 if !self.file.exists() {
116 return Err(OapiError::FileNotFoundError(self.file.clone()));
117 }
118
119 let content = tokio::fs::read(&self.file).await?;
120 let file_name = self
121 .file
122 .file_name()
123 .and_then(|name| name.to_str())
124 .ok_or_else(|| OapiError::ResponseError("Invalid file name".to_string()))?
125 .to_string();
126
127 let file_part = reqwest::multipart::Part::bytes(content).file_name(file_name);
128 let mut form = reqwest::multipart::Form::new().part("file", file_part);
129
130 form = form.text("model", self.model.clone());
131
132 if let Some(prompt) = &self.prompt {
133 form = form.text("prompt", prompt.clone());
134 }
135 if let Some(response_format) = self.response_format {
136 let literal = crate::audio::enum_to_literal(&response_format)?;
137 form = form.text("response_format", literal);
138 }
139 if let Some(temperature) = self.temperature {
140 form = form.text("temperature", temperature.to_string());
141 }
142
143 form = crate::rest::post::append_extra_body_map(form, &self.extra_body_map);
144
145 let url = self.build_url(base_url)?;
146 crate::rest::post::post_multipart_json(client, url, form, options).await
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_build_url() {
156 let request = TranslationRequest::default();
157 let url = request.build_url("https://api.openai.com/v1/").unwrap();
158 assert_eq!(url, "https://api.openai.com/v1/audio/translations");
159 }
160
161 #[test]
168 fn parse_plain_response() {
169 let content = r#"{
170 "text": "The quick brown fox jumped over the lazy dog."
171 }"#;
172
173 let response: TranslationResponse = content.parse().unwrap();
174 let TranslationResponse::Plain(translation) = response else {
175 panic!("expected plain translation");
176 };
177 assert_eq!(
178 translation.text,
179 "The quick brown fox jumped over the lazy dog."
180 );
181 }
182}