1use std::ffi::CString;
20use std::path::Path;
21use std::sync::{Arc, Mutex};
22
23use transcribe_cpp_sys as sys;
24
25use crate::backend::Device;
26use crate::diarize::{DiarizeInfo, DiarizeSession, DiarizeSessionOptions};
27use crate::error::{check, Error, Result};
28use crate::langid::{LangIdInfo, LangIdSession, LangIdSessionOptions};
29use crate::result::owned_str;
30use crate::session::Session;
31use crate::types::{Backend, ExtSlot, Feature, Roles, TimestampKind};
32use crate::vad::{VadInfo, VadSession, VadSessionOptions};
33use crate::version;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ModelOptions {
38 pub backend: Backend,
41 pub device: Option<crate::Device>,
44}
45
46impl Default for ModelOptions {
47 fn default() -> Self {
48 ModelOptions {
49 backend: Backend::Auto,
50 device: None,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct Capabilities {
59 pub native_sample_rate: i32,
60 pub languages: Vec<String>,
62 pub translate_target_languages: Vec<String>,
64 pub max_timestamp_kind: TimestampKind,
66 pub supports_language_detect: bool,
67 pub supports_translate: bool,
68 pub supports_streaming: bool,
69 pub supports_spec_decode: bool,
70 pub max_audio_ms: i64,
72}
73
74pub(crate) struct ModelInner {
76 pub(crate) ptr: *mut sys::transcribe_model,
77 pub(crate) compute_lock: Mutex<bool>,
84}
85
86impl ModelInner {
87 pub(crate) fn with_compute<R>(
91 &self,
92 refuse_if_streaming: Option<&str>,
93 f: impl FnOnce(&mut bool) -> R,
94 ) -> Result<R> {
95 let mut lease = self.compute_lock.lock().unwrap_or_else(|e| e.into_inner());
96 if let Some(msg) = refuse_if_streaming {
97 if *lease {
98 return Err(Error::Busy(msg.into()));
99 }
100 }
101 Ok(f(&mut lease))
102 }
103}
104
105unsafe impl Send for ModelInner {}
109unsafe impl Sync for ModelInner {}
110
111impl Drop for ModelInner {
112 fn drop(&mut self) {
113 unsafe { sys::transcribe_model_free(self.ptr) };
115 }
116}
117
118#[derive(Clone)]
120pub struct Model {
121 pub(crate) inner: Arc<ModelInner>,
122}
123
124impl std::fmt::Debug for Model {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("Model")
127 .field("arch", &self.arch())
128 .field("variant", &self.variant())
129 .field("backend", &self.backend())
130 .finish()
131 }
132}
133
134impl Model {
135 pub fn load(path: impl AsRef<Path>) -> Result<Model> {
139 Model::load_with(path, &ModelOptions::default())
140 }
141
142 pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
144 version::ensure_compatible()?;
146
147 let path = path.as_ref();
148 let c_path = CString::new(path_bytes(path)?)?;
149
150 let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
151 unsafe { sys::transcribe_model_load_params_init(&mut params) };
152 params.backend = options.backend.to_raw();
153 params.device = options
154 .device
155 .as_ref()
156 .map_or(std::ptr::null_mut(), |device| device.handle);
157
158 let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
159 let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
160 check_model_load(status, &format!("load {}", path.display()))?;
161 debug_assert!(!out.is_null());
162
163 Ok(Model {
164 inner: Arc::new(ModelInner {
165 ptr: out,
166 compute_lock: Mutex::new(false),
167 }),
168 })
169 }
170
171 pub fn session(&self) -> Result<Session> {
173 self.session_with(&SessionOptions::default())
174 }
175
176 pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
178 Session::new(self, options)
179 }
180
181 pub fn diarize_session(&self) -> Result<DiarizeSession> {
184 self.diarize_session_with(&DiarizeSessionOptions::default())
185 }
186
187 pub fn diarize_session_with(&self, options: &DiarizeSessionOptions) -> Result<DiarizeSession> {
189 DiarizeSession::new(self, options)
190 }
191
192 pub fn diarize_info(&self) -> Result<DiarizeInfo> {
195 let mut raw: sys::transcribe_diarize_info = unsafe { std::mem::zeroed() };
196 unsafe { sys::transcribe_diarize_info_init(&mut raw) };
197 check(
198 unsafe { sys::transcribe_diarize_get_info(self.inner.ptr, &mut raw) },
199 "diarize info",
200 )?;
201 Ok(DiarizeInfo {
202 sample_rate: raw.sample_rate,
203 max_speakers: raw.max_speakers,
204 })
205 }
206
207 pub fn langid_session(&self) -> Result<LangIdSession> {
210 self.langid_session_with(&LangIdSessionOptions::default())
211 }
212
213 pub fn langid_session_with(&self, options: &LangIdSessionOptions) -> Result<LangIdSession> {
215 LangIdSession::new(self, options)
216 }
217
218 pub fn langid_info(&self) -> Result<LangIdInfo> {
221 let mut raw: sys::transcribe_langid_info = unsafe { std::mem::zeroed() };
222 unsafe { sys::transcribe_langid_info_init(&mut raw) };
223 check(
224 unsafe { sys::transcribe_langid_get_info(self.inner.ptr, &mut raw) },
225 "langid info",
226 )?;
227 Ok(LangIdInfo {
228 sample_rate: raw.sample_rate,
229 n_labels: raw.n_labels,
230 min_audio_ms: raw.min_audio_ms,
231 max_audio_ms: raw.max_audio_ms,
232 })
233 }
234
235 pub fn langid_labels(&self) -> Result<Vec<(String, String)>> {
238 let n = self.langid_info()?.n_labels;
239 Ok((0..n)
240 .map(|i| unsafe {
241 (
242 owned_str(sys::transcribe_langid_label_code(self.inner.ptr, i)),
243 owned_str(sys::transcribe_langid_label_name(self.inner.ptr, i)),
244 )
245 })
246 .collect())
247 }
248
249 pub fn langid_label_index(&self, code: &str) -> Option<i32> {
252 let c = CString::new(code).ok()?;
253 let i = unsafe { sys::transcribe_langid_label_index(self.inner.ptr, c.as_ptr()) };
254 (i >= 0).then_some(i)
255 }
256
257 pub fn vad_session(&self) -> Result<VadSession> {
260 self.vad_session_with(&VadSessionOptions::default())
261 }
262
263 pub fn vad_session_with(&self, options: &VadSessionOptions) -> Result<VadSession> {
265 VadSession::new(self, options)
266 }
267
268 pub fn vad_info(&self) -> Result<VadInfo> {
271 let mut raw: sys::transcribe_vad_info = unsafe { std::mem::zeroed() };
272 unsafe { sys::transcribe_vad_info_init(&mut raw) };
273 check(
274 unsafe { sys::transcribe_vad_get_info(self.inner.ptr, &mut raw) },
275 "vad info",
276 )?;
277 Ok(VadInfo {
278 sample_rate: raw.sample_rate,
279 frame_samples: raw.frame_samples,
280 })
281 }
282
283 pub fn roles(&self) -> Roles {
285 Roles(unsafe { sys::transcribe_model_roles(self.inner.ptr) })
286 }
287
288 pub fn capabilities(&self) -> Result<Capabilities> {
291 let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
292 unsafe { sys::transcribe_capabilities_init(&mut caps) };
293 check(
294 unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) },
295 "capabilities",
296 )?;
297
298 let mut languages = Vec::new();
299 if !caps.languages.is_null() && caps.n_languages > 0 {
300 let slice =
301 unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
302 for &lang in slice {
303 languages.push(owned_str(lang));
304 }
305 }
306 let mut translate_target_languages = Vec::new();
307 if !caps.translate_target_languages.is_null() && caps.n_translate_target_languages > 0 {
308 let slice = unsafe {
309 std::slice::from_raw_parts(
310 caps.translate_target_languages,
311 caps.n_translate_target_languages as usize,
312 )
313 };
314 for &lang in slice {
315 translate_target_languages.push(owned_str(lang));
316 }
317 }
318
319 Ok(Capabilities {
320 native_sample_rate: caps.native_sample_rate,
321 languages,
322 translate_target_languages,
323 max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
324 supports_language_detect: caps.supports_language_detect,
325 supports_translate: caps.supports_translate,
326 supports_streaming: caps.supports_streaming,
327 supports_spec_decode: caps.supports_spec_decode,
328 max_audio_ms: caps.max_audio_ms,
329 })
330 }
331
332 pub fn supports(&self, feature: Feature) -> bool {
334 unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
335 }
336
337 pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
339 unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
340 }
341
342 pub fn arch(&self) -> String {
344 owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
345 }
346
347 pub fn variant(&self) -> String {
349 owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
350 }
351
352 pub fn backend(&self) -> String {
355 owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
356 }
357
358 pub fn device(&self) -> Result<Device> {
363 let handle = unsafe { sys::transcribe_model_device(self.inner.ptr) };
364 if handle.is_null() {
365 return Err(crate::Error::Backend(
366 "model has no resolved device".to_string(),
367 ));
368 }
369 let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
370 unsafe { sys::transcribe_device_info_init(&mut raw) };
371 let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
372 check(status, "device_get_info")?;
373 Ok(Device::from_raw(&raw, handle, None))
374 }
375
376 pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
380 let c_text = CString::new(text)?;
381 let mut buf = vec![0i32; text.len().max(16)];
383 loop {
384 let n = unsafe {
385 sys::transcribe_tokenize(
386 self.inner.ptr,
387 c_text.as_ptr(),
388 buf.as_mut_ptr(),
389 buf.len(),
390 )
391 };
392 if n == i32::MIN {
393 return Err(crate::error::Error::NotImplemented(
394 "model tokenizer has no encode path".to_string(),
395 ));
396 }
397 if n < 0 {
398 buf.resize((-n) as usize, 0);
399 continue;
400 }
401 buf.truncate(n as usize);
402 return Ok(buf);
403 }
404 }
405}
406
407fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
408 #[cfg(feature = "dynamic-backends")]
409 {
410 if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
411 && crate::backend::device_count() == 0
412 {
413 let code = status.0 as i32;
414 let base = crate::error::status_string(code);
415 return Err(crate::error::Error::Backend(format!(
416 "{context}: {base} (status {code}); dynamic-backends builds \
417 require backend modules to be loaded before model load. Call \
418 transcribe_cpp::init_backends_default() when modules are \
419 bundled next to libtranscribe, or init_backends(dir) for a \
420 custom provider directory."
421 )));
422 }
423 }
424 check(status, context)
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429#[cfg_attr(
430 feature = "serde",
431 derive(serde::Serialize, serde::Deserialize),
432 serde(default)
433)]
434pub struct SessionOptions {
435 pub n_threads: i32,
437 pub kv_type: crate::types::KvType,
439 pub n_ctx: i32,
441}
442
443impl Default for SessionOptions {
444 fn default() -> Self {
445 SessionOptions {
446 n_threads: 0,
447 kv_type: crate::types::KvType::Auto,
448 n_ctx: 0,
449 }
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
456pub struct SessionLimits {
457 pub effective_n_ctx: i32,
459 pub effective_max_audio_ms: i64,
461 pub max_kv_bytes: i64,
463}
464
465#[cfg(unix)]
471pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
472 use std::os::unix::ffi::OsStrExt;
473 Ok(path.as_os_str().as_bytes().to_vec())
474}
475
476#[cfg(not(unix))]
477pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
478 path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
480 crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
481 })
482}