use std::pin::Pin;
use async_trait::async_trait;
use futures::Stream;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::error::AiMuxError;
use crate::shared::{SharedHeaders, SharedProviderMetadata, SharedProviderOptions, Warning};
use crate::{AbortSignal, retry, timeout};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum AudioInput {
Binary(Vec<u8>),
Base64(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum AudioChunk {
Binary(Vec<u8>),
Base64(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TranscriptionSegment {
pub text: String,
pub start_second: f64,
pub end_second: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TranscriptionCallOptions {
pub audio: AudioInput,
pub media_type: String,
pub provider_options: Option<SharedProviderOptions>,
#[serde(skip)]
#[ts(skip)]
pub abort_signal: Option<AbortSignal>,
pub max_retries: Option<u32>,
pub timeout: Option<crate::options::TimeoutConfiguration>,
pub headers: Option<SharedHeaders>,
}
impl TranscriptionCallOptions {
pub fn new(audio: AudioInput, media_type: impl Into<String>) -> Self {
Self {
audio,
media_type: media_type.into(),
provider_options: None,
abort_signal: None,
max_retries: None,
timeout: None,
headers: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TranscriptionResult {
pub text: String,
pub segments: Vec<TranscriptionSegment>,
pub language: Option<String>,
pub duration_in_seconds: Option<f64>,
pub warnings: Vec<Warning>,
pub request: Option<TranscriptionRequest>,
pub response: TranscriptionResponse,
pub provider_metadata: Option<SharedProviderMetadata>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TranscriptionRequest {
pub body: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TranscriptionResponse {
pub timestamp: Option<String>,
pub model_id: Option<String>,
pub headers: Option<SharedHeaders>,
pub body: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct InputAudioFormat {
pub format_type: String,
pub rate: Option<u32>,
}
pub struct TranscriptionStreamOptions {
pub audio: Pin<Box<dyn Stream<Item = AudioChunk> + Send>>,
pub input_audio_format: InputAudioFormat,
pub provider_options: Option<SharedProviderOptions>,
pub abort_signal: Option<AbortSignal>,
pub headers: Option<SharedHeaders>,
pub include_raw_chunks: bool,
pub timeout: Option<crate::options::TimeoutConfiguration>,
}
impl std::fmt::Debug for TranscriptionStreamOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TranscriptionStreamOptions")
.field("input_audio_format", &self.input_audio_format)
.field("provider_options", &self.provider_options)
.field("abort_signal", &self.abort_signal)
.field("headers", &self.headers)
.field("include_raw_chunks", &self.include_raw_chunks)
.field("timeout", &self.timeout)
.field("audio", &"<stream>")
.finish()
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub enum TranscriptionStreamPart {
StreamStart { warnings: Vec<Warning> },
TranscriptDelta {
id: Option<String>,
delta: String,
provider_metadata: Option<SharedProviderMetadata>,
},
TranscriptPartial {
id: Option<String>,
text: String,
start_second: Option<f64>,
duration_in_seconds: Option<f64>,
channel_index: Option<u32>,
provider_metadata: Option<SharedProviderMetadata>,
},
TranscriptFinal {
id: Option<String>,
text: String,
start_second: Option<f64>,
end_second: Option<f64>,
channel_index: Option<u32>,
provider_metadata: Option<SharedProviderMetadata>,
},
ResponseMetadata {
timestamp: Option<String>,
model_id: Option<String>,
headers: Option<SharedHeaders>,
body: Option<serde_json::Value>,
},
Finish {
text: String,
segments: Vec<TranscriptionSegment>,
language: Option<String>,
duration_in_seconds: Option<f64>,
provider_metadata: Option<SharedProviderMetadata>,
},
Raw { raw_value: serde_json::Value },
Error { error: serde_json::Value },
}
pub struct TranscriptionStreamResult {
pub stream: Pin<Box<dyn Stream<Item = Result<TranscriptionStreamPart, AiMuxError>> + Send>>,
pub request: Option<TranscriptionRequest>,
pub response: Option<TranscriptionResponse>,
}
impl std::fmt::Debug for TranscriptionStreamResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TranscriptionStreamResult")
.field("request", &self.request)
.field("response", &self.response)
.field("stream", &"<stream>")
.finish()
}
}
#[async_trait]
pub trait TranscriptionModel: Send + Sync {
fn specification_version(&self) -> &'static str {
"v4"
}
fn provider(&self) -> &str;
fn model_id(&self) -> &str;
fn retry_config(&self) -> crate::retry::RetryConfig {
crate::retry::RetryConfig::default()
}
async fn do_generate(
&self,
options: &TranscriptionCallOptions,
) -> Result<TranscriptionResult, AiMuxError>;
async fn do_stream(
&self,
_options: TranscriptionStreamOptions,
) -> Result<TranscriptionStreamResult, AiMuxError> {
Err(AiMuxError::UnsupportedFunctionality(format!(
"transcription streaming is not supported by provider `{}`",
self.provider()
)))
}
}
pub async fn transcribe(
model: &dyn TranscriptionModel,
options: TranscriptionCallOptions,
) -> Result<TranscriptionResult, AiMuxError> {
let timeout = timeout::OperationTimeout::new(options.timeout.unwrap_or_default())?;
let abort_signal = options.abort_signal.clone();
let retries = retry::prepare_retries(
options.max_retries,
model.retry_config(),
abort_signal.clone(),
);
timeout::run(
retries.retry(|| model.do_generate(&options)),
abort_signal.as_ref(),
timeout,
)
.await
}
pub async fn stream_transcribe(
model: &dyn TranscriptionModel,
options: TranscriptionStreamOptions,
) -> Result<TranscriptionStreamResult, AiMuxError> {
model.do_stream(options).await
}