1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct LangIdInfo {
23 pub sample_rate: i32,
25 pub n_labels: i32,
27 pub min_audio_ms: i32,
29 pub max_audio_ms: i32,
31}
32
33#[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 pub n_threads: i32,
43}
44
45#[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 pub allowed: Option<Vec<String>>,
57}
58
59#[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 pub index: i32,
70 pub code: String,
71 pub name: String,
72 pub p: f32,
74 pub logit: f32,
75}
76
77#[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 pub candidates: Vec<LangIdCandidate>,
89 pub allowed_mass: f32,
92}
93
94impl LangIdResult {
95 pub fn code(&self) -> Option<&str> {
97 self.candidates.first().map(|c| c.code.as_str())
98 }
99}
100
101pub struct LangIdSession {
103 ptr: *mut sys::transcribe_langid_session,
104 model: Arc<ModelInner>,
106 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
116unsafe 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, ¶ms, &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 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 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 pub fn run(&mut self, pcm: &[f32], options: &LangIdOptions) -> Result<LangIdResult> {
166 let n = clamp_len(pcm.len())?;
167 let codes: Option<Vec<CString>> = match &options.allowed {
170 None => None,
171 Some(list) if list.is_empty() => {
172 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 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, ¶ms) },
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 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 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}