use std::sync::Arc;
use std::time::Duration;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use aimux_core::error::AiMuxError;
use aimux_core::shared::AbortSignal;
use aimux_core::transcription_model::{
AudioChunk, InputAudioFormat, TranscriptionModel, TranscriptionStreamOptions,
TranscriptionStreamPart,
};
use crate::runtime;
const AUDIO_CHANNEL_CAPACITY: usize = 64;
const PARTS_CHANNEL_CAPACITY: usize = 256;
const JOIN_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Default)]
pub struct SessionOptions {
pub input_audio_format: Option<InputAudioFormat>,
pub provider_options: Option<std::collections::HashMap<String, serde_json::Value>>,
pub headers: Option<std::collections::HashMap<String, String>>,
pub include_raw_chunks: bool,
pub timeout: Option<aimux_core::options::TimeoutConfiguration>,
}
pub struct TranscriptionFfiSession {
audio_tx: std::sync::Mutex<Option<mpsc::Sender<AudioChunk>>>,
parts_rx: tokio::sync::Mutex<mpsc::Receiver<Result<TranscriptionStreamPart, AiMuxError>>>,
token: AbortSignal,
task: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl TranscriptionFfiSession {
pub fn spawn(
model: Arc<dyn TranscriptionModel>,
opts: SessionOptions,
user_abort: Option<AbortSignal>,
) -> Arc<Self> {
let (audio_tx, audio_rx) = mpsc::channel::<AudioChunk>(AUDIO_CHANNEL_CAPACITY);
let (mut parts_tx, parts_rx) =
mpsc::channel::<Result<TranscriptionStreamPart, AiMuxError>>(PARTS_CHANNEL_CAPACITY);
let token = AbortSignal::new();
let effective = AbortSignal::new();
for source in std::iter::once(token.clone()).chain(user_abort) {
let linked = effective.clone();
runtime().spawn(async move {
source.cancelled().await;
linked.abort();
});
}
let task = runtime().spawn(async move {
let options = TranscriptionStreamOptions {
audio: Box::pin(audio_rx),
input_audio_format: opts.input_audio_format.unwrap_or(InputAudioFormat {
format_type: "audio/pcm".to_string(),
rate: None,
}),
provider_options: opts.provider_options,
abort_signal: Some(effective.clone()),
headers: opts.headers,
include_raw_chunks: opts.include_raw_chunks,
timeout: opts.timeout,
};
let result = model.do_stream(options).await;
match result {
Ok(stream_result) => {
let mut stream = stream_result.stream;
while let Some(part) = stream.next().await {
let mut part = part;
loop {
match parts_tx.try_send(part) {
Ok(()) => break,
Err(send_err) => {
if send_err.is_disconnected() {
return;
}
part = send_err.into_inner();
tokio::select! {
_ = effective.cancelled() => return,
res = parts_tx.flush() => {
let _ = res;
}
}
}
}
}
}
}
Err(e) => {
let mut err = Some(e);
loop {
let e = err.take().expect("err only None after successful send");
match parts_tx.try_send(Err(e)) {
Ok(()) => break,
Err(send_err) => {
if send_err.is_disconnected() {
return;
}
err = send_err.into_inner().err();
tokio::select! {
_ = effective.cancelled() => return,
res = parts_tx.flush() => {
let _ = res;
}
}
}
}
}
}
}
});
Arc::new(Self {
audio_tx: std::sync::Mutex::new(Some(audio_tx)),
parts_rx: tokio::sync::Mutex::new(parts_rx),
token,
task: std::sync::Mutex::new(Some(task)),
})
}
pub async fn push_audio(&self, bytes: Vec<u8>) -> Result<(), AiMuxError> {
let mut tx = {
let guard = self.audio_tx.lock().expect("session audio mutex poisoned");
match guard.as_ref() {
None => {
return Err(AiMuxError::Other(
"transcription session audio input already finished".into(),
));
}
Some(tx) => tx.clone(),
}
};
tx.send(AudioChunk::Binary(bytes))
.await
.map_err(|_| AiMuxError::Other("transcription session ended".into()))
}
pub fn input_done(&self) {
self.audio_tx
.lock()
.expect("session audio mutex poisoned")
.take();
}
pub async fn next_part(
&self,
timeout: Option<Duration>,
) -> Result<Option<Result<TranscriptionStreamPart, AiMuxError>>, AiMuxError> {
let mut rx = self.parts_rx.lock().await;
let recv = rx.next();
match timeout {
None => Ok(recv.await),
Some(d) => match tokio::time::timeout(d, recv).await {
Ok(part) => Ok(part),
Err(_) => Err(AiMuxError::Timeout(
"no transcription part within timeout".into(),
)),
},
}
}
pub fn terminate(&self) {
self.input_done();
self.token.abort();
let task = self
.task
.lock()
.expect("session task mutex poisoned")
.take();
if let Some(task) = task {
let joined =
crate::ffi_block_on(async { tokio::time::timeout(JOIN_TIMEOUT, task).await });
if joined.is_err() {
tracing::warn!(
"aimux-ffi: transcription session join timed out or was re-entrant; detaching"
);
}
}
}
}