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}
59
60#[derive(Debug, Deserialize, Clone)]
63#[serde(untagged)]
64pub enum TranslationResponse {
65 Verbose(TranscriptionVerbose),
68 Plain(Translation),
70}
71
72#[derive(Debug, Deserialize, Clone)]
74pub struct Translation {
75 pub text: String,
77}
78
79crate::impl_from_str!(TranslationResponse);
80
81impl Post for TranslationRequest {
82 #[inline]
83 fn is_streaming(&self) -> bool {
84 false
85 }
86
87 fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
91 let mut url = Url::parse(base_url.trim_end_matches('/')).map_err(OapiError::UrlError)?;
92 url.path_segments_mut()
93 .map_err(|_| OapiError::UrlCannotBeBase(base_url.to_string()))?
94 .push("audio")
95 .push("translations");
96
97 Ok(url.to_string())
98 }
99}
100
101impl PostNoStream for TranslationRequest {
102 type Response = TranslationResponse;
103
104 async fn get_response_string(
107 &self,
108 client: &reqwest::Client,
109 base_url: &str,
110 options: &RequestOptions,
111 ) -> Result<String, OapiError> {
112 if !self.file.exists() {
113 return Err(OapiError::FileNotFoundError(self.file.clone()));
114 }
115
116 let content = tokio::fs::read(&self.file).await?;
117 let file_name = self
118 .file
119 .file_name()
120 .and_then(|name| name.to_str())
121 .ok_or_else(|| OapiError::ResponseError("Invalid file name".to_string()))?
122 .to_string();
123
124 let file_part = reqwest::multipart::Part::bytes(content).file_name(file_name);
125 let mut form = reqwest::multipart::Form::new().part("file", file_part);
126
127 form = form.text("model", self.model.clone());
128
129 if let Some(prompt) = &self.prompt {
130 form = form.text("prompt", prompt.clone());
131 }
132 if let Some(response_format) = self.response_format {
133 let literal = crate::audio::enum_to_literal(&response_format)?;
134 form = form.text("response_format", literal);
135 }
136 if let Some(temperature) = self.temperature {
137 form = form.text("temperature", temperature.to_string());
138 }
139
140 let url = self.build_url(base_url)?;
141 crate::rest::post::post_multipart_json(client, url, form, options).await
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_build_url() {
151 let request = TranslationRequest::default();
152 let url = request.build_url("https://api.openai.com/v1/").unwrap();
153 assert_eq!(url, "https://api.openai.com/v1/audio/translations");
154 }
155
156 #[test]
163 fn parse_plain_response() {
164 let content = r#"{
165 "text": "The quick brown fox jumped over the lazy dog."
166 }"#;
167
168 let response: TranslationResponse = content.parse().unwrap();
169 let TranslationResponse::Plain(translation) = response else {
170 panic!("expected plain translation");
171 };
172 assert_eq!(
173 translation.text,
174 "The quick brown fox jumped over the lazy dog."
175 );
176 }
177}