use std::ffi::CString;
use std::os::raw::{c_char, c_void};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use transcribe_cpp_sys as sys;
use crate::cancel::{abort_trampoline, CancelToken};
use crate::error::{check, Error, Result};
use crate::model::{Model, ModelInner};
use crate::result::{owned_str, Timings};
use crate::session::clamp_len;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LangIdInfo {
pub sample_rate: i32,
pub n_labels: i32,
pub min_audio_ms: i32,
pub max_audio_ms: i32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct LangIdSessionOptions {
pub n_threads: i32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct LangIdOptions {
pub allowed: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct LangIdCandidate {
pub index: i32,
pub code: String,
pub name: String,
pub p: f32,
pub logit: f32,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct LangIdResult {
pub candidates: Vec<LangIdCandidate>,
pub allowed_mass: f32,
}
impl LangIdResult {
pub fn code(&self) -> Option<&str> {
self.candidates.first().map(|c| c.code.as_str())
}
}
pub struct LangIdSession {
ptr: *mut sys::transcribe_langid_session,
model: Arc<ModelInner>,
cancel: Option<Arc<AtomicBool>>,
}
impl std::fmt::Debug for LangIdSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LangIdSession").finish_non_exhaustive()
}
}
unsafe impl Send for LangIdSession {}
impl Drop for LangIdSession {
fn drop(&mut self) {
unsafe { sys::transcribe_langid_session_free(self.ptr) };
}
}
impl LangIdSession {
pub(crate) fn new(model: &Model, options: &LangIdSessionOptions) -> Result<LangIdSession> {
let mut params: sys::transcribe_langid_session_params = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_langid_session_params_init(&mut params) };
params.n_threads = options.n_threads;
let mut out: *mut sys::transcribe_langid_session = std::ptr::null_mut();
let status =
unsafe { sys::transcribe_langid_session_init(model.inner.ptr, ¶ms, &mut out) };
check(status, "langid session init")?;
debug_assert!(!out.is_null());
Ok(LangIdSession {
ptr: out,
model: Arc::clone(&model.inner),
cancel: None,
})
}
pub fn set_cancel_token(&mut self, token: &CancelToken) {
let flag = Arc::clone(&token.flag);
let userdata = Arc::as_ptr(&flag) as *mut c_void;
unsafe {
sys::transcribe_langid_set_abort_callback(self.ptr, Some(abort_trampoline), userdata)
};
self.cancel = Some(flag);
}
pub fn clear_cancel_token(&mut self) {
unsafe { sys::transcribe_langid_set_abort_callback(self.ptr, None, std::ptr::null_mut()) };
self.cancel = None;
}
pub fn run(&mut self, pcm: &[f32], options: &LangIdOptions) -> Result<LangIdResult> {
let n = clamp_len(pcm.len())?;
let codes: Option<Vec<CString>> = match &options.allowed {
None => None,
Some(list) if list.is_empty() => {
return Err(Error::InvalidArgument(
"allowed is empty; pass None for every label".into(),
));
}
Some(list) => Some(
list.iter()
.map(|c| CString::new(c.as_str()))
.collect::<std::result::Result<_, _>>()?,
),
};
let ptrs: Option<Vec<*const c_char>> = codes
.as_ref()
.map(|cs| cs.iter().map(|c| c.as_ptr()).collect());
let mut params: sys::transcribe_langid_params = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_langid_params_init(&mut params) };
if let Some(p) = &ptrs {
params.allowed = p.as_ptr();
params.n_allowed = i32::try_from(p.len())
.map_err(|_| Error::InvalidArgument("allowed list too long".into()))?;
}
let ptr = self.ptr;
self.model.with_compute(
Some("a stream is active on this model; finish or drop it before langid run()"),
|_| -> Result<LangIdResult> {
check(
unsafe { sys::transcribe_langid_run(ptr, pcm.as_ptr(), n, ¶ms) },
"langid run",
)?;
let mut res: sys::transcribe_langid_result = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_langid_result_init(&mut res) };
check(
unsafe { sys::transcribe_langid_get_result(ptr, &mut res) },
"langid result",
)?;
let candidates = (0..res.n_candidates)
.map(|i| {
let mut raw: sys::transcribe_langid_candidate =
unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_langid_candidate_init(&mut raw) };
let _ = unsafe { sys::transcribe_langid_get_candidate(ptr, i, &mut raw) };
LangIdCandidate {
index: raw.index,
code: owned_str(raw.code),
name: owned_str(raw.name),
p: raw.p,
logit: raw.logit,
}
})
.collect();
Ok(LangIdResult {
candidates,
allowed_mass: res.allowed_mass,
})
},
)?
}
pub fn timings(&self) -> Timings {
let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_timings_init(&mut raw) };
let _ = unsafe { sys::transcribe_langid_get_timings(self.ptr, &mut raw) };
Timings::from_raw(&raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn null_session(streaming: bool) -> LangIdSession {
LangIdSession {
ptr: std::ptr::null_mut(),
model: Arc::new(ModelInner {
ptr: std::ptr::null_mut(),
compute_lock: Mutex::new(streaming),
}),
cancel: None,
}
}
#[test]
fn run_is_busy_while_a_stream_holds_the_lease() {
let err = null_session(true)
.run(&[0.0; 160], &LangIdOptions::default())
.unwrap_err();
let Error::Busy(msg) = err else {
panic!("expected Busy, got {err:?}");
};
assert_eq!(
msg,
"a stream is active on this model; finish or drop it before langid run()"
);
}
#[test]
fn empty_allowed_is_rejected_not_all() {
let opts = LangIdOptions {
allowed: Some(vec![]),
};
let err = null_session(false).run(&[0.0; 160], &opts).unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)), "{err:?}");
}
#[test]
fn interior_nul_in_allowed_is_rejected() {
let opts = LangIdOptions {
allowed: Some(vec!["e\0n".into()]),
};
let err = null_session(false).run(&[0.0; 160], &opts).unwrap_err();
assert!(matches!(err, Error::Nul(_)), "{err:?}");
}
}