#![allow(deprecated)]
use std::os::raw::c_int;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio_util::sync::CancellationToken;
use crate::detail::api::Api;
use crate::detail::ffi::{flItem, flStreamingCallbackData, FOUNDRY_LOCAL_ITEM_BYTES};
use crate::detail::items::{
make_audio_item, read_speech_segment, read_text_item, SpeechSegmentText,
};
use crate::detail::native::NativeModel;
use crate::detail::session::{NativeItemQueue, NativeRequest, NativeSession};
use crate::detail::task::spawn_blocking;
use crate::error::{FoundryLocalError, Result};
#[derive(Debug, Clone)]
pub struct LiveAudioTranscriptionOptions {
pub sample_rate: u32,
pub channels: u32,
pub language: Option<String>,
}
impl Default for LiveAudioTranscriptionOptions {
fn default() -> Self {
Self {
sample_rate: 16000,
channels: 1,
language: None,
}
}
}
#[derive(Debug, Clone, serde::Deserialize)]
struct LiveAudioTranscriptionRaw {
#[serde(default)]
is_final: bool,
#[serde(default)]
text: String,
start_time: Option<f64>,
end_time: Option<f64>,
id: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ContentPart {
pub text: String,
pub transcript: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LiveAudioTranscriptionResponse {
pub content: Vec<ContentPart>,
pub is_final: bool,
pub start_time: Option<f64>,
pub end_time: Option<f64>,
pub id: Option<String>,
}
impl LiveAudioTranscriptionResponse {
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str::<LiveAudioTranscriptionRaw>(json)
.map(Self::from_raw)
.map_err(FoundryLocalError::from)
}
fn from_raw(raw: LiveAudioTranscriptionRaw) -> Self {
Self {
content: vec![ContentPart {
transcript: raw.text.clone(),
text: raw.text,
}],
is_final: raw.is_final,
start_time: raw.start_time,
end_time: raw.end_time,
id: raw.id,
}
}
fn from_text(text: String, is_final: bool) -> Self {
Self {
content: vec![ContentPart {
transcript: text.clone(),
text,
}],
is_final,
start_time: None,
end_time: None,
id: None,
}
}
fn from_segment(seg: SpeechSegmentText) -> Self {
Self {
content: vec![ContentPart {
transcript: seg.text.clone(),
text: seg.text,
}],
is_final: seg.is_final,
start_time: seg.start_time_s,
end_time: seg.end_time_s,
id: None,
}
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct CoreErrorResponse {
pub code: String,
pub message: String,
#[serde(rename = "isTransient", default)]
pub is_transient: bool,
}
impl CoreErrorResponse {
pub fn try_parse(error_string: &str) -> Option<Self> {
serde_json::from_str(error_string).ok()
}
}
pub struct LiveAudioTranscriptionStream {
rx: UnboundedReceiver<Result<LiveAudioTranscriptionResponse>>,
}
impl futures_core::Stream for LiveAudioTranscriptionStream {
type Item = Result<LiveAudioTranscriptionResponse>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.rx.poll_recv(cx)
}
}
#[derive(Default)]
struct SessionState {
started: bool,
stopped: bool,
queue: Option<Arc<NativeItemQueue>>,
output_rx: Option<UnboundedReceiver<Result<LiveAudioTranscriptionResponse>>>,
worker: Option<tokio::task::JoinHandle<()>>,
}
#[deprecated(
since = "2.0.0",
note = "The OpenAI direct clients are deprecated; use the Session API instead \
(`AudioSession::new(&model)` with streaming)."
)]
pub struct LiveAudioTranscriptionSession {
model: NativeModel,
pub settings: LiveAudioTranscriptionOptions,
state: tokio::sync::Mutex<SessionState>,
}
impl LiveAudioTranscriptionSession {
pub(crate) fn new(_model_id: &str, model: NativeModel) -> Self {
Self {
model,
settings: LiveAudioTranscriptionOptions::default(),
state: tokio::sync::Mutex::new(SessionState::default()),
}
}
pub async fn start(&self, ct: Option<CancellationToken>) -> Result<()> {
let mut state = self.state.lock().await;
if state.started {
return Err(FoundryLocalError::Validation {
reason: "Streaming session already started. Call stop() first.".into(),
});
}
if let Some(token) = &ct {
if token.is_cancelled() {
return Err(FoundryLocalError::CommandExecution {
reason: "Start cancelled".into(),
});
}
}
let settings = self.settings.clone();
let model = self.model.clone();
let api = Arc::clone(&model.api);
let queue = Arc::new(NativeItemQueue::new(api)?);
let (output_tx, output_rx) =
tokio::sync::mpsc::unbounded_channel::<Result<LiveAudioTranscriptionResponse>>();
let worker_queue = Arc::clone(&queue);
let worker = tokio::task::spawn_blocking(move || {
run_worker(model, settings, worker_queue, output_tx);
});
state.started = true;
state.stopped = false;
state.queue = Some(queue);
state.output_rx = Some(output_rx);
state.worker = Some(worker);
Ok(())
}
pub async fn append(&self, pcm_data: &[u8], ct: Option<CancellationToken>) -> Result<()> {
if let Some(token) = &ct {
if token.is_cancelled() {
return Err(FoundryLocalError::CommandExecution {
reason: "Append cancelled".into(),
});
}
}
let queue = {
let state = self.state.lock().await;
if !state.started || state.stopped {
return Err(FoundryLocalError::Validation {
reason: "No active streaming session. Call start() first.".into(),
});
}
state
.queue
.clone()
.ok_or_else(|| FoundryLocalError::Internal {
reason: "Input queue not available — session may be in an invalid state".into(),
})?
};
let data = pcm_data.to_vec();
spawn_blocking(move || queue.push_bytes(&data, FOUNDRY_LOCAL_ITEM_BYTES)).await
}
pub async fn get_stream(&self) -> Result<LiveAudioTranscriptionStream> {
let mut state = self.state.lock().await;
let rx = state
.output_rx
.take()
.ok_or_else(|| FoundryLocalError::Validation {
reason: "No active streaming session, or stream already taken. \
Call start() first and only call get_stream() once."
.into(),
})?;
Ok(LiveAudioTranscriptionStream { rx })
}
pub async fn stop(&self, _ct: Option<CancellationToken>) -> Result<()> {
let worker = {
let mut state = self.state.lock().await;
if !state.started || state.stopped {
return Ok(());
}
state.stopped = true;
if let Some(queue) = &state.queue {
queue.mark_finished();
}
state.worker.take()
};
if let Some(handle) = worker {
let _ = handle.await;
}
Ok(())
}
}
impl Drop for LiveAudioTranscriptionSession {
fn drop(&mut self) {
let state = self.state.get_mut();
if state.started && !state.stopped {
state.stopped = true;
if let Some(queue) = &state.queue {
queue.mark_finished();
}
state.worker.take();
}
}
}
struct LiveCtx {
api: Arc<Api>,
tx: UnboundedSender<Result<LiveAudioTranscriptionResponse>>,
}
unsafe extern "C" fn live_trampoline(
data: flStreamingCallbackData,
user_data: *mut std::ffi::c_void,
) -> c_int {
if user_data.is_null() {
return 0;
}
let result = catch_unwind(AssertUnwindSafe(|| {
let ctx = &*(user_data as *const LiveCtx);
let queue = data.item_queue;
if queue.is_null() {
return 0;
}
let item_api = ctx.api.item_api();
loop {
let mut item: *mut flItem = std::ptr::null_mut();
if !(item_api.ItemQueue_TryPop)(queue, &mut item) {
break;
}
if item.is_null() {
continue;
}
let response = (|| -> Result<Option<LiveAudioTranscriptionResponse>> {
if let Some(text) = read_text_item(&ctx.api, item)? {
return Ok((!text.is_empty())
.then(|| LiveAudioTranscriptionResponse::from_text(text, false)));
}
Ok(read_speech_segment(&ctx.api, item)?
.filter(|seg| !seg.text.is_empty())
.map(LiveAudioTranscriptionResponse::from_segment))
})();
(item_api.Item_Release)(item);
let response = match response {
Ok(response) => response,
Err(error) => {
let _ = ctx.tx.send(Err(error));
return 1; }
};
if let Some(response) = response {
if ctx.tx.send(Ok(response)).is_err() {
return 1; }
}
}
0
}));
result.unwrap_or(1)
}
fn run_worker(
model: NativeModel,
settings: LiveAudioTranscriptionOptions,
queue: Arc<NativeItemQueue>,
output_tx: UnboundedSender<Result<LiveAudioTranscriptionResponse>>,
) {
let api = Arc::clone(&model.api);
let run = (|| -> Result<()> {
let session = NativeSession::create(&model)?;
let guard = session.lock_ops();
let mut ctx = Box::new(LiveCtx {
api: Arc::clone(&api),
tx: output_tx.clone(),
});
let ctx_ptr = &mut *ctx as *mut LiveCtx as *mut std::ffi::c_void;
session.set_streaming_callback(Some(live_trampoline), ctx_ptr)?;
let request = NativeRequest::new(Arc::clone(&api))?;
let format = make_audio_item(
&api,
&[],
Some("pcm"),
settings.sample_rate as i32,
settings.channels as i32,
)?;
request.add_item(format, true)?;
request.add_item(queue.as_item_ptr(), false)?;
let response = session.process_request(&request);
let _ = session.set_streaming_callback(None, std::ptr::null_mut());
drop(guard);
let response = response?;
let mut final_text = String::new();
for i in 0..response.item_count() {
let text = match response.item_text(i)? {
Some(text) => Some(text),
None => response.item_speech_result_text(i)?,
};
if let Some(text) = text {
final_text.push_str(&text);
}
}
drop(ctx);
if !final_text.is_empty() {
let _ = output_tx.send(Ok(LiveAudioTranscriptionResponse::from_text(
final_text, true,
)));
}
Ok(())
})();
if let Err(e) = run {
let _ = output_tx.send(Err(e));
}
drop(queue);
}