use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_float, c_int};
#[derive(Debug, Clone)]
pub struct Segment {
pub text: String,
pub start: f64, pub end: f64, pub no_speech_prob: f32,
}
#[derive(Debug, Clone, Default)]
pub struct TranscribeOptions {
pub strategy: Option<i32>,
pub vad: bool,
pub vad_model_path: Option<String>,
pub vad_threshold: Option<f32>,
pub vad_min_speech_ms: Option<i32>,
pub vad_min_silence_ms: Option<i32>,
pub tdrz: bool,
}
#[deprecated(
since = "0.1.6",
note = "Use Session::open() instead — CrispASR can abort on C++ exceptions"
)]
pub struct CrispASR {
ctx: *mut crispasr_sys::WhisperContext,
}
unsafe impl Send for CrispASR {}
impl CrispASR {
pub fn new(model_path: &str) -> Result<Self, String> {
let path = CString::new(model_path).map_err(|e| format!("invalid path: {e}"))?;
let cparams = unsafe { crispasr_sys::whisper_context_default_params_by_ref() };
let ctx =
unsafe { crispasr_sys::whisper_init_from_file_with_params(path.as_ptr(), cparams) };
unsafe { crispasr_sys::whisper_free_context_params(cparams) };
if ctx.is_null() {
return Err(format!("failed to load model: {model_path}"));
}
Ok(Self { ctx })
}
pub fn transcribe_pcm(&self, pcm: &[f32]) -> Result<Vec<Segment>, String> {
self.transcribe_pcm_with_strategy(pcm, crispasr_sys::CRISPASR_SAMPLING_GREEDY)
}
pub fn transcribe_pcm_with_strategy(
&self,
pcm: &[f32],
strategy: i32,
) -> Result<Vec<Segment>, String> {
self.transcribe_pcm_with_options(
pcm,
&TranscribeOptions {
strategy: Some(strategy),
..Default::default()
},
)
}
pub fn transcribe_pcm_with_options(
&self,
pcm: &[f32],
opts: &TranscribeOptions,
) -> Result<Vec<Segment>, String> {
let strategy = opts
.strategy
.unwrap_or(crispasr_sys::CRISPASR_SAMPLING_GREEDY);
let params = unsafe { crispasr_sys::whisper_full_default_params_by_ref(strategy) };
if opts.vad {
unsafe {
crispasr_sys::crispasr_params_set_vad(params, 1);
if let Some(t) = opts.vad_threshold {
crispasr_sys::crispasr_params_set_vad_threshold(params, t);
}
if let Some(ms) = opts.vad_min_speech_ms {
crispasr_sys::crispasr_params_set_vad_min_speech_ms(params, ms);
}
if let Some(ms) = opts.vad_min_silence_ms {
crispasr_sys::crispasr_params_set_vad_min_silence_ms(params, ms);
}
}
let vad_path_cstr = opts
.vad_model_path
.as_ref()
.map(|s| CString::new(s.as_str()).ok())
.flatten();
if let Some(cs) = &vad_path_cstr {
unsafe {
crispasr_sys::crispasr_params_set_vad_model_path(params, cs.as_ptr());
}
}
return self.run_full(pcm, params, vad_path_cstr);
}
if opts.tdrz {
unsafe { crispasr_sys::crispasr_params_set_tdrz(params, 1) };
}
self.run_full(pcm, params, None)
}
fn run_full(
&self,
pcm: &[f32],
params: *mut crispasr_sys::WhisperFullParams,
_keep_alive_vad_path: Option<CString>,
) -> Result<Vec<Segment>, String> {
let ret =
unsafe { crispasr_sys::whisper_full(self.ctx, params, pcm.as_ptr(), pcm.len() as i32) };
unsafe { crispasr_sys::whisper_free_params(params) };
if ret != 0 {
return Err(format!("transcription failed (error code {ret})"));
}
let n = unsafe { crispasr_sys::whisper_full_n_segments(self.ctx) };
let mut segments = Vec::with_capacity(n as usize);
for i in 0..n {
let text_ptr = unsafe { crispasr_sys::whisper_full_get_segment_text(self.ctx, i) };
let text = if text_ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(text_ptr) }
.to_string_lossy()
.into_owned()
};
let t0 = unsafe { crispasr_sys::whisper_full_get_segment_t0(self.ctx, i) };
let t1 = unsafe { crispasr_sys::whisper_full_get_segment_t1(self.ctx, i) };
let nsp = unsafe { crispasr_sys::whisper_full_get_segment_no_speech_prob(self.ctx, i) };
segments.push(Segment {
text,
start: t0 as f64 / 100.0,
end: t1 as f64 / 100.0,
no_speech_prob: nsp,
});
}
Ok(segments)
}
pub fn detected_language(&self) -> String {
let id = unsafe { crispasr_sys::whisper_full_lang_id(self.ctx) };
let ptr = unsafe { crispasr_sys::whisper_lang_str(id) };
if ptr.is_null() {
"unknown".to_string()
} else {
unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
}
}
impl Drop for CrispASR {
fn drop(&mut self) {
unsafe { crispasr_sys::whisper_free(self.ctx) }
}
}
#[derive(Debug, Clone)]
pub struct SessionWord {
pub text: String,
pub start: f64,
pub end: f64,
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct SessionSegment {
pub text: String,
pub start: f64,
pub end: f64,
pub words: Vec<SessionWord>,
pub no_speech_prob: f32,
}
#[derive(Debug, Clone)]
pub struct CtcLogits {
pub n_vocab: usize,
pub n_frames: usize,
pub data: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct Stem {
pub name: String,
pub pcm: Vec<f32>,
}
pub struct Session {
handle: *mut crispasr_sys::CrispasrSession,
n_threads: c_int,
}
unsafe impl Send for Session {}
impl Session {
pub fn open(model_path: &str) -> Result<Self, String> {
Self::open_inner(model_path, None, 4)
}
pub fn open_with_backend(
model_path: &str,
backend: &str,
n_threads: i32,
) -> Result<Self, String> {
Self::open_inner(model_path, Some(backend), n_threads)
}
fn open_inner(model_path: &str, backend: Option<&str>, n_threads: i32) -> Result<Self, String> {
let path = CString::new(model_path).map_err(|e| format!("invalid path: {e}"))?;
let handle = if let Some(be) = backend {
let be_c = CString::new(be).map_err(|e| format!("invalid backend: {e}"))?;
unsafe {
crispasr_sys::crispasr_session_open_explicit(
path.as_ptr(),
be_c.as_ptr(),
n_threads,
)
}
} else {
unsafe { crispasr_sys::crispasr_session_open(path.as_ptr(), n_threads) }
};
if handle.is_null() {
let avail = Self::available_backends().join(",");
return Err(format!(
"Failed to open {model_path:?}. Library was built with: [{avail}]"
));
}
Ok(Self { handle, n_threads })
}
pub fn available_backends() -> Vec<String> {
let mut buf = vec![0i8; 256];
let mut n = unsafe {
crispasr_sys::crispasr_session_available_backends(buf.as_mut_ptr(), buf.len() as i32)
};
if n <= 0 {
return Vec::new();
}
if n as usize >= buf.len() {
buf.resize(n as usize + 1, 0);
n = unsafe {
crispasr_sys::crispasr_session_available_backends(
buf.as_mut_ptr(),
buf.len() as i32,
)
};
if n <= 0 {
return Vec::new();
}
}
let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) };
cstr.to_string_lossy()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_string())
.collect()
}
pub fn backend_caps(backend: &str) -> Result<Vec<String>, String> {
let name = CString::new(backend).map_err(|e| format!("invalid backend: {e}"))?;
let mut buf = [0i8; 1024];
let n = unsafe {
crispasr_sys::crispasr_backend_caps_abi(name.as_ptr(), buf.as_mut_ptr(), buf.len() as i32)
};
if n == -3 {
return Err(format!("unknown backend '{backend}'"));
}
if n < 0 {
return Err(format!("backend_caps failed (code {n})"));
}
let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy().into_owned();
Ok(s.split(',').filter(|x| !x.is_empty()).map(|x| x.to_string()).collect())
}
pub fn list_backends_with_caps() -> Result<Vec<(String, Vec<String>)>, String> {
let need = unsafe { crispasr_sys::crispasr_backend_caps_list_abi(std::ptr::null_mut(), 0) };
let cap = if need < 0 { (-need) as usize } else { 65536 };
let mut buf = vec![0i8; cap.max(1024)];
let n = unsafe {
crispasr_sys::crispasr_backend_caps_list_abi(buf.as_mut_ptr(), buf.len() as i32)
};
if n < 0 {
return Err(format!("list_backends_with_caps failed (code {n})"));
}
let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_string_lossy().into_owned();
Ok(s.lines()
.filter(|l| !l.is_empty())
.map(|l| {
let mut it = l.splitn(2, '\t');
let name = it.next().unwrap_or("").to_string();
let caps = it
.next()
.unwrap_or("")
.split(',')
.filter(|x| !x.is_empty())
.map(|x| x.to_string())
.collect();
(name, caps)
})
.collect())
}
pub fn detect_backends(model_path: &str) -> Result<Vec<String>, String> {
let path = CString::new(model_path).map_err(|e| format!("invalid path: {e}"))?;
let mut buf = [0i8; 512];
let n = unsafe {
crispasr_sys::crispasr_detect_backends_from_gguf(
path.as_ptr(),
buf.as_mut_ptr(),
buf.len() as i32,
)
};
if n <= 0 {
return Err(format!("backend detection failed (code {n})"));
}
let s = unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned();
Ok(s.lines().filter(|l| !l.is_empty()).map(|l| l.to_string()).collect())
}
pub fn detect_backend(model_path: &str) -> Result<String, String> {
let path = CString::new(model_path).map_err(|e| format!("invalid path: {e}"))?;
let mut buf = [0i8; 64];
let n = unsafe {
crispasr_sys::crispasr_detect_backend_from_gguf(
path.as_ptr(),
buf.as_mut_ptr(),
buf.len() as i32,
)
};
if n <= 0 {
return Err(format!("backend detection failed (code {n})"));
}
Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned())
}
pub fn backend(&self) -> String {
let p = unsafe { crispasr_sys::crispasr_session_backend(self.handle) };
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
pub fn ctc_vocab(&self) -> Option<Vec<String>> {
let n = unsafe { crispasr_sys::crispasr_session_n_vocab(self.handle) };
if n <= 0 {
return None;
}
let mut out = Vec::with_capacity(n as usize);
for id in 0..n {
let p = unsafe { crispasr_sys::crispasr_session_token_text(self.handle, id) };
let piece = if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
};
out.push(piece);
}
Some(out)
}
pub fn detected_language(&self) -> String {
let mut buf = [0 as c_char; 32];
let n = unsafe {
crispasr_sys::crispasr_session_detected_language(
self.handle,
buf.as_mut_ptr(),
buf.len() as c_int,
)
};
if n <= 0 {
return "unknown".to_string();
}
unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned()
}
pub fn transcribe(&self, pcm: &[f32]) -> Result<Vec<SessionSegment>, String> {
self.transcribe_with_language(pcm, None)
}
pub fn transcribe_with_language(
&self,
pcm: &[f32],
language: Option<&str>,
) -> Result<Vec<SessionSegment>, String> {
if pcm.is_empty() {
return Ok(Vec::new());
}
let lang_c = match language {
Some(l) if !l.is_empty() => {
Some(CString::new(l).map_err(|e| format!("language NUL: {e}"))?)
}
_ => None,
};
let res = unsafe {
match &lang_c {
Some(c) => crispasr_sys::crispasr_session_transcribe_lang(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
c.as_ptr(),
),
None => crispasr_sys::crispasr_session_transcribe(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
),
}
};
self.parse_session_result(res, "crispasr_session_transcribe")
}
pub fn set_return_logits(&self, on: bool) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_return_logits(self.handle, if on { 1 } else { 0 })
};
if rc != 0 {
return Err(format!("set_return_logits failed (rc={rc})"));
}
Ok(())
}
pub fn transcribe_with_logits(
&self,
pcm: &[f32],
) -> Result<(Vec<SessionSegment>, Option<CtcLogits>), String> {
if pcm.is_empty() {
return Ok((Vec::new(), None));
}
self.set_return_logits(true)?;
let res = unsafe {
crispasr_sys::crispasr_session_transcribe(self.handle, pcm.as_ptr(), pcm.len() as i32)
};
let parsed = self.parse_session_result_logits(res, "crispasr_session_transcribe");
let _ = self.set_return_logits(false);
parsed
}
pub fn transcribe_chunked(
&self,
pcm: &[f32],
chunk_seconds: i32,
overlap_seconds: i32,
) -> Result<Vec<SessionSegment>, String> {
self.transcribe_chunked_with_language(pcm, chunk_seconds, overlap_seconds, None)
}
pub fn transcribe_chunked_with_language(
&self,
pcm: &[f32],
chunk_seconds: i32,
overlap_seconds: i32,
language: Option<&str>,
) -> Result<Vec<SessionSegment>, String> {
if pcm.is_empty() {
return Ok(Vec::new());
}
let lang_c = match language {
Some(l) if !l.is_empty() => {
Some(CString::new(l).map_err(|e| format!("language NUL: {e}"))?)
}
_ => None,
};
let res = unsafe {
crispasr_sys::crispasr_session_transcribe_chunked_lang(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
chunk_seconds,
overlap_seconds,
lang_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
)
};
self.parse_session_result(res, "crispasr_session_transcribe_chunked")
}
pub fn transcribe_chunked_with_progress<F: FnMut(i32, i32)>(
&self,
pcm: &[f32],
chunk_seconds: i32,
overlap_seconds: i32,
language: Option<&str>,
mut progress: F,
) -> Result<Vec<SessionSegment>, String> {
if pcm.is_empty() {
return Ok(Vec::new());
}
let lang_c = match language {
Some(l) if !l.is_empty() => {
Some(CString::new(l).map_err(|e| format!("language NUL: {e}"))?)
}
_ => None,
};
extern "C" fn trampoline<F: FnMut(i32, i32)>(
processed: c_int,
total: c_int,
ud: *mut c_void,
) {
if ud.is_null() {
return;
}
let f = unsafe { &mut *(ud as *mut F) };
f(processed, total);
}
unsafe {
crispasr_sys::crispasr_session_set_progress_callback(
self.handle,
Some(trampoline::<F>),
&mut progress as *mut F as *mut c_void,
);
}
let res = unsafe {
crispasr_sys::crispasr_session_transcribe_chunked_lang(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
chunk_seconds,
overlap_seconds,
lang_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
)
};
unsafe {
crispasr_sys::crispasr_session_set_progress_callback(
self.handle,
None,
std::ptr::null_mut(),
);
}
self.parse_session_result(res, "crispasr_session_transcribe_chunked")
}
fn parse_session_result(
&self,
res: *mut crispasr_sys::CrispasrSessionResult,
ctx: &str,
) -> Result<Vec<SessionSegment>, String> {
self.parse_session_result_logits(res, ctx)
.map(|(segs, _)| segs)
}
fn parse_session_result_logits(
&self,
res: *mut crispasr_sys::CrispasrSessionResult,
ctx: &str,
) -> Result<(Vec<SessionSegment>, Option<CtcLogits>), String> {
if res.is_null() {
return Err(format!("{ctx} failed for backend {:?}", self.backend()));
}
let mut out = Vec::new();
unsafe {
let n = crispasr_sys::crispasr_session_result_n_segments(res);
for i in 0..n {
let tp = crispasr_sys::crispasr_session_result_segment_text(res, i);
let text = if tp.is_null() {
String::new()
} else {
CStr::from_ptr(tp).to_string_lossy().into_owned()
};
let t0 = crispasr_sys::crispasr_session_result_segment_t0(res, i) as f64 / 100.0;
let t1 = crispasr_sys::crispasr_session_result_segment_t1(res, i) as f64 / 100.0;
let wn = crispasr_sys::crispasr_session_result_n_words(res, i);
let mut words = Vec::with_capacity(wn as usize);
for j in 0..wn {
let wtp = crispasr_sys::crispasr_session_result_word_text(res, i, j);
let wt = if wtp.is_null() {
String::new()
} else {
CStr::from_ptr(wtp).to_string_lossy().into_owned()
};
let raw_p = crispasr_sys::crispasr_session_result_word_p(res, i, j);
words.push(SessionWord {
text: wt,
start: crispasr_sys::crispasr_session_result_word_t0(res, i, j) as f64
/ 100.0,
end: crispasr_sys::crispasr_session_result_word_t1(res, i, j) as f64
/ 100.0,
confidence: if raw_p < 0.0 { 1.0 } else { raw_p },
});
}
let nsp = crispasr_sys::crispasr_session_result_segment_no_speech_prob(res, i);
out.push(SessionSegment {
text: text.trim().to_string(),
start: t0,
end: t1,
words,
no_speech_prob: nsp,
});
}
let n_frames = crispasr_sys::crispasr_session_result_n_logit_frames(res);
let n_vocab = crispasr_sys::crispasr_session_result_n_logit_vocab(res);
let lp = crispasr_sys::crispasr_session_result_logits(res);
let logits = if n_frames > 0 && n_vocab > 0 && !lp.is_null() {
let n = n_vocab as usize * n_frames as usize;
Some(CtcLogits {
n_vocab: n_vocab as usize,
n_frames: n_frames as usize,
data: std::slice::from_raw_parts(lp, n).to_vec(),
})
} else {
None
};
crispasr_sys::crispasr_session_result_free(res);
Ok((out, logits))
}
}
pub fn transcribe_vad(
&self,
pcm: &[f32],
vad_model_path: &str,
opts: Option<VadOptions>,
) -> Result<Vec<SessionSegment>, String> {
self.transcribe_vad_with_language(pcm, vad_model_path, opts, None)
}
pub fn transcribe_vad_with_language(
&self,
pcm: &[f32],
vad_model_path: &str,
opts: Option<VadOptions>,
language: Option<&str>,
) -> Result<Vec<SessionSegment>, String> {
if pcm.is_empty() {
return Ok(Vec::new());
}
let path_c = CString::new(vad_model_path)
.map_err(|e| format!("vad_model_path contains NUL byte: {e}"))?;
let abi_opts = opts.unwrap_or_default().to_abi();
let lang_c = match language {
Some(l) if !l.is_empty() => {
Some(CString::new(l).map_err(|e| format!("language NUL: {e}"))?)
}
_ => None,
};
let res = unsafe {
match &lang_c {
Some(c) => crispasr_sys::crispasr_session_transcribe_vad_lang(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
16_000,
path_c.as_ptr(),
&abi_opts,
c.as_ptr(),
),
None => crispasr_sys::crispasr_session_transcribe_vad(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
16_000,
path_c.as_ptr(),
&abi_opts,
),
}
};
if res.is_null() {
return Err(format!(
"crispasr_session_transcribe_vad failed for backend {:?}",
self.backend()
));
}
let mut out = Vec::new();
unsafe {
let n = crispasr_sys::crispasr_session_result_n_segments(res);
for i in 0..n {
let tp = crispasr_sys::crispasr_session_result_segment_text(res, i);
let text = if tp.is_null() {
String::new()
} else {
CStr::from_ptr(tp).to_string_lossy().into_owned()
};
let t0 = crispasr_sys::crispasr_session_result_segment_t0(res, i) as f64 / 100.0;
let t1 = crispasr_sys::crispasr_session_result_segment_t1(res, i) as f64 / 100.0;
let wn = crispasr_sys::crispasr_session_result_n_words(res, i);
let mut words = Vec::with_capacity(wn as usize);
for j in 0..wn {
let wtp = crispasr_sys::crispasr_session_result_word_text(res, i, j);
let wt = if wtp.is_null() {
String::new()
} else {
CStr::from_ptr(wtp).to_string_lossy().into_owned()
};
let raw_p = crispasr_sys::crispasr_session_result_word_p(res, i, j);
words.push(SessionWord {
text: wt,
start: crispasr_sys::crispasr_session_result_word_t0(res, i, j) as f64
/ 100.0,
end: crispasr_sys::crispasr_session_result_word_t1(res, i, j) as f64
/ 100.0,
confidence: if raw_p < 0.0 { 1.0 } else { raw_p },
});
}
let nsp = crispasr_sys::crispasr_session_result_segment_no_speech_prob(res, i);
out.push(SessionSegment {
text: text.trim().to_string(),
start: t0,
end: t1,
words,
no_speech_prob: nsp,
});
}
crispasr_sys::crispasr_session_result_free(res);
}
Ok(out)
}
pub fn set_codec_path(&self, path: &str) -> Result<(), String> {
let cpath = CString::new(path).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_codec_path(self.handle, cpath.as_ptr()) };
if rc != 0 {
return Err(format!("set_codec_path failed (rc={})", rc));
}
Ok(())
}
pub fn set_voice(&self, path: &str, ref_text: Option<&str>) -> Result<(), String> {
let cpath = CString::new(path).map_err(|e| e.to_string())?;
let crt = match ref_text {
Some(t) => Some(CString::new(t).map_err(|e| e.to_string())?),
None => None,
};
let rt_ptr = crt.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
let rc = unsafe {
crispasr_sys::crispasr_session_set_voice(self.handle, cpath.as_ptr(), rt_ptr)
};
if rc != 0 {
return Err(format!("set_voice failed (rc={})", rc));
}
Ok(())
}
pub fn set_voice_samples(
&self,
pcm: &[f32],
sample_rate: i32,
ref_text: Option<&str>,
) -> Result<(), String> {
if pcm.is_empty() {
return Err("set_voice_samples: empty sample buffer".to_string());
}
if sample_rate <= 0 {
return Err(format!("set_voice_samples: invalid sample_rate {sample_rate}"));
}
let crt = match ref_text {
Some(t) => Some(CString::new(t).map_err(|e| e.to_string())?),
None => None,
};
let rt_ptr = crt.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
let rc = unsafe {
crispasr_sys::crispasr_session_set_voice_samples(
self.handle,
pcm.as_ptr(),
pcm.len() as i32,
sample_rate,
rt_ptr,
)
};
if rc != 0 {
return Err(format!("set_voice_samples failed (rc={rc})"));
}
Ok(())
}
pub fn set_speaker_name(&self, name: &str) -> Result<(), String> {
let cname = CString::new(name).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_speaker_name(self.handle, cname.as_ptr()) };
match rc {
0 => Ok(()),
-2 => Err(format!(
"unknown speaker {:?}; call .speakers() to enumerate",
name
)),
-3 => Err("backend has no preset speakers; use set_voice() instead".to_string()),
_ => Err(format!("set_speaker_name failed (rc={})", rc)),
}
}
pub fn speakers(&self) -> Vec<String> {
let n = unsafe { crispasr_sys::crispasr_session_n_speakers(self.handle) };
let mut out = Vec::with_capacity(n.max(0) as usize);
for i in 0..n {
let ptr = unsafe { crispasr_sys::crispasr_session_get_speaker_name(self.handle, i) };
if !ptr.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
out.push(s);
}
}
out
}
pub fn synthesize(&self, text: &str) -> Result<Vec<f32>, String> {
let ctext = CString::new(text).map_err(|e| e.to_string())?;
let mut n: c_int = 0;
let ptr = unsafe {
crispasr_sys::crispasr_session_synthesize(
self.handle,
ctext.as_ptr(),
&mut n as *mut c_int,
)
};
if ptr.is_null() || n <= 0 {
return Err(format!(
"synthesize returned no audio for backend {:?}",
self.backend()
));
}
let out = unsafe { std::slice::from_raw_parts(ptr, n as usize).to_vec() };
unsafe { crispasr_sys::crispasr_pcm_free(ptr) };
Ok(out)
}
pub fn speech_to_speech(&self, pcm: &[f32]) -> Result<(Vec<f32>, Option<String>), String> {
let mut n: c_int = 0;
let mut text_ptr: *mut c_char = std::ptr::null_mut();
let ptr = unsafe {
crispasr_sys::crispasr_session_speech_to_speech(
self.handle,
pcm.as_ptr(),
pcm.len() as c_int,
&mut text_ptr as *mut *mut c_char,
&mut n as *mut c_int,
)
};
if ptr.is_null() || n <= 0 {
if !text_ptr.is_null() {
unsafe { crispasr_sys::crispasr_session_translate_text_free(text_ptr) };
}
return Err(format!(
"speech_to_speech returned no audio for backend {:?} (S2S may be unsupported). \
Separation models (htdemucs, mel-band-roformer) are not S2S — use \
Session::separate() instead (#359).",
self.backend()
));
}
let out = unsafe { std::slice::from_raw_parts(ptr, n as usize).to_vec() };
unsafe { crispasr_sys::crispasr_pcm_free(ptr) };
let transcript = if text_ptr.is_null() {
None
} else {
let s = unsafe { CStr::from_ptr(text_ptr) }
.to_string_lossy()
.into_owned();
unsafe { crispasr_sys::crispasr_session_translate_text_free(text_ptr) };
Some(s)
};
Ok((out, transcript))
}
pub fn separate(&self, pcm_stereo: &[f32]) -> Result<Vec<Stem>, String> {
let n_frames = (pcm_stereo.len() / 2) as c_int;
if n_frames == 0 {
return Err("separate needs interleaved stereo PCM".to_string());
}
let n_stems = unsafe {
crispasr_sys::crispasr_session_separate(self.handle, pcm_stereo.as_ptr(), n_frames)
};
if n_stems <= 0 {
return Err(format!(
"separate returned no stems for backend {:?} (is it a separation model?)",
self.backend()
));
}
let mut stems = Vec::with_capacity(n_stems as usize);
for i in 0..n_stems {
let name_ptr =
unsafe { crispasr_sys::crispasr_session_separate_stem_name(self.handle, i) };
let name = if name_ptr.is_null() {
format!("stem{}", i)
} else {
unsafe { CStr::from_ptr(name_ptr) }
.to_string_lossy()
.into_owned()
};
let mut n_out: c_int = 0;
let ptr = unsafe {
crispasr_sys::crispasr_session_separate_stem(
self.handle,
i,
&mut n_out as *mut c_int,
)
};
if ptr.is_null() || n_out <= 0 {
return Err(format!("stem {} ({}) came back empty", i, name));
}
let pcm =
unsafe { std::slice::from_raw_parts(ptr, (n_out as usize) * 2).to_vec() };
stems.push(Stem { name, pcm });
}
Ok(stems)
}
pub fn separate_sample_rate(&self) -> i32 {
unsafe { crispasr_sys::crispasr_session_separate_sample_rate(self.handle) as i32 }
}
pub fn input_sample_rate(&self) -> i32 {
unsafe { crispasr_sys::crispasr_session_input_sample_rate(self.handle) as i32 }
}
pub fn output_sample_rate(&self) -> i32 {
unsafe { crispasr_sys::crispasr_session_output_sample_rate(self.handle) as i32 }
}
pub fn input_channels(&self) -> i32 {
unsafe { crispasr_sys::crispasr_session_input_channels(self.handle) as i32 }
}
pub fn output_channels(&self) -> i32 {
unsafe { crispasr_sys::crispasr_session_output_channels(self.handle) as i32 }
}
pub fn accept_marking_responsibility(&self, attestation: &str) -> Result<(), String> {
let c = CString::new(attestation).map_err(|e| e.to_string())?;
unsafe {
crispasr_sys::crispasr_session_accept_marking_responsibility(self.handle, c.as_ptr())
};
Ok(())
}
pub fn set_speaker_identity(&self, identity: &str) -> Result<(), String> {
let c = CString::new(identity).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_speaker_identity(self.handle, c.as_ptr()) };
match rc {
0 => Ok(()),
-2 => Err(format!(
"unrecognised speaker_identity {identity:?} (expected real_person, synthetic or unknown)"
)),
_ => Err(format!("set_speaker_identity failed (rc={rc})")),
}
}
pub fn watermark_embed(pcm: &mut [f32]) {
if pcm.is_empty() {
return;
}
unsafe {
crispasr_sys::crispasr_watermark_embed(pcm.as_mut_ptr(), pcm.len() as c_int, -1.0)
};
}
pub fn watermark_detect(pcm: &[f32]) -> f32 {
if pcm.is_empty() {
return 0.0;
}
unsafe { crispasr_sys::crispasr_watermark_detect(pcm.as_ptr(), pcm.len() as c_int) }
}
pub fn synthesize_raw(&self, text: &str) -> Result<Vec<f32>, String> {
let ctext = CString::new(text).map_err(|e| e.to_string())?;
let mut n: c_int = 0;
let ptr = unsafe {
crispasr_sys::crispasr_session_synthesize_raw(
self.handle,
ctext.as_ptr(),
&mut n as *mut c_int,
)
};
if ptr.is_null() || n <= 0 {
return Err(format!(
"synthesize_raw returned no audio for backend {:?} (call accept_marking_responsibility first?)",
self.backend()
));
}
let out = unsafe { std::slice::from_raw_parts(ptr, n as usize).to_vec() };
unsafe { crispasr_sys::crispasr_pcm_free(ptr) };
Ok(out)
}
pub fn clear_phoneme_cache(&self) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_kokoro_clear_phoneme_cache(self.handle) };
if rc != 0 {
return Err(format!("clear_phoneme_cache failed (rc={})", rc));
}
Ok(())
}
pub fn set_source_language(&self, lang: &str) -> Result<(), String> {
let c = CString::new(lang).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_source_language(self.handle, c.as_ptr()) };
if rc != 0 {
return Err(format!("set_source_language failed (rc={})", rc));
}
Ok(())
}
pub fn set_target_language(&self, lang: &str) -> Result<(), String> {
let c = CString::new(lang).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_target_language(self.handle, c.as_ptr()) };
if rc != 0 {
return Err(format!("set_target_language failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_reference_language(&self, lang: &str) -> Result<(), String> {
let c = CString::new(lang).map_err(|e| e.to_string())?;
let rc = unsafe {
crispasr_sys::crispasr_session_set_tts_reference_language(self.handle, c.as_ptr())
};
if rc != 0 {
return Err(format!("set_tts_reference_language failed (rc={})", rc));
}
Ok(())
}
pub fn set_punctuation(&self, enable: bool) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_punctuation(self.handle, if enable { 1 } else { 0 })
};
if rc != 0 {
return Err(format!("set_punctuation failed (rc={})", rc));
}
Ok(())
}
pub fn set_translate(&self, enable: bool) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_translate(self.handle, if enable { 1 } else { 0 })
};
if rc != 0 {
return Err(format!("set_translate failed (rc={})", rc));
}
Ok(())
}
pub fn translate_text(
&self,
text: &str,
src_lang: &str,
tgt_lang: &str,
max_tokens: i32,
) -> Result<String, String> {
let ctext = CString::new(text).map_err(|e| format!("text contains NUL: {e}"))?;
let csrc = CString::new(src_lang).map_err(|e| format!("src_lang contains NUL: {e}"))?;
let ctgt = CString::new(tgt_lang).map_err(|e| format!("tgt_lang contains NUL: {e}"))?;
let ptr = unsafe {
crispasr_sys::crispasr_session_translate_text(
self.handle,
ctext.as_ptr(),
csrc.as_ptr(),
ctgt.as_ptr(),
max_tokens,
)
};
if ptr.is_null() {
return Err(format!(
"translate_text returned no output (backend {:?} may not be MT-capable, \
or the pair {}→{} is unsupported)",
self.backend(),
src_lang,
tgt_lang
));
}
let out = unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned();
unsafe { crispasr_sys::crispasr_session_translate_text_free(ptr) };
Ok(out)
}
pub fn stream_open(
&self,
step_ms: i32,
length_ms: i32,
keep_ms: i32,
language: &str,
translate: bool,
) -> Result<Stream, String> {
self.stream_open_ex(step_ms, length_ms, keep_ms, language, translate, false)
}
pub fn stream_open_ex(
&self,
step_ms: i32,
length_ms: i32,
keep_ms: i32,
language: &str,
translate: bool,
live: bool,
) -> Result<Stream, String> {
let lang_c = CString::new(language).map_err(|e| e.to_string())?;
let h = unsafe {
crispasr_sys::crispasr_session_stream_open(
self.handle,
self.n_threads,
step_ms,
length_ms,
keep_ms,
lang_c.as_ptr(),
if translate { 1 } else { 0 },
)
};
if h.is_null() {
return Err(format!(
"stream_open failed for backend {:?}",
self.backend()
));
}
if live {
unsafe { crispasr_sys::crispasr_stream_set_live_decode(h, 1) };
}
Ok(Stream { handle: h })
}
pub fn set_temperature(&self, temperature: f32, seed: u64) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_temperature(self.handle, temperature, seed)
};
if rc != 0 && rc != -2 {
return Err(format!("set_temperature failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_seed(&self, seed: u64) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_tts_seed(self.handle, seed) };
if rc != 0 && rc != -2 {
return Err(format!("set_tts_seed failed (rc={})", rc));
}
Ok(())
}
pub fn set_max_new_tokens(&self, max_new_tokens: i32) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_max_new_tokens(self.handle, max_new_tokens)
};
if rc != 0 {
return Err(format!("set_max_new_tokens failed (rc={})", rc));
}
Ok(())
}
pub fn set_frequency_penalty(&self, penalty: f32) -> Result<(), String> {
let rc =
unsafe { crispasr_sys::crispasr_session_set_frequency_penalty(self.handle, penalty) };
if rc != 0 {
return Err(format!("set_frequency_penalty failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_steps(&self, steps: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_tts_steps(self.handle, steps) };
if rc != 0 && rc != -2 {
return Err(format!("set_tts_steps failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_num_candidates(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_tts_num_candidates(self.handle, n) };
if rc != 0 && rc != -2 {
return Err(format!("set_tts_num_candidates failed (rc={})", rc));
}
Ok(())
}
pub fn set_top_p(&self, top_p: f32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_top_p(self.handle, top_p) };
if rc != 0 && rc != -2 {
return Err(format!("set_top_p failed (rc={})", rc));
}
Ok(())
}
pub fn set_top_k(&self, top_k: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_top_k(self.handle, top_k) };
if rc != 0 && rc != -2 {
return Err(format!("set_top_k failed (rc={})", rc));
}
Ok(())
}
pub fn set_do_sample(&self, enable: bool) -> Result<(), String> {
let rc =
unsafe { crispasr_sys::crispasr_session_set_do_sample(self.handle, enable as i32) };
if rc != 0 && rc != -2 {
return Err(format!("set_do_sample failed (rc={})", rc));
}
Ok(())
}
pub fn set_min_p(&self, min_p: f32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_min_p(self.handle, min_p) };
if rc != 0 && rc != -2 {
return Err(format!("set_min_p failed (rc={})", rc));
}
Ok(())
}
pub fn set_repetition_penalty(&self, r: f32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_repetition_penalty(self.handle, r) };
if rc != 0 && rc != -2 {
return Err(format!("set_repetition_penalty failed (rc={})", rc));
}
Ok(())
}
pub fn set_cfg_weight(&self, cfg_weight: f32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_cfg_weight(self.handle, cfg_weight) };
if rc != 0 && rc != -2 {
return Err(format!("set_cfg_weight failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_noise_temp(&self, noise_temp: f32) -> Result<(), String> {
let rc =
unsafe { crispasr_sys::crispasr_session_set_tts_noise_temp(self.handle, noise_temp) };
if rc != 0 && rc != -2 {
return Err(format!("set_tts_noise_temp failed (rc={})", rc));
}
Ok(())
}
pub fn set_exaggeration(&self, exaggeration: f32) -> Result<(), String> {
let rc =
unsafe { crispasr_sys::crispasr_session_set_exaggeration(self.handle, exaggeration) };
if rc != 0 && rc != -2 {
return Err(format!("set_exaggeration failed (rc={})", rc));
}
Ok(())
}
pub fn set_max_speech_tokens(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_max_speech_tokens(self.handle, n) };
if rc != 0 && rc != -2 {
return Err(format!("set_max_speech_tokens failed (rc={})", rc));
}
Ok(())
}
pub fn set_min_speech_tokens(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_min_speech_tokens(self.handle, n) };
if rc != 0 && rc != -2 {
return Err(format!("set_min_speech_tokens failed (rc={})", rc));
}
Ok(())
}
pub fn set_length_scale(&self, scale: f32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_length_scale(self.handle, scale) };
if rc != 0 && rc != -2 {
return Err(format!("set_length_scale failed (rc={})", rc));
}
Ok(())
}
pub fn set_best_of(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_best_of(self.handle, n) };
if rc != 0 {
return Err(format!("set_best_of failed (rc={})", rc));
}
Ok(())
}
pub fn set_beam_size(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_beam_size(self.handle, n) };
if rc != 0 {
return Err(format!("set_beam_size failed (rc={})", rc));
}
Ok(())
}
pub fn set_grammar_text(
&self,
gbnf_text: &str,
root_rule: &str,
penalty: f32,
) -> Result<(), String> {
let cgbnf = CString::new(gbnf_text).map_err(|e| e.to_string())?;
let croot = CString::new(root_rule).map_err(|e| e.to_string())?;
let rc = unsafe {
crispasr_sys::crispasr_session_set_grammar_text(
self.handle,
cgbnf.as_ptr(),
croot.as_ptr(),
penalty,
)
};
if rc == -2 {
return Err("set_grammar_text: invalid GBNF or root rule not found".into());
}
if rc != 0 {
return Err(format!("set_grammar_text failed (rc={})", rc));
}
Ok(())
}
pub fn set_fallback_thresholds(
&self,
entropy_thold: f32,
logprob_thold: f32,
no_speech_thold: f32,
temperature_inc: f32,
) -> Result<(), String> {
let rc = unsafe {
crispasr_sys::crispasr_session_set_fallback_thresholds(
self.handle,
entropy_thold,
logprob_thold,
no_speech_thold,
temperature_inc,
)
};
if rc != 0 {
return Err(format!("set_fallback_thresholds failed (rc={})", rc));
}
Ok(())
}
pub fn set_alt_n(&self, n: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_alt_n(self.handle, n) };
if rc != 0 {
return Err(format!("set_alt_n failed (rc={})", rc));
}
Ok(())
}
pub fn set_whisper_decode_extras(
&self,
suppress_nst: bool,
suppress_regex: &str,
carry_initial_prompt: bool,
) -> Result<(), String> {
let cregex = CString::new(suppress_regex).map_err(|e| e.to_string())?;
let rc = unsafe {
crispasr_sys::crispasr_session_set_whisper_decode_extras(
self.handle,
suppress_nst as c_int,
cregex.as_ptr(),
carry_initial_prompt as c_int,
)
};
if rc != 0 {
return Err(format!("set_whisper_decode_extras failed (rc={})", rc));
}
Ok(())
}
pub fn set_ask(&self, prompt: &str) -> Result<(), String> {
let cprompt = CString::new(prompt).map_err(|e| e.to_string())?;
let rc = unsafe { crispasr_sys::crispasr_session_set_ask(self.handle, cprompt.as_ptr()) };
if rc != 0 {
return Err(format!("set_ask failed (rc={})", rc));
}
Ok(())
}
pub fn set_instruct(&self, instruct: &str) -> Result<(), String> {
let c = CString::new(instruct).map_err(|e| e.to_string())?;
let rc = unsafe { crispasr_sys::crispasr_session_set_instruct(self.handle, c.as_ptr()) };
if rc != 0 {
return Err(format!("set_instruct failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_phonemes(&self, phonemes: &str) -> Result<(), String> {
let c = CString::new(phonemes).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_tts_phonemes(self.handle, c.as_ptr()) };
if rc == -2 {
return Err("backend has no phonemes-in entry point (kokoro and piper do)".to_string());
}
if rc != 0 {
return Err(format!("set_tts_phonemes failed (rc={})", rc));
}
Ok(())
}
pub fn set_tts_pad_silence_ms(&self, ms: i32) {
unsafe { crispasr_sys::crispasr_session_set_tts_pad_silence_ms(self.handle, ms as std::os::raw::c_int) };
}
pub fn set_punc_model(&self, punc_model: &str) -> Result<(), String> {
let c = CString::new(punc_model).map_err(|e| e.to_string())?;
let rc = unsafe { crispasr_sys::crispasr_session_set_punc_model(self.handle, c.as_ptr()) };
if rc != 0 {
return Err(format!("set_punc_model failed (rc={})", rc));
}
Ok(())
}
pub fn set_hotwords(&self, hotwords: &str, boost: f32) -> Result<(), String> {
let c = CString::new(hotwords).map_err(|e| e.to_string())?;
let rc =
unsafe { crispasr_sys::crispasr_session_set_hotwords(self.handle, c.as_ptr(), boost) };
if rc != 0 {
return Err(format!("set_hotwords failed (rc={})", rc));
}
Ok(())
}
pub fn set_sensitivity(&self, preset: &str) -> Result<(), String> {
let c = CString::new(preset).map_err(|e| e.to_string())?;
let rc = unsafe { crispasr_sys::crispasr_session_set_sensitivity(self.handle, c.as_ptr()) };
if rc == -2 {
return Err(format!(
"unknown sensitivity preset {:?} (expected: conservative, balanced, aggressive)",
preset
));
}
if rc != 0 {
return Err(format!("set_sensitivity failed (rc={})", rc));
}
Ok(())
}
pub fn set_g2p_dict(&self, source: &str) -> Result<(), String> {
let c = CString::new(source).map_err(|e| e.to_string())?;
let rc = unsafe { crispasr_sys::crispasr_session_set_g2p_dict(self.handle, c.as_ptr()) };
if rc != 0 {
return Err(format!("set_g2p_dict failed (rc={})", rc));
}
Ok(())
}
pub fn set_speaker_id(&self, id: i32) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_session_set_speaker_id(self.handle, id) };
if rc != 0 {
return Err(format!("set_speaker_id failed (rc={})", rc));
}
Ok(())
}
pub fn detect_language(
&self,
pcm: &[f32],
lid_model_path: &str,
method: i32,
) -> Result<(String, f32), String> {
let cpath = CString::new(lid_model_path).map_err(|e| e.to_string())?;
let mut buf = [0u8; 16];
let mut prob: c_float = 0.0;
let rc = unsafe {
crispasr_sys::crispasr_session_detect_language(
self.handle,
pcm.as_ptr(),
pcm.len() as c_int,
cpath.as_ptr(),
method as c_int,
buf.as_mut_ptr() as *mut c_char,
buf.len() as c_int,
&mut prob as *mut c_float,
)
};
if rc != 0 {
return Err(format!("detect_language failed (rc={})", rc));
}
let cstr = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
Ok((cstr.to_string_lossy().into_owned(), prob))
}
}
#[derive(Clone, Debug)]
pub struct KokoroResolved {
pub model_path: String,
pub voice_path: Option<String>,
pub voice_name: Option<String>,
pub backbone_swapped: bool,
}
pub fn kokoro_resolve_for_lang(model_path: &str, lang: &str) -> Result<KokoroResolved, String> {
let cmodel = CString::new(model_path).map_err(|e| e.to_string())?;
let clang = CString::new(lang).map_err(|e| e.to_string())?;
let mut out_model = vec![0i8; 1024];
let mut out_voice = vec![0i8; 1024];
let mut out_picked = vec![0i8; 64];
let mut backbone_swapped = false;
unsafe {
let rc = crispasr_sys::crispasr_kokoro_resolve_model_for_lang_abi(
cmodel.as_ptr(),
clang.as_ptr(),
out_model.as_mut_ptr() as *mut c_char,
out_model.len() as c_int,
);
if rc < 0 {
return Err("kokoro_resolve_model_for_lang: buffer too small".into());
}
if rc == 0 {
backbone_swapped = true;
}
}
let model_resolved = unsafe { std::ffi::CStr::from_ptr(out_model.as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
let model_resolved = if model_resolved.is_empty() {
model_path.to_string()
} else {
model_resolved
};
let (voice_path, voice_name) = unsafe {
let rc = crispasr_sys::crispasr_kokoro_resolve_fallback_voice_abi(
cmodel.as_ptr(),
clang.as_ptr(),
out_voice.as_mut_ptr() as *mut c_char,
out_voice.len() as c_int,
out_picked.as_mut_ptr() as *mut c_char,
out_picked.len() as c_int,
);
if rc < 0 {
return Err("kokoro_resolve_fallback_voice: buffer too small".into());
}
if rc == 0 {
let p = std::ffi::CStr::from_ptr(out_voice.as_ptr() as *const c_char)
.to_string_lossy()
.into_owned();
let n = std::ffi::CStr::from_ptr(out_picked.as_ptr() as *const c_char)
.to_string_lossy()
.into_owned();
(Some(p), Some(n))
} else {
(None, None)
}
};
Ok(KokoroResolved {
model_path: model_resolved,
voice_path,
voice_name,
backbone_swapped,
})
}
#[derive(Clone, Copy, Debug)]
pub struct VadOptions {
pub threshold: f32,
pub min_speech_duration_ms: i32,
pub min_silence_duration_ms: i32,
pub speech_pad_ms: i32,
pub chunk_seconds: i32,
pub n_threads: i32,
}
impl Default for VadOptions {
fn default() -> Self {
Self {
threshold: 0.5,
min_speech_duration_ms: 250,
min_silence_duration_ms: 100,
speech_pad_ms: 30,
chunk_seconds: 30,
n_threads: 4,
}
}
}
impl VadOptions {
fn to_abi(self) -> crispasr_sys::CrispasrVadAbiOpts {
crispasr_sys::CrispasrVadAbiOpts {
threshold: self.threshold,
min_speech_duration_ms: self.min_speech_duration_ms,
min_silence_duration_ms: self.min_silence_duration_ms,
speech_pad_ms: self.speech_pad_ms,
chunk_seconds: self.chunk_seconds,
n_threads: self.n_threads,
}
}
}
impl Drop for Session {
fn drop(&mut self) {
unsafe { crispasr_sys::crispasr_session_close(self.handle) }
}
}
#[derive(Debug, Clone)]
pub struct StreamingUpdate {
pub text: String,
pub t0: f64,
pub t1: f64,
pub counter: i64,
}
pub struct Stream {
handle: *mut crispasr_sys::CrispasrStream,
}
unsafe impl Send for Stream {}
impl Stream {
pub fn set_live_decode(&self, enabled: bool) {
unsafe {
crispasr_sys::crispasr_stream_set_live_decode(self.handle, if enabled { 1 } else { 0 })
};
}
pub fn feed(&self, pcm: &[f32]) -> Result<i32, String> {
let rc = unsafe {
crispasr_sys::crispasr_stream_feed(self.handle, pcm.as_ptr(), pcm.len() as c_int)
};
if rc < 0 {
return Err(format!("stream_feed failed (rc={})", rc));
}
Ok(rc)
}
pub fn get_text(&self) -> Result<StreamingUpdate, String> {
let mut buf = vec![0u8; 8192];
let mut t0: f64 = 0.0;
let mut t1: f64 = 0.0;
let mut counter: i64 = 0;
let rc = unsafe {
crispasr_sys::crispasr_stream_get_text(
self.handle,
buf.as_mut_ptr() as *mut c_char,
buf.len() as c_int,
&mut t0,
&mut t1,
&mut counter,
)
};
if rc < 0 {
return Err(format!("stream_get_text failed (rc={})", rc));
}
let text = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }
.to_string_lossy()
.into_owned();
Ok(StreamingUpdate {
text,
t0,
t1,
counter,
})
}
pub fn flush(&self) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_stream_flush(self.handle) };
if rc < 0 {
return Err(format!("stream_flush failed (rc={})", rc));
}
Ok(())
}
}
impl Drop for Stream {
fn drop(&mut self) {
unsafe { crispasr_sys::crispasr_stream_close(self.handle) }
}
}
use std::os::raw::c_void;
use std::sync::Mutex;
pub struct Mic {
handle: *mut crispasr_sys::CrispasrMic,
_trampoline: Box<TrampolineState>,
}
unsafe impl Send for Mic {}
struct TrampolineState {
cb: Mutex<Box<dyn FnMut(&[f32]) + Send + 'static>>,
}
extern "C" fn mic_trampoline(pcm: *const c_float, n_samples: c_int, userdata: *mut c_void) {
if userdata.is_null() || pcm.is_null() || n_samples <= 0 {
return;
}
unsafe {
let state = &*(userdata as *const TrampolineState);
let slice = std::slice::from_raw_parts(pcm, n_samples as usize);
if let Ok(mut cb) = state.cb.lock() {
(cb)(slice);
}
}
}
impl Mic {
pub fn open<F>(sample_rate: i32, channels: i32, callback: F) -> Result<Mic, String>
where
F: FnMut(&[f32]) + Send + 'static,
{
let trampoline = Box::new(TrampolineState {
cb: Mutex::new(Box::new(callback)),
});
let userdata_ptr = trampoline.as_ref() as *const TrampolineState as *mut c_void;
let handle = unsafe {
crispasr_sys::crispasr_mic_open(sample_rate, channels, mic_trampoline, userdata_ptr)
};
if handle.is_null() {
return Err("crispasr_mic_open failed".to_string());
}
Ok(Mic {
handle,
_trampoline: trampoline,
})
}
pub fn start(&self) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_mic_start(self.handle) };
if rc != 0 {
return Err(format!("mic_start failed (rc={})", rc));
}
Ok(())
}
pub fn stop(&self) -> Result<(), String> {
let rc = unsafe { crispasr_sys::crispasr_mic_stop(self.handle) };
if rc != 0 {
return Err(format!("mic_stop failed (rc={})", rc));
}
Ok(())
}
}
impl Drop for Mic {
fn drop(&mut self) {
unsafe { crispasr_sys::crispasr_mic_close(self.handle) }
}
}
pub fn mic_default_device_name() -> String {
let p = unsafe { crispasr_sys::crispasr_mic_default_device_name() };
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
#[derive(Clone, Debug)]
pub struct RegistryEntry {
pub filename: String,
pub url: String,
pub approx_size: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RegistryArtifactKind {
Primary,
Companion,
Extra,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegistryArtifact {
pub kind: RegistryArtifactKind,
pub filename: String,
pub url: String,
pub approx_size: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegistryBundle {
pub backend: String,
pub license: String,
pub requires_acceptance: bool,
pub artifacts: Vec<RegistryArtifact>,
}
pub fn list_known_models() -> Vec<String> {
let mut buf = vec![0u8; 8192];
let n = unsafe {
crispasr_sys::crispasr_registry_list_backends_abi(
buf.as_mut_ptr() as *mut c_char,
buf.len() as c_int,
)
};
if n < 0 {
return Vec::new();
}
let cstr = unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) };
cstr.to_string_lossy()
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
pub fn registry_lookup(backend: &str) -> Result<Option<RegistryEntry>, String> {
registry_call_inner(backend, true)
}
pub fn registry_lookup_by_filename(filename: &str) -> Result<Option<RegistryEntry>, String> {
registry_call_inner(filename, false)
}
pub fn registry_default_bundle(backend: &str) -> Result<Option<RegistryBundle>, String> {
if backend.is_empty() {
return Ok(None);
}
let backend_c = CString::new(backend).map_err(|e| format!("backend NUL: {e}"))?;
let mut canonical_buf = [0u8; 256];
let mut license_buf = [0u8; 1024];
let mut requires_acceptance = 0;
let count = unsafe {
crispasr_sys::crispasr_registry_default_bundle_info_abi(
backend_c.as_ptr(),
canonical_buf.as_mut_ptr() as *mut c_char,
canonical_buf.len() as c_int,
license_buf.as_mut_ptr() as *mut c_char,
license_buf.len() as c_int,
&mut requires_acceptance,
)
};
if count == 0 {
return Ok(None);
}
if count < 0 {
return Err(format!(
"default-bundle registry lookup failed (rc={count})"
));
}
fn slice_to_string(buf: &[u8]) -> String {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).into_owned()
}
let mut artifacts = Vec::with_capacity(count as usize);
for index in 0..count {
let mut kind = 0;
let mut filename_buf = [0u8; 256];
let mut url_buf = [0u8; 2048];
let mut size_buf = [0u8; 64];
let rc = unsafe {
crispasr_sys::crispasr_registry_default_bundle_artifact_abi(
backend_c.as_ptr(),
index,
&mut kind,
filename_buf.as_mut_ptr() as *mut c_char,
filename_buf.len() as c_int,
url_buf.as_mut_ptr() as *mut c_char,
url_buf.len() as c_int,
size_buf.as_mut_ptr() as *mut c_char,
size_buf.len() as c_int,
)
};
if rc != 0 {
return Err(format!(
"default-bundle artifact {index} lookup failed (rc={rc})"
));
}
let kind = match kind {
0 => RegistryArtifactKind::Primary,
1 => RegistryArtifactKind::Companion,
2 => RegistryArtifactKind::Extra,
value => {
return Err(format!(
"default-bundle artifact {index} has unknown kind {value}"
))
}
};
artifacts.push(RegistryArtifact {
kind,
filename: slice_to_string(&filename_buf),
url: slice_to_string(&url_buf),
approx_size: slice_to_string(&size_buf),
});
}
Ok(Some(RegistryBundle {
backend: slice_to_string(&canonical_buf),
license: slice_to_string(&license_buf),
requires_acceptance: requires_acceptance != 0,
artifacts,
}))
}
fn registry_call_inner(key: &str, by_backend: bool) -> Result<Option<RegistryEntry>, String> {
if key.is_empty() {
return Ok(None);
}
let key_c = CString::new(key).map_err(|e| format!("key NUL: {e}"))?;
let mut fn_buf = [0u8; 256];
let mut url_buf = [0u8; 512];
let mut size_buf = [0u8; 32];
let rc = unsafe {
if by_backend {
crispasr_sys::crispasr_registry_lookup_abi(
key_c.as_ptr(),
fn_buf.as_mut_ptr() as *mut c_char,
fn_buf.len() as i32,
url_buf.as_mut_ptr() as *mut c_char,
url_buf.len() as i32,
size_buf.as_mut_ptr() as *mut c_char,
size_buf.len() as i32,
)
} else {
crispasr_sys::crispasr_registry_lookup_by_filename_abi(
key_c.as_ptr(),
fn_buf.as_mut_ptr() as *mut c_char,
fn_buf.len() as i32,
url_buf.as_mut_ptr() as *mut c_char,
url_buf.len() as i32,
size_buf.as_mut_ptr() as *mut c_char,
size_buf.len() as i32,
)
}
};
if rc != 0 {
return Ok(None);
}
fn slice_to_string(buf: &[u8]) -> String {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).into_owned()
}
Ok(Some(RegistryEntry {
filename: slice_to_string(&fn_buf),
url: slice_to_string(&url_buf),
approx_size: slice_to_string(&size_buf),
}))
}
pub fn cache_ensure_file(
filename: &str,
url: &str,
quiet: bool,
cache_dir_override: Option<&str>,
) -> Result<Option<String>, String> {
if filename.is_empty() || url.is_empty() {
return Ok(None);
}
let fn_c = CString::new(filename).map_err(|e| format!("filename NUL: {e}"))?;
let url_c = CString::new(url).map_err(|e| format!("url NUL: {e}"))?;
let ov_c = CString::new(cache_dir_override.unwrap_or(""))
.map_err(|e| format!("cache_dir_override NUL: {e}"))?;
let mut buf = vec![0u8; 2048];
let rc = unsafe {
crispasr_sys::crispasr_cache_ensure_file_abi(
fn_c.as_ptr(),
url_c.as_ptr(),
if quiet { 1 } else { 0 },
ov_c.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
)
};
if rc != 0 {
return Ok(None);
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
Ok(Some(String::from_utf8_lossy(&buf[..end]).into_owned()))
}
pub fn cache_dir(override_path: Option<&str>) -> Result<Option<String>, String> {
let ov_c =
CString::new(override_path.unwrap_or("")).map_err(|e| format!("override NUL: {e}"))?;
let mut buf = vec![0u8; 2048];
let rc = unsafe {
crispasr_sys::crispasr_cache_dir_abi(
ov_c.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
)
};
if rc != 0 {
return Ok(None);
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
Ok(Some(String::from_utf8_lossy(&buf[..end]).into_owned()))
}
#[derive(Clone, Debug)]
pub struct AlignedWord {
pub text: String,
pub start: f64, pub end: f64,
}
pub fn align_words(
aligner_model: &str,
transcript: &str,
pcm: &[f32],
t_offset: f64,
n_threads: i32,
) -> Result<Vec<AlignedWord>, String> {
if aligner_model.is_empty() || transcript.is_empty() || pcm.is_empty() {
return Ok(Vec::new());
}
let model_c = CString::new(aligner_model).map_err(|e| format!("aligner_model NUL: {e}"))?;
let trans_c = CString::new(transcript).map_err(|e| format!("transcript NUL: {e}"))?;
let res = unsafe {
crispasr_sys::crispasr_align_words_abi(
model_c.as_ptr(),
trans_c.as_ptr(),
pcm.as_ptr(),
pcm.len() as i32,
(t_offset * 100.0).round() as i64,
n_threads,
)
};
if res.is_null() {
return Ok(Vec::new());
}
let mut out = Vec::new();
unsafe {
let n = crispasr_sys::crispasr_align_result_n_words(res);
for i in 0..n {
let tp = crispasr_sys::crispasr_align_result_word_text(res, i);
let text = if tp.is_null() {
String::new()
} else {
CStr::from_ptr(tp).to_string_lossy().into_owned()
};
let t0 = crispasr_sys::crispasr_align_result_word_t0(res, i) as f64 / 100.0;
let t1 = crispasr_sys::crispasr_align_result_word_t1(res, i) as f64 / 100.0;
out.push(AlignedWord {
text,
start: t0,
end: t1,
});
}
crispasr_sys::crispasr_align_result_free(res);
}
Ok(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum LidMethod {
Whisper = 0,
Silero = 1,
Firered = 2,
Ecapa = 3,
}
#[derive(Clone, Debug)]
pub struct LidResult {
pub lang_code: String,
pub confidence: f32,
}
pub fn detect_language_pcm(
pcm: &[f32],
method: LidMethod,
model_path: &str,
n_threads: i32,
use_gpu: bool,
gpu_device: i32,
flash_attn: bool,
) -> Result<LidResult, String> {
if pcm.is_empty() || model_path.is_empty() {
return Ok(LidResult {
lang_code: String::new(),
confidence: -1.0,
});
}
let path_c = CString::new(model_path).map_err(|e| format!("model_path contains NUL: {e}"))?;
let mut buf = [0u8; 16];
let mut conf: c_float = -1.0;
let rc = unsafe {
crispasr_sys::crispasr_detect_language_pcm(
pcm.as_ptr(),
pcm.len() as i32,
method as i32,
path_c.as_ptr(),
n_threads,
if use_gpu { 1 } else { 0 },
gpu_device,
if flash_attn { 1 } else { 0 },
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
&mut conf,
)
};
if rc != 0 {
return Ok(LidResult {
lang_code: String::new(),
confidence: -1.0,
});
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let code = std::str::from_utf8(&buf[..end])
.map_err(|e| format!("LID returned non-UTF8 bytes: {e}"))?
.to_string();
Ok(LidResult {
lang_code: code,
confidence: conf as f32,
})
}
#[derive(Clone, Debug)]
pub struct TextLidResult {
pub label: String,
pub confidence: f32,
}
pub fn text_detect_language(
text: &str,
model_path: &str,
n_threads: i32,
) -> Result<TextLidResult, String> {
let ctext = CString::new(text).map_err(|e| format!("text contains NUL: {e}"))?;
let cmodel = CString::new(model_path).map_err(|e| format!("model_path contains NUL: {e}"))?;
let mut buf = [0u8; 256];
let mut conf: c_float = -1.0;
let rc = unsafe {
crispasr_sys::crispasr_text_detect_language(
ctext.as_ptr(),
cmodel.as_ptr(),
n_threads,
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
&mut conf,
)
};
match rc {
0 => {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let label = std::str::from_utf8(&buf[..end])
.map_err(|e| format!("text-LID returned non-UTF8 bytes: {e}"))?
.to_string();
Ok(TextLidResult {
label,
confidence: conf as f32,
})
}
-1 => Err("text-LID: invalid args (null pointer or bad buffer size)".to_string()),
1 => Err(format!(
"text-LID dispatcher init/predict failed for model {model_path} \
(check the GGUF's architecture key — must be `lid-cld3` or `lid-fasttext`)"
)),
2 => Err(
"text-LID label exceeded 256-byte output buffer — file an issue, this shouldn't happen \
with the dispatcher's current label spaces"
.to_string(),
),
other => Err(format!("text-LID returned unexpected status code {other}")),
}
}
#[derive(Clone, Copy, Debug)]
pub struct DiarizeSegment {
pub t0: f64,
pub t1: f64,
pub speaker: i32,
}
impl DiarizeSegment {
pub fn new(t0: f64, t1: f64) -> Self {
Self {
t0,
t1,
speaker: -1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum DiarizeMethod {
Energy = 0,
Xcorr = 1,
VadTurns = 2,
Pyannote = 3,
FoxNose = 4,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DiarizeOptions {
pub method: DiarizeMethod,
pub pyannote_model_path: Option<String>,
pub n_threads: i32,
pub slice_t0: f64,
pub foxnose_embedder_path: Option<String>,
pub min_speakers: i32,
pub max_speakers: i32,
pub num_speakers: i32,
}
impl Default for DiarizeOptions {
fn default() -> Self {
Self {
method: DiarizeMethod::VadTurns,
pyannote_model_path: None,
n_threads: 4,
slice_t0: 0.0,
foxnose_embedder_path: None,
min_speakers: 0,
max_speakers: 0,
num_speakers: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DiarizeTurn {
pub t0: f64,
pub t1: f64,
pub speaker: i32,
}
pub fn diarize_segments(
segs: &mut [DiarizeSegment],
left: &[f32],
right: Option<&[f32]>,
is_stereo: bool,
opts: &DiarizeOptions,
) -> Result<(), String> {
diarize_inner(segs, left, right, is_stereo, opts, false).map(|_| ())
}
pub fn diarize_segments_with_turns(
segs: &mut [DiarizeSegment],
left: &[f32],
right: Option<&[f32]>,
is_stereo: bool,
opts: &DiarizeOptions,
) -> Result<Vec<DiarizeTurn>, String> {
diarize_inner(segs, left, right, is_stereo, opts, true)
}
fn diarize_inner(
segs: &mut [DiarizeSegment],
left: &[f32],
right: Option<&[f32]>,
is_stereo: bool,
opts: &DiarizeOptions,
want_turns: bool,
) -> Result<Vec<DiarizeTurn>, String> {
if segs.is_empty() || left.is_empty() {
return Ok(Vec::new());
}
let path_c = match (&opts.pyannote_model_path, opts.method) {
(Some(p), DiarizeMethod::Pyannote) => Some(
CString::new(p.as_str())
.map_err(|e| format!("pyannote_model_path contains NUL: {e}"))?,
),
_ => None,
};
let foxnose_c = match (&opts.foxnose_embedder_path, opts.method) {
(Some(p), DiarizeMethod::FoxNose) => Some(
CString::new(p.as_str())
.map_err(|e| format!("foxnose_embedder_path contains NUL: {e}"))?,
),
_ => None,
};
let abi_opts = crispasr_sys::CrispasrDiarizeOptsAbi {
method: opts.method as i32,
n_threads: opts.n_threads,
slice_t0_cs: (opts.slice_t0 * 100.0).round() as i64,
pyannote_model_path: path_c
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null()),
foxnose_embedder_path: foxnose_c
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null()),
min_speakers: opts.min_speakers,
max_speakers: opts.max_speakers,
num_speakers: opts.num_speakers,
_pad2: 0,
};
let mut abi_segs: Vec<crispasr_sys::CrispasrDiarizeSegAbi> = segs
.iter()
.map(|s| crispasr_sys::CrispasrDiarizeSegAbi {
t0_cs: (s.t0 * 100.0).round() as i64,
t1_cs: (s.t1 * 100.0).round() as i64,
speaker: s.speaker,
_pad: 0,
})
.collect();
let right_ptr = match (is_stereo, right) {
(true, Some(r)) => r.as_ptr(),
_ => left.as_ptr(),
};
if !want_turns {
let rc = unsafe {
crispasr_sys::crispasr_diarize_segments_abi(
left.as_ptr(),
right_ptr,
left.len() as i32,
if is_stereo { 1 } else { 0 },
abi_segs.as_mut_ptr(),
abi_segs.len() as i32,
&abi_opts,
)
};
return finish(rc, segs, &abi_segs, &[], 0).map(|_| Vec::new());
}
let mut cap = (left.len() as f64 / 16_000.0 / 0.5).ceil() as usize + segs.len() + 16;
let mut turns: Vec<crispasr_sys::CrispasrDiarizeTurnAbi> = Vec::new();
for attempt in 0..2 {
turns.clear();
turns.resize(
cap,
crispasr_sys::CrispasrDiarizeTurnAbi {
t0_cs: 0,
t1_cs: 0,
speaker: -1,
_pad: 0,
},
);
let mut n_turns: i32 = 0;
let rc = unsafe {
crispasr_sys::crispasr_diarize_segments_turns_abi(
left.as_ptr(),
right_ptr,
left.len() as i32,
if is_stereo { 1 } else { 0 },
abi_segs.as_mut_ptr(),
abi_segs.len() as i32,
&abi_opts,
turns.as_mut_ptr(),
cap as i32,
&mut n_turns,
)
};
if rc == 2 && attempt == 0 {
cap = n_turns.max(1) as usize;
continue;
}
return finish(rc, segs, &abi_segs, &turns, n_turns);
}
unreachable!("the retry loop returns on its second pass")
}
fn finish(
rc: i32,
segs: &mut [DiarizeSegment],
abi_segs: &[crispasr_sys::CrispasrDiarizeSegAbi],
abi_turns: &[crispasr_sys::CrispasrDiarizeTurnAbi],
n_turns: i32,
) -> Result<Vec<DiarizeTurn>, String> {
match rc {
0 => {
for (i, s) in segs.iter_mut().enumerate() {
s.speaker = abi_segs[i].speaker;
}
let n = (n_turns.max(0) as usize).min(abi_turns.len());
Ok(abi_turns[..n]
.iter()
.map(|t| DiarizeTurn {
t0: t.t0_cs as f64 / 100.0,
t1: t.t1_cs as f64 / 100.0,
speaker: t.speaker,
})
.collect())
}
1 => Err("diarize model load failed (pyannote / foxnose embedder)".to_string()),
-1 => Err("invalid arguments to crispasr_diarize_segments_abi".to_string()),
2 => Err(format!(
"diarize turn buffer too small — {n_turns} turns needed; this is a \
crispasr bug, please report it with the audio length and segment count"
)),
other => Err(format!("crispasr_diarize_segments_abi returned {other}")),
}
}
pub struct SpeakerEmbedder {
raw: *mut std::ffi::c_void,
}
impl SpeakerEmbedder {
pub fn new(model_spec: &str, n_threads: i32, cache_dir: Option<&str>) -> Result<Self, String> {
let spec_c = std::ffi::CString::new(model_spec).map_err(|e| e.to_string())?;
let cache_c = cache_dir
.map(|s| std::ffi::CString::new(s))
.transpose()
.map_err(|e: std::ffi::NulError| e.to_string())?;
let cache_ptr = cache_c
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(std::ptr::null());
let raw = unsafe {
crispasr_sys::crispasr_speaker_embedder_make_abi(spec_c.as_ptr(), n_threads, cache_ptr)
};
if raw.is_null() {
return Err(format!("failed to build speaker embedder '{model_spec}'"));
}
Ok(Self { raw })
}
pub fn dim(&self) -> i32 {
unsafe { crispasr_sys::crispasr_speaker_embedder_dim_abi(self.raw) }
}
pub fn name(&self) -> String {
unsafe {
let p = crispasr_sys::crispasr_speaker_embedder_name_abi(self.raw);
if p.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
}
pub fn embed(&self, pcm_16k: &[f32]) -> Option<Vec<f32>> {
let dim = self.dim();
if dim <= 0 || pcm_16k.is_empty() {
return None;
}
let mut out = vec![0.0f32; dim as usize];
let ok = unsafe {
crispasr_sys::crispasr_speaker_embedder_embed_abi(
self.raw,
pcm_16k.as_ptr(),
pcm_16k.len() as i32,
out.as_mut_ptr(),
)
};
if ok != 0 {
Some(out)
} else {
None
}
}
}
impl Drop for SpeakerEmbedder {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe { crispasr_sys::crispasr_speaker_embedder_free_abi(self.raw) };
self.raw = std::ptr::null_mut();
}
}
}
unsafe impl Send for SpeakerEmbedder {}
pub fn agglomerative_cluster(
embeddings: &[f32],
n: i32,
dim: i32,
merge_threshold: f32,
max_speakers: i32,
) -> Result<Vec<i32>, String> {
if n <= 0 || dim <= 0 || embeddings.len() < (n as usize) * (dim as usize) {
return Err("invalid arguments to agglomerative_cluster".to_string());
}
let mut out = vec![0i32; n as usize];
let k = unsafe {
crispasr_sys::crispasr_speaker_cluster_abi(
embeddings.as_ptr(),
n,
dim,
merge_threshold,
max_speakers,
out.as_mut_ptr(),
)
};
if k < 0 {
return Err("crispasr_speaker_cluster_abi returned -1".to_string());
}
Ok(out)
}
pub struct PyannoteCache {
raw: *mut std::ffi::c_void,
}
impl PyannoteCache {
pub fn compute(pcm_16k: &[f32], model_path: &str, n_threads: i32) -> Result<Self, String> {
if pcm_16k.is_empty() {
return Err("empty audio".to_string());
}
let model_c = std::ffi::CString::new(model_path).map_err(|e| e.to_string())?;
let raw = unsafe {
crispasr_sys::crispasr_pyannote_cache_compute_abi(
pcm_16k.as_ptr(),
pcm_16k.len() as i32,
model_c.as_ptr(),
n_threads,
)
};
if raw.is_null() {
return Err(format!(
"failed to compute pyannote cache from '{model_path}'"
));
}
Ok(Self { raw })
}
pub fn apply(&self, segs: &mut [DiarizeSegment], slice_t0: f64) -> Result<(), String> {
if segs.is_empty() {
return Ok(());
}
let mut abi_segs: Vec<crispasr_sys::CrispasrDiarizeSegAbi> = segs
.iter()
.map(|s| crispasr_sys::CrispasrDiarizeSegAbi {
t0_cs: (s.t0 * 100.0).round() as i64,
t1_cs: (s.t1 * 100.0).round() as i64,
speaker: s.speaker,
_pad: 0,
})
.collect();
let rc = unsafe {
crispasr_sys::crispasr_pyannote_cache_apply_abi(
self.raw,
(slice_t0 * 100.0).round() as i64,
abi_segs.as_mut_ptr(),
abi_segs.len() as i32,
)
};
if rc != 0 {
return Err(format!("crispasr_pyannote_cache_apply_abi returned {rc}"));
}
for (i, s) in segs.iter_mut().enumerate() {
s.speaker = abi_segs[i].speaker;
}
Ok(())
}
}
impl Drop for PyannoteCache {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe { crispasr_sys::crispasr_pyannote_cache_free_abi(self.raw) };
self.raw = std::ptr::null_mut();
}
}
}
unsafe impl Send for PyannoteCache {}
pub struct PuncModel {
handle: *mut std::ffi::c_void,
}
unsafe impl Send for PuncModel {}
impl PuncModel {
pub fn open(model_path: &str) -> Result<Self, String> {
let c_path = CString::new(model_path).map_err(|e| e.to_string())?;
let handle = unsafe { crispasr_sys::crispasr_punc_init(c_path.as_ptr()) };
if handle.is_null() {
return Err(format!("Failed to load punc model: {model_path}"));
}
Ok(Self { handle })
}
pub fn process(&self, text: &str) -> String {
let c_text = CString::new(text).unwrap_or_default();
let result = unsafe { crispasr_sys::crispasr_punc_process(self.handle, c_text.as_ptr()) };
if result.is_null() {
return text.to_string();
}
let out = unsafe { CStr::from_ptr(result) }
.to_string_lossy()
.into_owned();
unsafe { crispasr_sys::crispasr_punc_free_text(result) };
out
}
}
impl Drop for PuncModel {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { crispasr_sys::crispasr_punc_free(self.handle) };
self.handle = std::ptr::null_mut();
}
}
}
pub struct Parakeet {
handle: *mut std::ffi::c_void,
}
unsafe impl Send for Parakeet {}
pub struct ParakeetResult {
handle: *mut std::ffi::c_void,
}
impl ParakeetResult {
pub fn text(&self) -> String {
let p = unsafe { crispasr_sys::crispasr_parakeet_result_text(self.handle) };
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
pub fn n_words(&self) -> i32 {
unsafe { crispasr_sys::crispasr_parakeet_result_n_words(self.handle) }
}
pub fn word_text(&self, i: i32) -> String {
let p = unsafe { crispasr_sys::crispasr_parakeet_result_word_text(self.handle, i) };
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
pub fn word_t0(&self, i: i32) -> i64 {
unsafe { crispasr_sys::crispasr_parakeet_result_word_t0(self.handle, i) }
}
pub fn word_t1(&self, i: i32) -> i64 {
unsafe { crispasr_sys::crispasr_parakeet_result_word_t1(self.handle, i) }
}
pub fn n_tokens(&self) -> i32 {
unsafe { crispasr_sys::crispasr_parakeet_result_n_tokens(self.handle) }
}
pub fn token_text(&self, i: i32) -> String {
let p = unsafe { crispasr_sys::crispasr_parakeet_result_token_text(self.handle, i) };
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
pub fn token_t0(&self, i: i32) -> i64 {
unsafe { crispasr_sys::crispasr_parakeet_result_token_t0(self.handle, i) }
}
pub fn token_t1(&self, i: i32) -> i64 {
unsafe { crispasr_sys::crispasr_parakeet_result_token_t1(self.handle, i) }
}
pub fn token_p(&self, i: i32) -> f32 {
unsafe { crispasr_sys::crispasr_parakeet_result_token_p(self.handle, i) }
}
}
impl Drop for ParakeetResult {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { crispasr_sys::crispasr_parakeet_result_free(self.handle) };
self.handle = std::ptr::null_mut();
}
}
}
impl Parakeet {
pub fn new(model_path: &str, n_threads: i32, use_flash: bool) -> Result<Self, String> {
let c_path = CString::new(model_path).map_err(|e| e.to_string())?;
let handle = unsafe {
crispasr_sys::crispasr_parakeet_init(
c_path.as_ptr(),
n_threads,
if use_flash { 1 } else { 0 },
)
};
if handle.is_null() {
return Err(format!("Failed to load Parakeet model: {model_path}"));
}
Ok(Self { handle })
}
pub fn transcribe(
&self,
pcm: &[f32],
language: Option<&str>,
) -> Result<ParakeetResult, String> {
let lang = language.map(|l| CString::new(l).unwrap_or_default());
let lang_ptr = lang.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
let res = unsafe {
crispasr_sys::crispasr_parakeet_transcribe(
self.handle,
pcm.as_ptr(),
pcm.len() as c_int,
lang_ptr,
)
};
if res.is_null() {
return Err("crispasr_parakeet_transcribe returned null".to_string());
}
Ok(ParakeetResult { handle: res })
}
}
impl Drop for Parakeet {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { crispasr_sys::crispasr_parakeet_free(self.handle) };
self.handle = std::ptr::null_mut();
}
}
}
pub fn lcs_dedup_prefix_count(prev_tail: &[i32], curr: &[i32], min_lcs_length: i32) -> i32 {
unsafe {
crispasr_sys::crispasr_lcs_dedup_prefix_count(
prev_tail.as_ptr(),
prev_tail.len() as c_int,
curr.as_ptr(),
curr.len() as c_int,
min_lcs_length,
)
}
}
pub fn vad_segments(
model_path: &str,
pcm: &[f32],
sample_rate: i32,
threshold: f32,
min_speech_ms: i32,
min_silence_ms: i32,
n_threads: i32,
use_gpu: bool,
) -> Result<Vec<(f32, f32)>, String> {
let c_path = CString::new(model_path).map_err(|e| e.to_string())?;
let mut out_spans: *mut f32 = std::ptr::null_mut();
let n = unsafe {
crispasr_sys::crispasr_vad_segments(
c_path.as_ptr(),
pcm.as_ptr(),
pcm.len() as c_int,
sample_rate,
threshold,
min_speech_ms,
min_silence_ms,
n_threads,
if use_gpu { 1 } else { 0 },
&mut out_spans,
)
};
if n < 0 {
return Err(format!("crispasr_vad_segments failed (rc={n})"));
}
let mut spans = Vec::with_capacity(n as usize);
for i in 0..n as isize {
unsafe {
spans.push((*out_spans.offset(2 * i), *out_spans.offset(2 * i + 1)));
}
}
if n > 0 {
unsafe { crispasr_sys::crispasr_vad_free(out_spans) };
}
Ok(spans)
}
pub fn vad_slices(
model_path: &str,
pcm: &[f32],
sample_rate: i32,
threshold: f32,
min_speech_ms: i32,
min_silence_ms: i32,
speech_pad_ms: i32,
max_chunk_duration_s: f32,
n_threads: i32,
) -> Result<Vec<(f32, f32)>, String> {
let c_path = CString::new(model_path).map_err(|e| e.to_string())?;
let mut out_spans: *mut f32 = std::ptr::null_mut();
let n = unsafe {
crispasr_sys::crispasr_vad_slices(
c_path.as_ptr(),
pcm.as_ptr(),
pcm.len() as c_int,
sample_rate,
threshold,
min_speech_ms,
min_silence_ms,
speech_pad_ms,
max_chunk_duration_s,
n_threads,
&mut out_spans,
)
};
if n < 0 {
let why = match n {
-1 => " (bad arguments)",
-2 => " (allocation failed)",
-3 => " (the VAD model could not be loaded)",
_ => "",
};
return Err(format!("crispasr_vad_slices failed (rc={n}){why}"));
}
let mut spans = Vec::with_capacity(n as usize);
for i in 0..n as isize {
unsafe {
spans.push((*out_spans.offset(2 * i), *out_spans.offset(2 * i + 1)));
}
}
if n > 0 {
unsafe { crispasr_sys::crispasr_vad_free(out_spans) };
}
Ok(spans)
}
pub fn enhance_audio_rnnoise(pcm: &[f32]) -> Result<Vec<f32>, String> {
let mut out = vec![0f32; pcm.len()];
let rc = unsafe {
crispasr_sys::crispasr_enhance_audio_rnnoise(
pcm.as_ptr(),
pcm.len() as i32,
out.as_mut_ptr(),
out.len() as i32,
)
};
if rc != 0 {
return Err(format!("enhance_audio_rnnoise failed (rc={rc})"));
}
Ok(out)
}
pub fn titanet_cosine_sim(a: &[f32], b: &[f32]) -> f32 {
let dim = a.len().min(b.len()) as i32;
unsafe { crispasr_sys::crispasr_titanet_cosine_sim(a.as_ptr(), b.as_ptr(), dim) }
}
pub struct SpeakerDB {
handle: *mut std::ffi::c_void,
dir_path: String,
}
unsafe impl Send for SpeakerDB {}
impl SpeakerDB {
pub fn load(dir_path: &str) -> Result<Self, String> {
let c_path = CString::new(dir_path).map_err(|e| e.to_string())?;
let handle = unsafe { crispasr_sys::crispasr_speaker_db_load(c_path.as_ptr()) };
if handle.is_null() {
return Err(format!("Failed to load speaker DB: {dir_path}"));
}
Ok(Self {
handle,
dir_path: dir_path.to_string(),
})
}
pub fn count(&self) -> i32 {
unsafe { crispasr_sys::crispasr_speaker_db_count(self.handle) }
}
pub fn match_embedding(&self, embedding: &[f32], threshold: f32) -> (Option<String>, f32) {
let mut name_buf = vec![0u8; 256];
let score = unsafe {
crispasr_sys::crispasr_speaker_db_match(
self.handle,
embedding.as_ptr(),
embedding.len() as i32,
threshold,
name_buf.as_mut_ptr() as *mut c_char,
256,
)
};
let name = if score >= threshold {
let c_str = unsafe { CStr::from_ptr(name_buf.as_ptr() as *const c_char) };
Some(c_str.to_string_lossy().into_owned())
} else {
None
};
(name, score)
}
pub fn enroll(&self, name: &str, embedding: &[f32]) -> Result<(), String> {
let c_dir = CString::new(&*self.dir_path).map_err(|e| e.to_string())?;
let c_name = CString::new(name).map_err(|e| e.to_string())?;
let rc = unsafe {
crispasr_sys::crispasr_speaker_db_enroll(
c_dir.as_ptr(),
c_name.as_ptr(),
embedding.as_ptr(),
embedding.len() as i32,
)
};
if rc != 0 {
return Err(format!("speaker_db_enroll failed (rc={rc})"));
}
Ok(())
}
}
impl Drop for SpeakerDB {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { crispasr_sys::crispasr_speaker_db_free(self.handle) };
self.handle = std::ptr::null_mut();
}
}
}
pub fn kokoro_lang_is_german(lang: &str) -> bool {
let c = CString::new(lang).unwrap_or_default();
unsafe { crispasr_sys::crispasr_kokoro_lang_is_german_abi(c.as_ptr()) }
}
pub fn kokoro_lang_has_native_voice(lang: &str) -> bool {
let c = CString::new(lang).unwrap_or_default();
unsafe { crispasr_sys::crispasr_kokoro_lang_has_native_voice_abi(c.as_ptr()) }
}
use std::cell::Cell;
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChatError {
Aborted(String),
Failed(String),
}
impl ChatError {
pub fn is_aborted(&self) -> bool {
matches!(self, ChatError::Aborted(_))
}
pub fn message(&self) -> &str {
match self {
ChatError::Aborted(m) | ChatError::Failed(m) => m,
}
}
fn from_raw(err: &crispasr_sys::CrispasrChatError, code_hint: i32, fallback: &str) -> Self {
let code = if err.code != 0 { err.code } else { code_hint };
let bytes: Vec<u8> = err
.message
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
let mut msg = String::from_utf8_lossy(&bytes).into_owned();
if msg.is_empty() {
msg = fallback.to_string();
}
if code == crispasr_sys::CRISPASR_CHAT_ERR_ABORTED {
ChatError::Aborted(msg)
} else {
ChatError::Failed(msg)
}
}
}
impl std::fmt::Display for ChatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChatError::Aborted(m) => write!(f, "aborted: {m}"),
ChatError::Failed(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for ChatError {}
impl From<ChatError> for String {
fn from(e: ChatError) -> String {
e.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
impl ChatMessage {
pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: role.into(),
content: content.into(),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self::new("system", content)
}
pub fn user(content: impl Into<String>) -> Self {
Self::new("user", content)
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::new("assistant", content)
}
}
#[derive(Debug, Clone, Default)]
pub struct ChatOptions {
pub n_threads: Option<i32>,
pub n_threads_batch: Option<i32>,
pub n_ctx: Option<i32>,
pub n_batch: Option<i32>,
pub n_ubatch: Option<i32>,
pub n_gpu_layers: Option<i32>,
pub use_mmap: Option<bool>,
pub use_mlock: Option<bool>,
pub chat_template: Option<String>,
}
struct RawOpenParams {
_template: Option<CString>,
params: crispasr_sys::CrispasrChatOpenParams,
}
impl ChatOptions {
fn to_raw(&self) -> Result<RawOpenParams, ChatError> {
let mut params = crispasr_sys::CrispasrChatOpenParams::default();
unsafe { crispasr_sys::crispasr_chat_open_params_default(&mut params) };
if let Some(v) = self.n_threads {
params.n_threads = v;
}
if let Some(v) = self.n_threads_batch {
params.n_threads_batch = v;
}
if let Some(v) = self.n_ctx {
params.n_ctx = v;
}
if let Some(v) = self.n_batch {
params.n_batch = v;
}
if let Some(v) = self.n_ubatch {
params.n_ubatch = v;
}
if let Some(v) = self.n_gpu_layers {
params.n_gpu_layers = v;
}
if let Some(v) = self.use_mmap {
params.use_mmap = v;
}
if let Some(v) = self.use_mlock {
params.use_mlock = v;
}
let template = match &self.chat_template {
Some(t) => {
let c = CString::new(t.as_str())
.map_err(|e| ChatError::Failed(format!("chat_template NUL: {e}")))?;
params.chat_template = c.as_ptr();
Some(c)
}
None => None,
};
Ok(RawOpenParams {
_template: template,
params,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct ChatGenerateOptions {
pub max_tokens: Option<i32>,
pub temperature: Option<f32>,
pub top_k: Option<i32>,
pub top_p: Option<f32>,
pub min_p: Option<f32>,
pub repeat_penalty: Option<f32>,
pub repeat_last_n: Option<i32>,
pub seed: Option<u32>,
pub stop: Vec<String>,
pub prefill_only: bool,
}
struct RawGenerateParams {
_stop_owned: Vec<CString>,
_stop_ptrs: Vec<*const c_char>,
params: crispasr_sys::CrispasrChatGenerateParams,
}
impl ChatGenerateOptions {
fn to_raw(&self) -> Result<RawGenerateParams, ChatError> {
let mut params = crispasr_sys::CrispasrChatGenerateParams::default();
unsafe { crispasr_sys::crispasr_chat_generate_params_default(&mut params) };
if let Some(v) = self.max_tokens {
params.max_tokens = v;
}
if let Some(v) = self.temperature {
params.temperature = v;
}
if let Some(v) = self.top_k {
params.top_k = v;
}
if let Some(v) = self.top_p {
params.top_p = v;
}
if let Some(v) = self.min_p {
params.min_p = v;
}
if let Some(v) = self.repeat_penalty {
params.repeat_penalty = v;
}
if let Some(v) = self.repeat_last_n {
params.repeat_last_n = v;
}
if let Some(v) = self.seed {
params.seed = v;
}
params.prefill_only = self.prefill_only;
let mut stop_owned = Vec::with_capacity(self.stop.len());
for s in &self.stop {
stop_owned.push(
CString::new(s.as_str())
.map_err(|e| ChatError::Failed(format!("stop sequence NUL: {e}")))?,
);
}
let stop_ptrs: Vec<*const c_char> = stop_owned.iter().map(|s| s.as_ptr()).collect();
if stop_ptrs.is_empty() {
params.stop = std::ptr::null();
params.n_stop = 0;
} else {
params.stop = stop_ptrs.as_ptr();
params.n_stop = stop_ptrs.len();
}
Ok(RawGenerateParams {
_stop_owned: stop_owned,
_stop_ptrs: stop_ptrs,
params,
})
}
}
struct RawMessages {
_owned: Vec<(CString, CString)>,
raw: Vec<crispasr_sys::CrispasrChatMessage>,
}
fn raw_messages(messages: &[ChatMessage]) -> Result<RawMessages, ChatError> {
let mut owned = Vec::with_capacity(messages.len());
for m in messages {
let role = CString::new(m.role.as_str())
.map_err(|e| ChatError::Failed(format!("role NUL: {e}")))?;
let content = CString::new(m.content.as_str())
.map_err(|e| ChatError::Failed(format!("content NUL: {e}")))?;
owned.push((role, content));
}
let raw = owned
.iter()
.map(|(r, c)| crispasr_sys::CrispasrChatMessage {
role: r.as_ptr(),
content: c.as_ptr(),
})
.collect();
Ok(RawMessages { _owned: owned, raw })
}
struct TokenState<'a> {
on_token: &'a mut dyn FnMut(&str),
failed: &'a Cell<bool>,
panic: Option<Box<dyn std::any::Any + Send>>,
pending: Vec<u8>,
}
impl TokenState<'_> {
fn deliver(&mut self, text: &str) {
if self.panic.is_some() || text.is_empty() {
return;
}
let on_token = &mut *self.on_token;
let outcome = catch_unwind(AssertUnwindSafe(|| on_token(text)));
if let Err(p) = outcome {
self.panic = Some(p);
self.failed.set(true);
}
}
fn deliver_utf8(&mut self, bytes: &[u8]) {
let text = take_complete_utf8(&mut self.pending, bytes);
self.deliver(&text);
}
fn flush(&mut self) {
if self.pending.is_empty() {
return;
}
let tail = String::from_utf8_lossy(&self.pending).into_owned();
self.pending.clear();
self.deliver(&tail);
}
}
fn take_complete_utf8(pending: &mut Vec<u8>, bytes: &[u8]) -> String {
pending.extend_from_slice(bytes);
let mut out = String::new();
let mut consumed = 0;
loop {
match std::str::from_utf8(&pending[consumed..]) {
Ok(rest) => {
out.push_str(rest);
consumed = pending.len();
break;
}
Err(e) => {
let good = e.valid_up_to();
out.push_str(&String::from_utf8_lossy(
&pending[consumed..consumed + good],
));
match e.error_len() {
Some(bad) => {
out.push('\u{fffd}');
consumed += good + bad;
}
None => {
consumed += good;
break;
}
}
}
}
}
pending.drain(..consumed);
out
}
extern "C" fn token_trampoline(chunk: *const c_char, user: *mut c_void) {
if user.is_null() || chunk.is_null() {
return;
}
let st = unsafe { &mut *(user as *mut TokenState) };
if st.panic.is_some() {
return;
}
let bytes = unsafe { CStr::from_ptr(chunk) }.to_bytes();
st.deliver_utf8(bytes);
}
struct AbortState<'a> {
should_continue: &'a mut dyn FnMut() -> bool,
token_failed: &'a Cell<bool>,
panic: Option<Box<dyn std::any::Any + Send>>,
}
extern "C" fn abort_trampoline(user: *mut c_void) -> bool {
if user.is_null() {
return true; }
let st = unsafe { &mut *(user as *mut AbortState) };
if st.panic.is_some() {
return false; }
if st.token_failed.get() {
return false;
}
let outcome = catch_unwind(AssertUnwindSafe(|| (st.should_continue)()));
match outcome {
Ok(keep_going) => keep_going,
Err(p) => {
st.panic = Some(p);
false
}
}
}
pub struct ChatSession {
handle: *mut crispasr_sys::CrispasrChatSession,
token_failed: Cell<bool>,
abort_user: Cell<*mut c_void>,
}
unsafe impl Send for ChatSession {}
impl ChatSession {
pub fn open(model_path: &str) -> Result<Self, ChatError> {
Self::open_with_options(model_path, &ChatOptions::default())
}
pub fn open_with_options(model_path: &str, options: &ChatOptions) -> Result<Self, ChatError> {
let path = CString::new(model_path)
.map_err(|e| ChatError::Failed(format!("invalid path: {e}")))?;
let raw = options.to_raw()?;
let mut err = crispasr_sys::CrispasrChatError::default();
let handle =
unsafe { crispasr_sys::crispasr_chat_open(path.as_ptr(), &raw.params, &mut err) };
if handle.is_null() {
return Err(ChatError::from_raw(
&err,
0,
&format!("failed to open chat model {model_path:?}"),
));
}
Ok(Self {
handle,
token_failed: Cell::new(false),
abort_user: Cell::new(std::ptr::null_mut()),
})
}
pub fn memory_estimate(model_path: &str, options: &ChatOptions) -> Result<usize, ChatError> {
let path = CString::new(model_path)
.map_err(|e| ChatError::Failed(format!("invalid path: {e}")))?;
let raw = options.to_raw()?;
let mut err = crispasr_sys::CrispasrChatError::default();
let bytes = unsafe {
crispasr_sys::crispasr_chat_memory_estimate(path.as_ptr(), &raw.params, &mut err)
};
if bytes == 0 {
return Err(ChatError::from_raw(
&err,
0,
"could not estimate chat model memory",
));
}
Ok(bytes)
}
pub fn ai_disclosure_text() -> &'static str {
let p = unsafe { crispasr_sys::crispasr_chat_ai_disclosure_text() };
if p.is_null() {
return "";
}
unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")
}
pub fn n_ctx(&self) -> i32 {
unsafe { crispasr_sys::crispasr_chat_n_ctx(self.handle) }
}
pub fn template_name(&self) -> String {
let p = unsafe { crispasr_sys::crispasr_chat_template_name(self.handle) };
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
pub fn reset(&self) -> Result<(), ChatError> {
let mut err = crispasr_sys::CrispasrChatError::default();
let rc = unsafe { crispasr_sys::crispasr_chat_reset(self.handle, &mut err) };
if rc != 0 {
return Err(ChatError::from_raw(&err, rc, "crispasr_chat_reset failed"));
}
Ok(())
}
pub fn count_tokens(&self, messages: &[ChatMessage]) -> Result<i32, ChatError> {
let msgs = raw_messages(messages)?;
let mut err = crispasr_sys::CrispasrChatError::default();
let n = unsafe {
crispasr_sys::crispasr_chat_count_tokens(
self.handle,
msgs.raw.as_ptr(),
msgs.raw.len(),
&mut err,
)
};
if n < 0 {
return Err(ChatError::from_raw(
&err,
0,
"crispasr_chat_count_tokens failed",
));
}
Ok(n)
}
pub fn generate(&self, messages: &[ChatMessage]) -> Result<String, ChatError> {
self.generate_with_options(messages, &ChatGenerateOptions::default())
}
pub fn generate_with_options(
&self,
messages: &[ChatMessage],
params: &ChatGenerateOptions,
) -> Result<String, ChatError> {
let msgs = raw_messages(messages)?;
let gp = params.to_raw()?;
self.token_failed.set(false);
let mut err = crispasr_sys::CrispasrChatError::default();
let out = unsafe {
crispasr_sys::crispasr_chat_generate(
self.handle,
msgs.raw.as_ptr(),
msgs.raw.len(),
&gp.params,
&mut err,
)
};
if out.is_null() {
return Err(ChatError::from_raw(
&err,
0,
"crispasr_chat_generate failed",
));
}
let text = unsafe { CStr::from_ptr(out) }
.to_string_lossy()
.into_owned();
unsafe { crispasr_sys::crispasr_chat_string_free(out) };
Ok(text)
}
pub fn generate_stream<F: FnMut(&str)>(
&self,
messages: &[ChatMessage],
on_token: F,
) -> Result<(), ChatError> {
self.generate_stream_with_options(messages, &ChatGenerateOptions::default(), on_token)
}
pub fn generate_stream_with_options<F: FnMut(&str)>(
&self,
messages: &[ChatMessage],
params: &ChatGenerateOptions,
mut on_token: F,
) -> Result<(), ChatError> {
let msgs = raw_messages(messages)?;
let gp = params.to_raw()?;
self.token_failed.set(false);
let mut state = TokenState {
on_token: &mut on_token,
failed: &self.token_failed,
panic: None,
pending: Vec::new(),
};
let mut err = crispasr_sys::CrispasrChatError::default();
let rc = unsafe {
crispasr_sys::crispasr_chat_generate_stream(
self.handle,
msgs.raw.as_ptr(),
msgs.raw.len(),
&gp.params,
Some(token_trampoline),
&mut state as *mut TokenState as *mut c_void,
&mut err,
)
};
state.flush();
if let Some(p) = state.panic.take() {
resume_unwind(p);
}
if rc != 0 {
return Err(ChatError::from_raw(
&err,
rc,
"crispasr_chat_generate_stream failed",
));
}
Ok(())
}
pub fn with_abort_callback<A, B, R>(&self, mut should_continue: A, body: B) -> R
where
A: FnMut() -> bool,
B: FnOnce(&ChatSession) -> R,
{
let mut state = AbortState {
should_continue: &mut should_continue,
token_failed: &self.token_failed,
panic: None,
};
struct Restore<'s> {
session: &'s ChatSession,
previous: *mut c_void,
}
impl Drop for Restore<'_> {
fn drop(&mut self) {
unsafe {
if self.previous.is_null() {
crispasr_sys::crispasr_chat_set_abort_callback(
self.session.handle,
None,
std::ptr::null_mut(),
);
} else {
crispasr_sys::crispasr_chat_set_abort_callback(
self.session.handle,
Some(abort_trampoline),
self.previous,
);
}
}
self.session.abort_user.set(self.previous);
}
}
let previous = self.abort_user.get();
let user = &mut state as *mut AbortState as *mut c_void;
unsafe {
crispasr_sys::crispasr_chat_set_abort_callback(
self.handle,
Some(abort_trampoline),
user,
)
};
self.abort_user.set(user);
let guard = Restore {
session: self,
previous,
};
let out = body(self);
drop(guard);
if let Some(p) = state.panic.take() {
resume_unwind(p);
}
out
}
}
impl Drop for ChatSession {
fn drop(&mut self) {
unsafe { crispasr_sys::crispasr_chat_close(self.handle) }
}
}
#[cfg(test)]
mod chat_stream_tests {
use super::take_complete_utf8;
#[test]
fn a_character_split_over_several_chunks_is_delivered_once() {
let mut pending = Vec::new();
let mut out = String::new();
for b in "🪿".as_bytes() {
out.push_str(&take_complete_utf8(&mut pending, &[*b]));
}
assert_eq!(out, "🪿");
assert!(pending.is_empty(), "nothing left over: {pending:?}");
}
#[test]
fn a_complete_chunk_passes_straight_through() {
let mut pending = Vec::new();
assert_eq!(take_complete_utf8(&mut pending, b"hello"), "hello");
assert!(pending.is_empty());
}
#[test]
fn a_partial_tail_is_held_back_and_nothing_before_it_is_lost() {
let mut pending = Vec::new();
assert_eq!(take_complete_utf8(&mut pending, b"ab\xe2\x82"), "ab");
assert_eq!(pending, b"\xe2\x82");
assert_eq!(take_complete_utf8(&mut pending, b"\xacc"), "€c");
assert!(pending.is_empty());
}
#[test]
fn a_genuinely_invalid_sequence_is_replaced_not_buffered() {
let mut pending = Vec::new();
assert_eq!(take_complete_utf8(&mut pending, b"a\xffb"), "a\u{fffd}b");
assert!(
pending.is_empty(),
"invalid bytes must not be held: {pending:?}"
);
}
#[test]
fn a_truncated_sequence_is_replaced_once_the_next_character_arrives() {
let mut pending = Vec::new();
assert_eq!(take_complete_utf8(&mut pending, b"\xe2\x82"), "");
assert_eq!(take_complete_utf8(&mut pending, b"x"), "\u{fffd}x");
assert!(pending.is_empty());
}
#[test]
fn an_empty_chunk_delivers_nothing_and_keeps_the_buffer() {
let mut pending = Vec::new();
assert_eq!(take_complete_utf8(&mut pending, b"\xf0\x9f"), "");
assert_eq!(take_complete_utf8(&mut pending, b""), "");
assert_eq!(pending, b"\xf0\x9f");
}
}