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 pub no_mmap: bool,
51}
52
53impl Default for ModelOptions {
54 fn default() -> Self {
55 ModelOptions {
56 backend: Backend::Auto,
57 device: None,
58 no_mmap: false,
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct Capabilities {
67 pub native_sample_rate: i32,
68 pub languages: Vec<String>,
70 pub translate_target_languages: Vec<String>,
72 pub max_timestamp_kind: TimestampKind,
74 pub supports_language_detect: bool,
75 pub supports_translate: bool,
76 pub supports_streaming: bool,
77 pub supports_spec_decode: bool,
78 pub max_audio_ms: i64,
80}
81
82pub(crate) struct ModelInner {
84 pub(crate) ptr: *mut sys::transcribe_model,
85 pub(crate) compute_lock: Mutex<bool>,
92}
93
94impl ModelInner {
95 pub(crate) fn with_compute<R>(
99 &self,
100 refuse_if_streaming: Option<&str>,
101 f: impl FnOnce(&mut bool) -> R,
102 ) -> Result<R> {
103 let mut lease = self.compute_lock.lock().unwrap_or_else(|e| e.into_inner());
104 if let Some(msg) = refuse_if_streaming {
105 if *lease {
106 return Err(Error::Busy(msg.into()));
107 }
108 }
109 Ok(f(&mut lease))
110 }
111}
112
113unsafe impl Send for ModelInner {}
117unsafe impl Sync for ModelInner {}
118
119impl Drop for ModelInner {
120 fn drop(&mut self) {
121 unsafe { sys::transcribe_model_free(self.ptr) };
123 }
124}
125
126#[derive(Clone)]
128pub struct Model {
129 pub(crate) inner: Arc<ModelInner>,
130}
131
132impl std::fmt::Debug for Model {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct("Model")
135 .field("arch", &self.arch())
136 .field("variant", &self.variant())
137 .field("backend", &self.backend())
138 .finish()
139 }
140}
141
142impl Model {
143 pub fn load(path: impl AsRef<Path>) -> Result<Model> {
147 Model::load_with(path, &ModelOptions::default())
148 }
149
150 pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
152 version::ensure_compatible()?;
154
155 let path = path.as_ref();
156 let c_path = CString::new(path_bytes(path)?)?;
157
158 let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
159 unsafe { sys::transcribe_model_load_params_init(&mut params) };
160 params.backend = options.backend.to_raw();
161 params.device = options
162 .device
163 .as_ref()
164 .map_or(std::ptr::null_mut(), |device| device.handle);
165 params.no_mmap = options.no_mmap;
166
167 let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
168 let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
169 check_model_load(status, &format!("load {}", path.display()))?;
170 debug_assert!(!out.is_null());
171
172 Ok(Model {
173 inner: Arc::new(ModelInner {
174 ptr: out,
175 compute_lock: Mutex::new(false),
176 }),
177 })
178 }
179
180 pub fn session(&self) -> Result<Session> {
182 self.session_with(&SessionOptions::default())
183 }
184
185 pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
187 Session::new(self, options)
188 }
189
190 pub fn diarize_session(&self) -> Result<DiarizeSession> {
193 self.diarize_session_with(&DiarizeSessionOptions::default())
194 }
195
196 pub fn diarize_session_with(&self, options: &DiarizeSessionOptions) -> Result<DiarizeSession> {
198 DiarizeSession::new(self, options)
199 }
200
201 pub fn diarize_info(&self) -> Result<DiarizeInfo> {
204 let mut raw: sys::transcribe_diarize_info = unsafe { std::mem::zeroed() };
205 unsafe { sys::transcribe_diarize_info_init(&mut raw) };
206 check(
207 unsafe { sys::transcribe_diarize_get_info(self.inner.ptr, &mut raw) },
208 "diarize info",
209 )?;
210 Ok(DiarizeInfo {
211 sample_rate: raw.sample_rate,
212 max_speakers: raw.max_speakers,
213 })
214 }
215
216 pub fn langid_session(&self) -> Result<LangIdSession> {
219 self.langid_session_with(&LangIdSessionOptions::default())
220 }
221
222 pub fn langid_session_with(&self, options: &LangIdSessionOptions) -> Result<LangIdSession> {
224 LangIdSession::new(self, options)
225 }
226
227 pub fn langid_info(&self) -> Result<LangIdInfo> {
230 let mut raw: sys::transcribe_langid_info = unsafe { std::mem::zeroed() };
231 unsafe { sys::transcribe_langid_info_init(&mut raw) };
232 check(
233 unsafe { sys::transcribe_langid_get_info(self.inner.ptr, &mut raw) },
234 "langid info",
235 )?;
236 Ok(LangIdInfo {
237 sample_rate: raw.sample_rate,
238 n_labels: raw.n_labels,
239 min_audio_ms: raw.min_audio_ms,
240 max_audio_ms: raw.max_audio_ms,
241 })
242 }
243
244 pub fn langid_labels(&self) -> Result<Vec<(String, String)>> {
247 let n = self.langid_info()?.n_labels;
248 Ok((0..n)
249 .map(|i| unsafe {
250 (
251 owned_str(sys::transcribe_langid_label_code(self.inner.ptr, i)),
252 owned_str(sys::transcribe_langid_label_name(self.inner.ptr, i)),
253 )
254 })
255 .collect())
256 }
257
258 pub fn langid_label_index(&self, code: &str) -> Option<i32> {
261 let c = CString::new(code).ok()?;
262 let i = unsafe { sys::transcribe_langid_label_index(self.inner.ptr, c.as_ptr()) };
263 (i >= 0).then_some(i)
264 }
265
266 pub fn vad_session(&self) -> Result<VadSession> {
269 self.vad_session_with(&VadSessionOptions::default())
270 }
271
272 pub fn vad_session_with(&self, options: &VadSessionOptions) -> Result<VadSession> {
274 VadSession::new(self, options)
275 }
276
277 pub fn vad_info(&self) -> Result<VadInfo> {
280 let mut raw: sys::transcribe_vad_info = unsafe { std::mem::zeroed() };
281 unsafe { sys::transcribe_vad_info_init(&mut raw) };
282 check(
283 unsafe { sys::transcribe_vad_get_info(self.inner.ptr, &mut raw) },
284 "vad info",
285 )?;
286 Ok(VadInfo {
287 sample_rate: raw.sample_rate,
288 frame_samples: raw.frame_samples,
289 })
290 }
291
292 pub fn roles(&self) -> Roles {
294 Roles(unsafe { sys::transcribe_model_roles(self.inner.ptr) })
295 }
296
297 pub fn capabilities(&self) -> Result<Capabilities> {
300 let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
301 unsafe { sys::transcribe_capabilities_init(&mut caps) };
302 check(
303 unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) },
304 "capabilities",
305 )?;
306
307 let mut languages = Vec::new();
308 if !caps.languages.is_null() && caps.n_languages > 0 {
309 let slice =
310 unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
311 for &lang in slice {
312 languages.push(owned_str(lang));
313 }
314 }
315 let mut translate_target_languages = Vec::new();
316 if !caps.translate_target_languages.is_null() && caps.n_translate_target_languages > 0 {
317 let slice = unsafe {
318 std::slice::from_raw_parts(
319 caps.translate_target_languages,
320 caps.n_translate_target_languages as usize,
321 )
322 };
323 for &lang in slice {
324 translate_target_languages.push(owned_str(lang));
325 }
326 }
327
328 Ok(Capabilities {
329 native_sample_rate: caps.native_sample_rate,
330 languages,
331 translate_target_languages,
332 max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
333 supports_language_detect: caps.supports_language_detect,
334 supports_translate: caps.supports_translate,
335 supports_streaming: caps.supports_streaming,
336 supports_spec_decode: caps.supports_spec_decode,
337 max_audio_ms: caps.max_audio_ms,
338 })
339 }
340
341 pub fn supports(&self, feature: Feature) -> bool {
343 unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
344 }
345
346 pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
348 unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
349 }
350
351 pub fn arch(&self) -> String {
353 owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
354 }
355
356 pub fn variant(&self) -> String {
358 owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
359 }
360
361 pub fn backend(&self) -> String {
364 owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
365 }
366
367 pub fn device(&self) -> Result<Device> {
372 let handle = unsafe { sys::transcribe_model_device(self.inner.ptr) };
373 if handle.is_null() {
374 return Err(crate::Error::Backend(
375 "model has no resolved device".to_string(),
376 ));
377 }
378 let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
379 unsafe { sys::transcribe_device_info_init(&mut raw) };
380 let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
381 check(status, "device_get_info")?;
382 Ok(Device::from_raw(&raw, handle, None))
383 }
384
385 pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
389 let c_text = CString::new(text)?;
390 let mut buf = vec![0i32; text.len().max(16)];
392 loop {
393 let n = unsafe {
394 sys::transcribe_tokenize(
395 self.inner.ptr,
396 c_text.as_ptr(),
397 buf.as_mut_ptr(),
398 buf.len(),
399 )
400 };
401 if n == i32::MIN {
402 return Err(crate::error::Error::NotImplemented(
403 "model tokenizer has no encode path".to_string(),
404 ));
405 }
406 if n < 0 {
407 buf.resize((-n) as usize, 0);
408 continue;
409 }
410 buf.truncate(n as usize);
411 return Ok(buf);
412 }
413 }
414}
415
416fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
417 #[cfg(feature = "dynamic-backends")]
418 {
419 if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
420 && crate::backend::device_count() == 0
421 {
422 let code = status.0 as i32;
423 let base = crate::error::status_string(code);
424 return Err(crate::error::Error::Backend(format!(
425 "{context}: {base} (status {code}); dynamic-backends builds \
426 require backend modules to be loaded before model load. Call \
427 transcribe_cpp::init_backends_default() when modules are \
428 bundled next to libtranscribe, or init_backends(dir) for a \
429 custom provider directory."
430 )));
431 }
432 }
433 check(status, context)
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
438#[cfg_attr(
439 feature = "serde",
440 derive(serde::Serialize, serde::Deserialize),
441 serde(default)
442)]
443pub struct SessionOptions {
444 pub n_threads: i32,
446 pub kv_type: crate::types::KvType,
448 pub n_ctx: i32,
450}
451
452impl Default for SessionOptions {
453 fn default() -> Self {
454 SessionOptions {
455 n_threads: 0,
456 kv_type: crate::types::KvType::Auto,
457 n_ctx: 0,
458 }
459 }
460}
461
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
465pub struct SessionLimits {
466 pub effective_n_ctx: i32,
468 pub effective_max_audio_ms: i64,
470 pub max_kv_bytes: i64,
472}
473
474#[cfg(unix)]
480pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
481 use std::os::unix::ffi::OsStrExt;
482 Ok(path.as_os_str().as_bytes().to_vec())
483}
484
485#[cfg(not(unix))]
486pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
487 path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
489 crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
490 })
491}