1use std::ffi::CString;
18use std::path::Path;
19use std::sync::{Arc, Mutex};
20
21use transcribe_cpp_sys as sys;
22
23use crate::backend::Device;
24use crate::error::{check, Result};
25use crate::result::owned_str;
26use crate::session::Session;
27use crate::types::{Backend, ExtSlot, Feature, TimestampKind};
28use crate::version;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ModelOptions {
33 pub backend: Backend,
36 pub device: Option<crate::Device>,
39}
40
41impl Default for ModelOptions {
42 fn default() -> Self {
43 ModelOptions {
44 backend: Backend::Auto,
45 device: None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct Capabilities {
54 pub native_sample_rate: i32,
55 pub languages: Vec<String>,
57 pub translate_target_languages: Vec<String>,
59 pub max_timestamp_kind: TimestampKind,
61 pub supports_language_detect: bool,
62 pub supports_translate: bool,
63 pub supports_streaming: bool,
64 pub supports_spec_decode: bool,
65 pub max_audio_ms: i64,
67}
68
69pub(crate) struct ModelInner {
71 pub(crate) ptr: *mut sys::transcribe_model,
72 pub(crate) compute_lock: Mutex<bool>,
78}
79
80unsafe impl Send for ModelInner {}
84unsafe impl Sync for ModelInner {}
85
86impl Drop for ModelInner {
87 fn drop(&mut self) {
88 unsafe { sys::transcribe_model_free(self.ptr) };
90 }
91}
92
93#[derive(Clone)]
95pub struct Model {
96 pub(crate) inner: Arc<ModelInner>,
97}
98
99impl std::fmt::Debug for Model {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("Model")
102 .field("arch", &self.arch())
103 .field("variant", &self.variant())
104 .field("backend", &self.backend())
105 .finish()
106 }
107}
108
109impl Model {
110 pub fn load(path: impl AsRef<Path>) -> Result<Model> {
112 Model::load_with(path, &ModelOptions::default())
113 }
114
115 pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
117 version::ensure_compatible()?;
119
120 let path = path.as_ref();
121 let c_path = CString::new(path_bytes(path)?)?;
122
123 let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
124 unsafe { sys::transcribe_model_load_params_init(&mut params) };
125 params.backend = options.backend.to_raw();
126 params.device = options
127 .device
128 .as_ref()
129 .map_or(std::ptr::null_mut(), |device| device.handle);
130
131 let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
132 let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
133 check_model_load(status, &format!("load {}", path.display()))?;
134 debug_assert!(!out.is_null());
135
136 Ok(Model {
137 inner: Arc::new(ModelInner {
138 ptr: out,
139 compute_lock: Mutex::new(false),
140 }),
141 })
142 }
143
144 pub fn session(&self) -> Result<Session> {
146 self.session_with(&SessionOptions::default())
147 }
148
149 pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
151 Session::new(self, options)
152 }
153
154 pub fn capabilities(&self) -> Capabilities {
156 let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
157 unsafe { sys::transcribe_capabilities_init(&mut caps) };
158 let _ = unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) };
161
162 let mut languages = Vec::new();
163 if !caps.languages.is_null() && caps.n_languages > 0 {
164 let slice =
165 unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
166 for &lang in slice {
167 languages.push(owned_str(lang));
168 }
169 }
170 let mut translate_target_languages = Vec::new();
171 if !caps.translate_target_languages.is_null() && caps.n_translate_target_languages > 0 {
172 let slice = unsafe {
173 std::slice::from_raw_parts(
174 caps.translate_target_languages,
175 caps.n_translate_target_languages as usize,
176 )
177 };
178 for &lang in slice {
179 translate_target_languages.push(owned_str(lang));
180 }
181 }
182
183 Capabilities {
184 native_sample_rate: caps.native_sample_rate,
185 languages,
186 translate_target_languages,
187 max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
188 supports_language_detect: caps.supports_language_detect,
189 supports_translate: caps.supports_translate,
190 supports_streaming: caps.supports_streaming,
191 supports_spec_decode: caps.supports_spec_decode,
192 max_audio_ms: caps.max_audio_ms,
193 }
194 }
195
196 pub fn supports(&self, feature: Feature) -> bool {
198 unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
199 }
200
201 pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
203 unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
204 }
205
206 pub fn arch(&self) -> String {
208 owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
209 }
210
211 pub fn variant(&self) -> String {
213 owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
214 }
215
216 pub fn backend(&self) -> String {
219 owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
220 }
221
222 pub fn device(&self) -> Result<Device> {
227 let handle = unsafe { sys::transcribe_model_device(self.inner.ptr) };
228 if handle.is_null() {
229 return Err(crate::Error::Backend(
230 "model has no resolved device".to_string(),
231 ));
232 }
233 let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
234 unsafe { sys::transcribe_device_info_init(&mut raw) };
235 let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
236 check(status, "device_get_info")?;
237 Ok(Device::from_raw(&raw, handle, None))
238 }
239
240 pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
244 let c_text = CString::new(text)?;
245 let mut buf = vec![0i32; text.len().max(16)];
247 loop {
248 let n = unsafe {
249 sys::transcribe_tokenize(
250 self.inner.ptr,
251 c_text.as_ptr(),
252 buf.as_mut_ptr(),
253 buf.len(),
254 )
255 };
256 if n == i32::MIN {
257 return Err(crate::error::Error::NotImplemented(
258 "model tokenizer has no encode path".to_string(),
259 ));
260 }
261 if n < 0 {
262 buf.resize((-n) as usize, 0);
263 continue;
264 }
265 buf.truncate(n as usize);
266 return Ok(buf);
267 }
268 }
269}
270
271fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
272 #[cfg(feature = "dynamic-backends")]
273 {
274 if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
275 && crate::backend::device_count() == 0
276 {
277 let code = status.0 as i32;
278 let base = crate::error::status_string(code);
279 return Err(crate::error::Error::Backend(format!(
280 "{context}: {base} (status {code}); dynamic-backends builds \
281 require backend modules to be loaded before model load. Call \
282 transcribe_cpp::init_backends_default() when modules are \
283 bundled next to libtranscribe, or init_backends(dir) for a \
284 custom provider directory."
285 )));
286 }
287 }
288 check(status, context)
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293#[cfg_attr(
294 feature = "serde",
295 derive(serde::Serialize, serde::Deserialize),
296 serde(default)
297)]
298pub struct SessionOptions {
299 pub n_threads: i32,
301 pub kv_type: crate::types::KvType,
303 pub n_ctx: i32,
305}
306
307impl Default for SessionOptions {
308 fn default() -> Self {
309 SessionOptions {
310 n_threads: 0,
311 kv_type: crate::types::KvType::Auto,
312 n_ctx: 0,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
320pub struct SessionLimits {
321 pub effective_n_ctx: i32,
323 pub effective_max_audio_ms: i64,
325 pub max_kv_bytes: i64,
327}
328
329#[cfg(unix)]
335pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
336 use std::os::unix::ffi::OsStrExt;
337 Ok(path.as_os_str().as_bytes().to_vec())
338}
339
340#[cfg(not(unix))]
341pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
342 path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
344 crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
345 })
346}