use std::ffi::CString;
use std::path::Path;
use std::sync::{Arc, Mutex};
use transcribe_cpp_sys as sys;
use crate::backend::Device;
use crate::error::{check, Result};
use crate::result::owned_str;
use crate::session::Session;
use crate::types::{Backend, ExtSlot, Feature, TimestampKind};
use crate::version;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelOptions {
pub backend: Backend,
pub device: Option<crate::Device>,
}
impl Default for ModelOptions {
fn default() -> Self {
ModelOptions {
backend: Backend::Auto,
device: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Capabilities {
pub native_sample_rate: i32,
pub languages: Vec<String>,
pub translate_target_languages: Vec<String>,
pub max_timestamp_kind: TimestampKind,
pub supports_language_detect: bool,
pub supports_translate: bool,
pub supports_streaming: bool,
pub supports_spec_decode: bool,
pub max_audio_ms: i64,
}
pub(crate) struct ModelInner {
pub(crate) ptr: *mut sys::transcribe_model,
pub(crate) compute_lock: Mutex<bool>,
}
unsafe impl Send for ModelInner {}
unsafe impl Sync for ModelInner {}
impl Drop for ModelInner {
fn drop(&mut self) {
unsafe { sys::transcribe_model_free(self.ptr) };
}
}
#[derive(Clone)]
pub struct Model {
pub(crate) inner: Arc<ModelInner>,
}
impl std::fmt::Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Model")
.field("arch", &self.arch())
.field("variant", &self.variant())
.field("backend", &self.backend())
.finish()
}
}
impl Model {
pub fn load(path: impl AsRef<Path>) -> Result<Model> {
Model::load_with(path, &ModelOptions::default())
}
pub fn load_with(path: impl AsRef<Path>, options: &ModelOptions) -> Result<Model> {
version::ensure_compatible()?;
let path = path.as_ref();
let c_path = CString::new(path_bytes(path)?)?;
let mut params: sys::transcribe_model_load_params = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_model_load_params_init(&mut params) };
params.backend = options.backend.to_raw();
params.device = options
.device
.as_ref()
.map_or(std::ptr::null_mut(), |device| device.handle);
let mut out: *mut sys::transcribe_model = std::ptr::null_mut();
let status = unsafe { sys::transcribe_model_load_file(c_path.as_ptr(), ¶ms, &mut out) };
check_model_load(status, &format!("load {}", path.display()))?;
debug_assert!(!out.is_null());
Ok(Model {
inner: Arc::new(ModelInner {
ptr: out,
compute_lock: Mutex::new(false),
}),
})
}
pub fn session(&self) -> Result<Session> {
self.session_with(&SessionOptions::default())
}
pub fn session_with(&self, options: &SessionOptions) -> Result<Session> {
Session::new(self, options)
}
pub fn capabilities(&self) -> Capabilities {
let mut caps: sys::transcribe_capabilities = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_capabilities_init(&mut caps) };
let _ = unsafe { sys::transcribe_model_get_capabilities(self.inner.ptr, &mut caps) };
let mut languages = Vec::new();
if !caps.languages.is_null() && caps.n_languages > 0 {
let slice =
unsafe { std::slice::from_raw_parts(caps.languages, caps.n_languages as usize) };
for &lang in slice {
languages.push(owned_str(lang));
}
}
let mut translate_target_languages = Vec::new();
if !caps.translate_target_languages.is_null() && caps.n_translate_target_languages > 0 {
let slice = unsafe {
std::slice::from_raw_parts(
caps.translate_target_languages,
caps.n_translate_target_languages as usize,
)
};
for &lang in slice {
translate_target_languages.push(owned_str(lang));
}
}
Capabilities {
native_sample_rate: caps.native_sample_rate,
languages,
translate_target_languages,
max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
supports_language_detect: caps.supports_language_detect,
supports_translate: caps.supports_translate,
supports_streaming: caps.supports_streaming,
supports_spec_decode: caps.supports_spec_decode,
max_audio_ms: caps.max_audio_ms,
}
}
pub fn supports(&self, feature: Feature) -> bool {
unsafe { sys::transcribe_model_supports(self.inner.ptr, feature.to_raw()) }
}
pub fn accepts_ext(&self, slot: ExtSlot, kind: u32) -> bool {
unsafe { sys::transcribe_model_accepts_ext_kind(self.inner.ptr, slot.to_raw(), kind) }
}
pub fn arch(&self) -> String {
owned_str(unsafe { sys::transcribe_model_arch_string(self.inner.ptr) })
}
pub fn variant(&self) -> String {
owned_str(unsafe { sys::transcribe_model_variant_string(self.inner.ptr) })
}
pub fn backend(&self) -> String {
owned_str(unsafe { sys::transcribe_model_backend(self.inner.ptr) })
}
pub fn device(&self) -> Result<Device> {
let handle = unsafe { sys::transcribe_model_device(self.inner.ptr) };
if handle.is_null() {
return Err(crate::Error::Backend(
"model has no resolved device".to_string(),
));
}
let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
unsafe { sys::transcribe_device_info_init(&mut raw) };
let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
check(status, "device_get_info")?;
Ok(Device::from_raw(&raw, handle, None))
}
pub fn tokenize(&self, text: &str) -> Result<Vec<i32>> {
let c_text = CString::new(text)?;
let mut buf = vec![0i32; text.len().max(16)];
loop {
let n = unsafe {
sys::transcribe_tokenize(
self.inner.ptr,
c_text.as_ptr(),
buf.as_mut_ptr(),
buf.len(),
)
};
if n == i32::MIN {
return Err(crate::error::Error::NotImplemented(
"model tokenizer has no encode path".to_string(),
));
}
if n < 0 {
buf.resize((-n) as usize, 0);
continue;
}
buf.truncate(n as usize);
return Ok(buf);
}
}
}
fn check_model_load(status: sys::transcribe_status, context: &str) -> Result<()> {
#[cfg(feature = "dynamic-backends")]
{
if status == sys::transcribe_status::TRANSCRIBE_ERR_BACKEND
&& crate::backend::device_count() == 0
{
let code = status.0 as i32;
let base = crate::error::status_string(code);
return Err(crate::error::Error::Backend(format!(
"{context}: {base} (status {code}); dynamic-backends builds \
require backend modules to be loaded before model load. Call \
transcribe_cpp::init_backends_default() when modules are \
bundled next to libtranscribe, or init_backends(dir) for a \
custom provider directory."
)));
}
}
check(status, context)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionOptions {
pub n_threads: i32,
pub kv_type: crate::types::KvType,
pub n_ctx: i32,
}
impl Default for SessionOptions {
fn default() -> Self {
SessionOptions {
n_threads: 0,
kv_type: crate::types::KvType::Auto,
n_ctx: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionLimits {
pub effective_n_ctx: i32,
pub effective_max_audio_ms: i64,
pub max_kv_bytes: i64,
}
#[cfg(unix)]
pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
use std::os::unix::ffi::OsStrExt;
Ok(path.as_os_str().as_bytes().to_vec())
}
#[cfg(not(unix))]
pub(crate) fn path_bytes(path: &Path) -> Result<Vec<u8>> {
path.to_str().map(|s| s.as_bytes().to_vec()).ok_or_else(|| {
crate::error::Error::InvalidArgument(format!("non-UTF-8 path: {}", path.display()))
})
}