use reqwest::multipart::Form;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::client::OpenAiClient;
use crate::error::OpenAiError;
use crate::file::FilePart;
pub struct Audio<'a> {
pub(crate) client: &'a OpenAiClient,
}
impl Audio<'_> {
pub async fn speech(&self, request: &SpeechRequest) -> Result<Vec<u8>, OpenAiError> {
self.client.post_json_bytes("/audio/speech", request).await
}
pub async fn transcribe(
&self,
request: TranscriptionRequest,
) -> Result<Transcript, OpenAiError> {
let json_response = request.expects_json();
let text = self
.client
.post_multipart_text("/audio/transcriptions", request.into_form()?)
.await?;
parse_transcript(json_response, text)
}
pub async fn translate(&self, request: TranslationRequest) -> Result<Transcript, OpenAiError> {
let json_response = request.expects_json();
let text = self
.client
.post_multipart_text("/audio/translations", request.into_form()?)
.await?;
parse_transcript(json_response, text)
}
}
fn parse_transcript(json_response: bool, text: String) -> Result<Transcript, OpenAiError> {
if json_response {
return serde_json::from_str(&text).map_err(|e| OpenAiError::parse(e, &text));
}
Ok(Transcript {
text,
..Default::default()
})
}
#[derive(Debug, Clone, Serialize)]
pub struct SpeechRequest {
model: String,
input: String,
voice: String,
#[serde(skip_serializing_if = "Option::is_none")]
response_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
speed: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
}
impl SpeechRequest {
pub fn new(
model: impl Into<String>,
input: impl Into<String>,
voice: impl Into<String>,
) -> Self {
Self {
model: model.into(),
input: input.into(),
voice: voice.into(),
response_format: None,
speed: None,
instructions: None,
}
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.speed = Some(speed);
self
}
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
}
#[derive(Debug, Clone)]
pub struct TranscriptionRequest {
model: String,
file: FilePart,
language: Option<String>,
prompt: Option<String>,
response_format: Option<String>,
temperature: Option<f32>,
}
impl TranscriptionRequest {
pub fn new(model: impl Into<String>, file: FilePart) -> Self {
Self {
model: model.into(),
file,
language: None,
prompt: None,
response_format: None,
temperature: None,
}
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
fn expects_json(&self) -> bool {
matches!(
self.response_format.as_deref(),
None | Some("json") | Some("verbose_json")
)
}
fn into_form(self) -> Result<Form, OpenAiError> {
let mut form = Form::new()
.text("model", self.model)
.part("file", self.file.into_part()?);
if let Some(language) = self.language {
form = form.text("language", language);
}
if let Some(prompt) = self.prompt {
form = form.text("prompt", prompt);
}
if let Some(response_format) = self.response_format {
form = form.text("response_format", response_format);
}
if let Some(temperature) = self.temperature {
form = form.text("temperature", temperature.to_string());
}
Ok(form)
}
}
#[derive(Debug, Clone)]
pub struct TranslationRequest {
model: String,
file: FilePart,
prompt: Option<String>,
response_format: Option<String>,
temperature: Option<f32>,
}
impl TranslationRequest {
pub fn new(model: impl Into<String>, file: FilePart) -> Self {
Self {
model: model.into(),
file,
prompt: None,
response_format: None,
temperature: None,
}
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
fn expects_json(&self) -> bool {
matches!(
self.response_format.as_deref(),
None | Some("json") | Some("verbose_json")
)
}
fn into_form(self) -> Result<Form, OpenAiError> {
let mut form = Form::new()
.text("model", self.model)
.part("file", self.file.into_part()?);
if let Some(prompt) = self.prompt {
form = form.text("prompt", prompt);
}
if let Some(response_format) = self.response_format {
form = form.text("response_format", response_format);
}
if let Some(temperature) = self.temperature {
form = form.text("temperature", temperature.to_string());
}
Ok(form)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Transcript {
#[serde(default)]
pub text: String,
pub language: Option<String>,
pub duration: Option<f64>,
pub segments: Option<Value>,
pub words: Option<Value>,
pub usage: Option<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serializes_speech_request() {
let request = SpeechRequest::new("gpt-4o-mini-tts", "こんにちは", "marin")
.response_format("wav")
.instructions("Speak slowly");
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["voice"], "marin");
assert_eq!(value["response_format"], "wav");
assert!(value.get("speed").is_none());
}
#[test]
fn parses_json_transcript() {
let transcript =
parse_transcript(true, json!({"text": "hello", "language": "en"}).to_string()).unwrap();
assert_eq!(transcript.text, "hello");
assert_eq!(transcript.language.as_deref(), Some("en"));
}
#[test]
fn wraps_plain_text_transcript() {
let transcript = parse_transcript(false, "1\n00:00:00,000 --> ...".to_string()).unwrap();
assert!(transcript.text.starts_with("1\n"));
assert!(transcript.language.is_none());
}
#[test]
fn detects_expected_response_kind() {
let file = FilePart::new("a.mp3", vec![0]);
assert!(TranscriptionRequest::new("whisper-1", file.clone()).expects_json());
assert!(TranscriptionRequest::new("whisper-1", file.clone())
.response_format("verbose_json")
.expects_json());
assert!(!TranscriptionRequest::new("whisper-1", file)
.response_format("srt")
.expects_json());
}
}