1use std::ffi::CString;
18use std::path::Path;
19use std::sync::{Arc, Mutex};
20
21use transcribe_cpp_sys as sys;
22
23use crate::error::{check, Result};
24use crate::result::owned_str;
25use crate::session::Session;
26use crate::types::{Backend, ExtSlot, Feature, TimestampKind};
27use crate::version;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ModelOptions {
32 pub backend: Backend,
34 pub gpu_device: i32,
36}
37
38impl Default for ModelOptions {
39 fn default() -> Self {
40 ModelOptions {
41 backend: Backend::Auto,
42 gpu_device: 0,
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub struct Capabilities {
50 pub native_sample_rate: i32,
51 pub languages: Vec<String>,
53 pub max_timestamp_kind: TimestampKind,
55 pub supports_language_detect: bool,
56 pub supports_translate: bool,
57 pub supports_streaming: bool,
58 pub supports_spec_decode: bool,
59 pub max_audio_ms: i64,
61}
62
63pub(crate) struct ModelInner {
65 pub(crate) ptr: *mut sys::transcribe_model,
66 pub(crate) compute_lock: Mutex<bool>,
72}
73
74unsafe impl Send for ModelInner {}
78unsafe impl Sync for ModelInner {}
79
80impl Drop for ModelInner {
81 fn drop(&mut self) {
82 unsafe { sys::transcribe_model_free(self.ptr) };
84 }
85}
86
87#[derive(Clone)]
89pub struct Model {
90 pub(crate) inner: Arc<ModelInner>,
91}
92
93impl std::fmt::Debug for Model {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("Model")
96 .field("arch", &self.arch())
97 .field("variant", &self.variant())
98 .field("backend", &self.backend())
99 .finish()
100 }
101}
102
103impl Model {
104 pub fn load(path: impl AsRef<Path>) -> Result<Model> {
106 Model::load_with(path, &ModelOptions::default())
107 }
108
109 pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
111 version::ensure_compatible()?;
113
114 let path = path.as_ref();
115 let c_path = CString::new(path_bytes(path)?)?;
116
117 let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
118 unsafe { sys::transcribe_model_load_params_init(&mut params) };
119 params.backend = options.backend.to_raw();
120 params.gpu_device = options.gpu_device;
121
122 let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
123 let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
124 check_model_load(status, &format!("load {}", path.display()))?;
125 debug_assert!(!out.is_null());
126
127 Ok(Model {
128 inner: Arc::new(ModelInner {
129 ptr: out,
130 compute_lock: Mutex::new(false),
131 }),
132 })
133 }
134
135 pub fn session(&self) -> Result<Session> {
137 self.session_with(&SessionOptions::default())
138 }
139
140 pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
142 Session::new(self, options)
143 }
144
145 pub fn capabilities(&self) -> Capabilities {
147 let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
148 unsafe { sys::transcribe_capabilities_init(&mut caps) };
149 let _ = unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) };
152
153 let mut languages = Vec::new();
154 if !caps.languages.is_null() && caps.n_languages > 0 {
155 let slice =
156 unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
157 for &lang in slice {
158 languages.push(owned_str(lang));
159 }
160 }
161
162 Capabilities {
163 native_sample_rate: caps.native_sample_rate,
164 languages,
165 max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
166 supports_language_detect: caps.supports_language_detect,
167 supports_translate: caps.supports_translate,
168 supports_streaming: caps.supports_streaming,
169 supports_spec_decode: caps.supports_spec_decode,
170 max_audio_ms: caps.max_audio_ms,
171 }
172 }
173
174 pub fn supports(&self, feature: Feature) -> bool {
176 unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
177 }
178
179 pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
181 unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
182 }
183
184 pub fn arch(&self) -> String {
186 owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
187 }
188
189 pub fn variant(&self) -> String {
191 owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
192 }
193
194 pub fn backend(&self) -> String {
197 owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
198 }
199
200 pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
204 let c_text = CString::new(text)?;
205 let mut buf = vec![0i32; text.len().max(16)];
207 loop {
208 let n = unsafe {
209 sys::transcribe_tokenize(
210 self.inner.ptr,
211 c_text.as_ptr(),
212 buf.as_mut_ptr(),
213 buf.len(),
214 )
215 };
216 if n == i32::MIN {
217 return Err(crate::error::Error::NotImplemented(
218 "model tokenizer has no encode path".to_string(),
219 ));
220 }
221 if n < 0 {
222 buf.resize((-n) as usize, 0);
223 continue;
224 }
225 buf.truncate(n as usize);
226 return Ok(buf);
227 }
228 }
229}
230
231fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
232 #[cfg(feature = "dynamic-backends")]
233 {
234 if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
235 && crate::backend::device_count() == 0
236 {
237 let code = status.0 as i32;
238 let base = crate::error::status_string(code);
239 return Err(crate::error::Error::Backend(format!(
240 "{context}: {base} (status {code}); dynamic-backends builds \
241 require backend modules to be loaded before model load. Call \
242 transcribe_cpp::init_backends_default() when modules are \
243 bundled next to libtranscribe, or init_backends(dir) for a \
244 custom provider directory."
245 )));
246 }
247 }
248 check(status, context)
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct SessionOptions {
254 pub n_threads: i32,
256 pub kv_type: crate::types::KvType,
258 pub n_ctx: i32,
260}
261
262impl Default for SessionOptions {
263 fn default() -> Self {
264 SessionOptions {
265 n_threads: 0,
266 kv_type: crate::types::KvType::Auto,
267 n_ctx: 0,
268 }
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct SessionLimits {
275 pub effective_n_ctx: i32,
277 pub effective_max_audio_ms: i64,
279 pub max_kv_bytes: i64,
281}
282
283#[cfg(unix)]
289pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
290 use std::os::unix::ffi::OsStrExt;
291 Ok(path.as_os_str().as_bytes().to_vec())
292}
293
294#[cfg(not(unix))]
295pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
296 path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
298 crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
299 })
300}