Skip to main content

transcribe_cpp/
langid.rs

1//! [`LangIdSession`] — the LANGID role (which language is spoken), from
2//! [`Model::langid_session`]. Threading, lifetime, and compute-lock rules are
3//! those of [`Session`](crate::Session).
4
5use std::ffi::CString;
6use std::os::raw::{c_char, c_void};
7use std::sync::atomic::AtomicBool;
8use std::sync::Arc;
9
10use transcribe_cpp_sys as sys;
11
12use crate::cancel::{abort_trampoline, CancelToken};
13use crate::error::{check, Error, Result};
14use crate::model::{Model, ModelInner};
15use crate::result::{owned_str, Timings};
16use crate::session::clamp_len;
17
18/// Static facts about a language ID model ([`Model::langid_info`]).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct LangIdInfo {
23    /// Input PCM rate (16000).
24    pub sample_rate: i32,
25    /// Label indices are `[0, n_labels)`.
26    pub n_labels: i32,
27    /// Shorter scored audio is [`Error::InputTooShort`].
28    pub min_audio_ms: i32,
29    /// Longer input is scored on its first `max_audio_ms` (30000).
30    pub max_audio_ms: i32,
31}
32
33/// Options for creating a language ID session.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35#[cfg_attr(
36    feature = "serde",
37    derive(serde::Serialize, serde::Deserialize),
38    serde(default)
39)]
40pub struct LangIdSessionOptions {
41    /// CPU threads for ops that run on CPU; 0 = library default.
42    pub n_threads: i32,
43}
44
45/// Per-run language ID parameters.
46#[derive(Debug, Clone, Default, PartialEq, Eq)]
47#[cfg_attr(
48    feature = "serde",
49    derive(serde::Serialize, serde::Deserialize),
50    serde(default)
51)]
52pub struct LangIdOptions {
53    /// Restrict the decision to these codes or aliases. `None` means every
54    /// label; `Some` of an empty list is [`Error::InvalidArgument`]. An
55    /// unknown code is [`Error::Unsupported`].
56    pub allowed: Option<Vec<String>>,
57}
58
59/// One ranked label. `code` is the model's own label (`"en"`, `"iw"`).
60#[derive(Debug, Clone, Default, PartialEq)]
61#[non_exhaustive]
62#[cfg_attr(
63    feature = "serde",
64    derive(serde::Serialize, serde::Deserialize),
65    serde(default)
66)]
67pub struct LangIdCandidate {
68    /// Label index.
69    pub index: i32,
70    pub code: String,
71    pub name: String,
72    /// Softmax renormalized over the allowed set.
73    pub p: f32,
74    pub logit: f32,
75}
76
77/// The result of one [`LangIdSession::run`].
78#[derive(Debug, Clone, Default, PartialEq)]
79#[non_exhaustive]
80#[cfg_attr(
81    feature = "serde",
82    derive(serde::Serialize, serde::Deserialize),
83    serde(default)
84)]
85pub struct LangIdResult {
86    /// Every allowed label (duplicates once; all labels when unrestricted),
87    /// ranked by `p`, descending; ties keep label order.
88    pub candidates: Vec<LangIdCandidate>,
89    /// Unrestricted probability inside the allowed set (1.0 when
90    /// unrestricted); a low value means the speech is probably outside it.
91    pub allowed_mass: f32,
92}
93
94impl LangIdResult {
95    /// The top candidate's code, if any.
96    pub fn code(&self) -> Option<&str> {
97        self.candidates.first().map(|c| c.code.as_str())
98    }
99}
100
101/// A language ID session.
102pub struct LangIdSession {
103    ptr: *mut sys::transcribe_langid_session,
104    // Keeps the native model alive and carries the per-model compute lock.
105    model: Arc<ModelInner>,
106    // Keeps the abort callback's userdata alive while installed.
107    cancel: Option<Arc<AtomicBool>>,
108}
109
110impl std::fmt::Debug for LangIdSession {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("LangIdSession").finish_non_exhaustive()
113    }
114}
115
116// SAFETY: as for `Session` — `&mut self` on the mutating calls keeps use to
117// one thread at a time; deliberately NOT Sync.
118unsafe impl Send for LangIdSession {}
119
120impl Drop for LangIdSession {
121    fn drop(&mut self) {
122        unsafe { sys::transcribe_langid_session_free(self.ptr) };
123    }
124}
125
126impl LangIdSession {
127    pub(crate) fn new(model: &Model, options: &LangIdSessionOptions) -> Result<LangIdSession> {
128        let mut params: sys::transcribe_langid_session_params = unsafe { std::mem::zeroed() };
129        unsafe { sys::transcribe_langid_session_params_init(&mut params) };
130        params.n_threads = options.n_threads;
131
132        let mut out: *mut sys::transcribe_langid_session = std::ptr::null_mut();
133        let status =
134            unsafe { sys::transcribe_langid_session_init(model.inner.ptr, &params, &mut out) };
135        check(status, "langid session init")?;
136        debug_assert!(!out.is_null());
137
138        Ok(LangIdSession {
139            ptr: out,
140            model: Arc::clone(&model.inner),
141            cancel: None,
142        })
143    }
144
145    /// Install a [`CancelToken`] so an in-flight run can be aborted from
146    /// another thread (the run then returns [`Error::Aborted`]).
147    /// Replaces any previously installed token.
148    pub fn set_cancel_token(&mut self, token: &CancelToken) {
149        let flag = Arc::clone(&token.flag);
150        let userdata = Arc::as_ptr(&flag) as *mut c_void;
151        unsafe {
152            sys::transcribe_langid_set_abort_callback(self.ptr, Some(abort_trampoline), userdata)
153        };
154        self.cancel = Some(flag);
155    }
156
157    /// Remove any installed cancel token.
158    pub fn clear_cancel_token(&mut self) {
159        unsafe { sys::transcribe_langid_set_abort_callback(self.ptr, None, std::ptr::null_mut()) };
160        self.cancel = None;
161    }
162
163    /// Identify the language of one clip of 16 kHz mono float32 PCM. Input
164    /// longer than [`LangIdInfo::max_audio_ms`] is scored on its first `max_audio_ms`.
165    pub fn run(&mut self, pcm: &[f32], options: &LangIdOptions) -> Result<LangIdResult> {
166        let n = clamp_len(pcm.len())?;
167        // The CStrings and the pointer array stay alive until the end of this
168        // call, past the native run.
169        let codes: Option<Vec<CString>> = match &options.allowed {
170            None => None,
171            Some(list) if list.is_empty() => {
172                // NULL would mean "every label", the opposite of an empty list.
173                return Err(Error::InvalidArgument(
174                    "allowed is empty; pass None for every label".into(),
175                ));
176            }
177            Some(list) => Some(
178                list.iter()
179                    .map(|c| CString::new(c.as_str()))
180                    .collect::<std::result::Result<_, _>>()?,
181            ),
182        };
183        let ptrs: Option<Vec<*const c_char>> = codes
184            .as_ref()
185            .map(|cs| cs.iter().map(|c| c.as_ptr()).collect());
186        let mut params: sys::transcribe_langid_params = unsafe { std::mem::zeroed() };
187        unsafe { sys::transcribe_langid_params_init(&mut params) };
188        if let Some(p) = &ptrs {
189            params.allowed = p.as_ptr();
190            params.n_allowed = i32::try_from(p.len())
191                .map_err(|_| Error::InvalidArgument("allowed list too long".into()))?;
192        }
193
194        // Results are copied out under the compute lock, like every other
195        // binding, so a concurrent run on the same model cannot interleave.
196        let ptr = self.ptr;
197        self.model.with_compute(
198            Some("a stream is active on this model; finish or drop it before langid run()"),
199            |_| -> Result<LangIdResult> {
200                check(
201                    unsafe { sys::transcribe_langid_run(ptr, pcm.as_ptr(), n, &params) },
202                    "langid run",
203                )?;
204                let mut res: sys::transcribe_langid_result = unsafe { std::mem::zeroed() };
205                unsafe { sys::transcribe_langid_result_init(&mut res) };
206                check(
207                    unsafe { sys::transcribe_langid_get_result(ptr, &mut res) },
208                    "langid result",
209                )?;
210                let candidates = (0..res.n_candidates)
211                    .map(|i| {
212                        let mut raw: sys::transcribe_langid_candidate =
213                            unsafe { std::mem::zeroed() };
214                        unsafe { sys::transcribe_langid_candidate_init(&mut raw) };
215                        let _ = unsafe { sys::transcribe_langid_get_candidate(ptr, i, &mut raw) };
216                        LangIdCandidate {
217                            index: raw.index,
218                            code: owned_str(raw.code),
219                            name: owned_str(raw.name),
220                            p: raw.p,
221                            logit: raw.logit,
222                        }
223                    })
224                    .collect();
225                Ok(LangIdResult {
226                    candidates,
227                    allowed_mass: res.allowed_mass,
228                })
229            },
230        )?
231    }
232
233    /// Model load time plus the last run's stage timings (`decode_ms` is 0).
234    pub fn timings(&self) -> Timings {
235        let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
236        unsafe { sys::transcribe_timings_init(&mut raw) };
237        let _ = unsafe { sys::transcribe_langid_get_timings(self.ptr, &mut raw) };
238        Timings::from_raw(&raw)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use std::sync::Mutex;
246
247    fn null_session(streaming: bool) -> LangIdSession {
248        // Null native handles (both frees are NULL no-ops): every check below
249        // fires before any native call.
250        LangIdSession {
251            ptr: std::ptr::null_mut(),
252            model: Arc::new(ModelInner {
253                ptr: std::ptr::null_mut(),
254                compute_lock: Mutex::new(streaming),
255            }),
256            cancel: None,
257        }
258    }
259
260    #[test]
261    fn run_is_busy_while_a_stream_holds_the_lease() {
262        let err = null_session(true)
263            .run(&[0.0; 160], &LangIdOptions::default())
264            .unwrap_err();
265        let Error::Busy(msg) = err else {
266            panic!("expected Busy, got {err:?}");
267        };
268        assert_eq!(
269            msg,
270            "a stream is active on this model; finish or drop it before langid run()"
271        );
272    }
273
274    #[test]
275    fn empty_allowed_is_rejected_not_all() {
276        let opts = LangIdOptions {
277            allowed: Some(vec![]),
278        };
279        let err = null_session(false).run(&[0.0; 160], &opts).unwrap_err();
280        assert!(matches!(err, Error::InvalidArgument(_)), "{err:?}");
281    }
282
283    #[test]
284    fn interior_nul_in_allowed_is_rejected() {
285        let opts = LangIdOptions {
286            allowed: Some(vec!["e\0n".into()]),
287        };
288        let err = null_session(false).run(&[0.0; 160], &opts).unwrap_err();
289        assert!(matches!(err, Error::Nul(_)), "{err:?}");
290    }
291}