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,
35 pub gpu_device: i32,
37}
38
39impl Default for ModelOptions {
40 fn default() -> Self {
41 ModelOptions {
42 backend: Backend::Auto,
43 gpu_device: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct Capabilities {
51 pub native_sample_rate: i32,
52 pub languages: Vec<String>,
54 pub max_timestamp_kind: TimestampKind,
56 pub supports_language_detect: bool,
57 pub supports_translate: bool,
58 pub supports_streaming: bool,
59 pub supports_spec_decode: bool,
60 pub max_audio_ms: i64,
62}
63
64pub(crate) struct ModelInner {
66 pub(crate) ptr: *mut sys::transcribe_model,
67 pub(crate) compute_lock: Mutex<bool>,
73}
74
75unsafe impl Send for ModelInner {}
79unsafe impl Sync for ModelInner {}
80
81impl Drop for ModelInner {
82 fn drop(&mut self) {
83 unsafe { sys::transcribe_model_free(self.ptr) };
85 }
86}
87
88#[derive(Clone)]
90pub struct Model {
91 pub(crate) inner: Arc<ModelInner>,
92}
93
94impl std::fmt::Debug for Model {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("Model")
97 .field("arch", &self.arch())
98 .field("variant", &self.variant())
99 .field("backend", &self.backend())
100 .finish()
101 }
102}
103
104impl Model {
105 pub fn load(path: impl AsRef<Path>) -> Result<Model> {
107 Model::load_with(path, &ModelOptions::default())
108 }
109
110 pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
112 version::ensure_compatible()?;
114
115 let path = path.as_ref();
116 let c_path = CString::new(path_bytes(path)?)?;
117
118 let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
119 unsafe { sys::transcribe_model_load_params_init(&mut params) };
120 params.backend = options.backend.to_raw();
121 params.gpu_device = options.gpu_device;
122
123 let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
124 let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
125 check_model_load(status, &format!("load {}", path.display()))?;
126 debug_assert!(!out.is_null());
127
128 Ok(Model {
129 inner: Arc::new(ModelInner {
130 ptr: out,
131 compute_lock: Mutex::new(false),
132 }),
133 })
134 }
135
136 pub fn session(&self) -> Result<Session> {
138 self.session_with(&SessionOptions::default())
139 }
140
141 pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
143 Session::new(self, options)
144 }
145
146 pub fn capabilities(&self) -> Capabilities {
148 let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
149 unsafe { sys::transcribe_capabilities_init(&mut caps) };
150 let _ = unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) };
153
154 let mut languages = Vec::new();
155 if !caps.languages.is_null() && caps.n_languages > 0 {
156 let slice =
157 unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
158 for &lang in slice {
159 languages.push(owned_str(lang));
160 }
161 }
162
163 Capabilities {
164 native_sample_rate: caps.native_sample_rate,
165 languages,
166 max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
167 supports_language_detect: caps.supports_language_detect,
168 supports_translate: caps.supports_translate,
169 supports_streaming: caps.supports_streaming,
170 supports_spec_decode: caps.supports_spec_decode,
171 max_audio_ms: caps.max_audio_ms,
172 }
173 }
174
175 pub fn supports(&self, feature: Feature) -> bool {
177 unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
178 }
179
180 pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
182 unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
183 }
184
185 pub fn arch(&self) -> String {
187 owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
188 }
189
190 pub fn variant(&self) -> String {
192 owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
193 }
194
195 pub fn backend(&self) -> String {
198 owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
199 }
200
201 pub fn device(&self) -> Result<Device> {
206 let mut raw: sys::transcribe_backend_device = unsafe { std::mem::zeroed() };
207 unsafe { sys::transcribe_backend_device_init(&mut raw) };
208 let status = unsafe { sys::transcribe_model_get_device(self.inner.ptr, &mut raw) };
209 check(status, "model_get_device")?;
210 Ok(Device::from_raw(&raw, None))
211 }
212
213 pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
217 let c_text = CString::new(text)?;
218 let mut buf = vec![0i32; text.len().max(16)];
220 loop {
221 let n = unsafe {
222 sys::transcribe_tokenize(
223 self.inner.ptr,
224 c_text.as_ptr(),
225 buf.as_mut_ptr(),
226 buf.len(),
227 )
228 };
229 if n == i32::MIN {
230 return Err(crate::error::Error::NotImplemented(
231 "model tokenizer has no encode path".to_string(),
232 ));
233 }
234 if n < 0 {
235 buf.resize((-n) as usize, 0);
236 continue;
237 }
238 buf.truncate(n as usize);
239 return Ok(buf);
240 }
241 }
242}
243
244fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
245 #[cfg(feature = "dynamic-backends")]
246 {
247 if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
248 && crate::backend::device_count() == 0
249 {
250 let code = status.0 as i32;
251 let base = crate::error::status_string(code);
252 return Err(crate::error::Error::Backend(format!(
253 "{context}: {base} (status {code}); dynamic-backends builds \
254 require backend modules to be loaded before model load. Call \
255 transcribe_cpp::init_backends_default() when modules are \
256 bundled next to libtranscribe, or init_backends(dir) for a \
257 custom provider directory."
258 )));
259 }
260 }
261 check(status, context)
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct SessionOptions {
267 pub n_threads: i32,
269 pub kv_type: crate::types::KvType,
271 pub n_ctx: i32,
273}
274
275impl Default for SessionOptions {
276 fn default() -> Self {
277 SessionOptions {
278 n_threads: 0,
279 kv_type: crate::types::KvType::Auto,
280 n_ctx: 0,
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub struct SessionLimits {
288 pub effective_n_ctx: i32,
290 pub effective_max_audio_ms: i64,
292 pub max_kv_bytes: i64,
294}
295
296#[cfg(unix)]
302pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
303 use std::os::unix::ffi::OsStrExt;
304 Ok(path.as_os_str().as_bytes().to_vec())
305}
306
307#[cfg(not(unix))]
308pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
309 path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
311 crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
312 })
313}