aither-openai 0.6.0

OpenAI provider integration for aither
Documentation
use crate::{
    client::{Config, OpenAI},
    error::OpenAIError,
};
use aither_core::audio::{AudioGenerator, AudioTranscriber, Data};
use futures_lite::StreamExt;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use zenwave::{
    Client, client, header,
    multipart::{MultipartPart, encode as encode_multipart},
};

impl AudioGenerator for OpenAI {
    type Error = OpenAIError;

    fn generate(
        &self,
        prompt: &str,
    ) -> impl futures_core::Stream<Item = Result<Data, Self::Error>> + Send {
        let cfg = self.config();
        let text = prompt.to_owned();
        futures_lite::stream::iter(vec![synthesize(cfg, text)]).then(|fut| fut)
    }
}

impl AudioTranscriber for OpenAI {
    type Error = OpenAIError;

    fn transcribe(
        &self,
        audio: &[u8],
    ) -> impl futures_core::Stream<Item = Result<String, Self::Error>> + Send {
        let cfg = self.config();
        let payload = audio.to_vec();
        futures_lite::stream::iter(vec![transcribe_once(cfg, payload)]).then(|fut| fut)
    }
}

async fn synthesize(cfg: Arc<Config>, text: String) -> Result<Vec<u8>, OpenAIError> {
    let endpoint = cfg.request_url("/audio/speech");
    let mut backend = client();
    let mut builder = backend
        .post(endpoint)
        .map_err(OpenAIError::Http)?
        .header(header::AUTHORIZATION.as_str(), cfg.request_auth())
        .map_err(OpenAIError::Http)?
        .header(header::USER_AGENT.as_str(), "aither-openai/0.1")
        .map_err(OpenAIError::Http)?;
    if let Some(org) = &cfg.organization {
        builder = builder
            .header("OpenAI-Organization", org.clone())
            .map_err(OpenAIError::Http)?;
    }

    let request = SpeechRequest {
        model: &cfg.audio_model,
        input: &text,
        voice: &cfg.audio_voice,
        format: &cfg.audio_format,
    };
    let bytes = builder
        .json_body(&request)
        .map_err(OpenAIError::Http)?
        .bytes()
        .await
        .map_err(OpenAIError::Http)?;

    Ok(bytes.to_vec())
}

async fn transcribe_once(cfg: Arc<Config>, audio: Vec<u8>) -> Result<String, OpenAIError> {
    let endpoint = cfg.request_url("/audio/transcriptions");
    let mut backend = client();
    let mut builder = backend
        .post(endpoint)
        .map_err(OpenAIError::Http)?
        .header(header::AUTHORIZATION.as_str(), cfg.request_auth())
        .map_err(OpenAIError::Http)?
        .header(header::USER_AGENT.as_str(), "aither-openai/0.1")
        .map_err(OpenAIError::Http)?;
    if let Some(org) = &cfg.organization {
        builder = builder
            .header("OpenAI-Organization", org.clone())
            .map_err(OpenAIError::Http)?;
    }

    let parts = vec![
        MultipartPart::text("model", cfg.transcription_model.clone()),
        MultipartPart::text("response_format", "json"),
        MultipartPart::binary("file", "audio.wav", "application/octet-stream", audio),
    ];

    let (boundary, body) = encode_multipart(parts);
    let response: TranscriptionResponse = builder
        .header(
            header::CONTENT_TYPE.as_str(),
            format!("multipart/form-data; boundary={boundary}"),
        )
        .map_err(OpenAIError::Http)?
        .bytes_body(body)
        .json()
        .await
        .map_err(OpenAIError::Http)?;

    Ok(response.text)
}

#[derive(Debug, Serialize)]
struct SpeechRequest<'a> {
    model: &'a str,
    input: &'a str,
    voice: &'a str,
    #[serde(rename = "response_format")]
    format: &'a str,
}

#[derive(Debug, Deserialize)]
struct TranscriptionResponse {
    text: String,
}