use std::ffi::{CStr, CString, c_char, c_int, c_ulonglong};
use std::ptr;
use std::sync::{Mutex, MutexGuard, PoisonError};
use crate::error::{Error, Result};
use crate::model::Model;
pub(crate) const MAX_SAMPLES: usize = 480_000;
static ENGINE: Mutex<State> = Mutex::new(State {
taken: [false; 2],
loaded: [None; 2],
});
struct State {
taken: [bool; 2],
loaded: [Option<u64>; 2],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Kind {
Text,
Speech,
}
impl Kind {
pub(crate) const fn bit(self) -> c_int {
match self {
Kind::Text => cactus_sys::NEEDLE_TEXT,
Kind::Speech => cactus_sys::NEEDLE_SPEECH,
}
}
const fn other(self) -> Kind {
match self {
Kind::Text => Kind::Speech,
Kind::Speech => Kind::Text,
}
}
const fn index(self) -> usize {
match self {
Kind::Text => 0,
Kind::Speech => 1,
}
}
const fn model(self) -> Model {
match self {
Kind::Text => Model::Needle,
Kind::Speech => Model::Whistle,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Input<'a> {
Text(&'a CStr),
Audio(&'a [f32]),
}
impl Input<'_> {
fn raw(self) -> Result<(*const c_char, *const f32, c_int)> {
match self {
Input::Text(text) => Ok((text.as_ptr(), ptr::null(), 0)),
Input::Audio(pcm) => Ok((ptr::null(), pcm.as_ptr(), samples(pcm)?)),
}
}
}
fn samples(pcm: &[f32]) -> Result<c_int> {
if pcm.len() > MAX_SAMPLES {
return Err(Error::AudioTooLong { samples: pcm.len() });
}
Ok(clamp(pcm.len()))
}
fn clamp(len: usize) -> c_int {
c_int::try_from(len).unwrap_or(c_int::MAX)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct EncodedAudio {
language: Option<CString>,
keywords: Option<CString>,
word_timestamps: c_int,
}
impl EncodedAudio {
pub(crate) fn new(
language: Option<CString>,
keywords: Option<CString>,
word_timestamps: bool,
) -> Self {
EncodedAudio {
language,
keywords,
word_timestamps: c_int::from(word_timestamps),
}
}
#[cfg(test)]
pub(crate) fn language(&self) -> Option<&CStr> {
self.language.as_deref()
}
#[cfg(test)]
pub(crate) fn keywords(&self) -> Option<&CStr> {
self.keywords.as_deref()
}
#[cfg(test)]
pub(crate) fn word_timestamps(&self) -> bool {
self.word_timestamps != 0
}
fn raw(&self) -> (*const c_char, *const c_char, c_int) {
(
self.language.as_deref().map_or(ptr::null(), CStr::as_ptr),
self.keywords.as_deref().map_or(ptr::null(), CStr::as_ptr),
self.word_timestamps,
)
}
}
pub(crate) struct Guard(MutexGuard<'static, State>);
pub(crate) fn lock() -> Guard {
Guard(ENGINE.lock().unwrap_or_else(PoisonError::into_inner))
}
impl Guard {
pub(crate) fn models(&mut self) -> c_int {
unsafe { cactus_sys::needle_models() }
}
pub(crate) fn last_error(&mut self) -> String {
let reason = unsafe { cactus_sys::needle_last_error() };
if reason.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(reason) }
.to_string_lossy()
.into_owned()
}
fn load(&mut self, bytes: &[u8]) -> c_int {
unsafe { cactus_sys::needle_load(bytes.as_ptr(), bytes.len() as c_ulonglong) }
}
pub(crate) fn init(
&mut self,
system: Option<&CStr>,
tools: &CStr,
tool_index: Option<&CStr>,
) -> c_int {
let system = system.map_or(ptr::null(), CStr::as_ptr);
let tool_index = tool_index.map_or(ptr::null(), CStr::as_ptr);
unsafe { cactus_sys::needle_init(system, tools.as_ptr(), tool_index) }
}
pub(crate) fn complete(
&mut self,
input: Input<'_>,
max_new_tokens: c_int,
out: &mut [u8],
) -> Result<c_int> {
let (text, pcm, samples) = input.raw()?;
let capacity = clamp(out.len());
let rc = unsafe {
cactus_sys::needle_complete(
text,
pcm,
samples,
max_new_tokens,
out.as_mut_ptr().cast::<c_char>(),
capacity,
)
};
Ok(rc)
}
pub(crate) fn set_audio(&mut self, audio: &EncodedAudio) {
let (language, keywords, word_timestamps) = audio.raw();
unsafe { cactus_sys::needle_set_audio(language, keywords, word_timestamps) };
}
pub(crate) fn transcribe(
&mut self,
pcm: &[f32],
audio: &EncodedAudio,
out: &mut [u8],
) -> Result<c_int> {
let samples = samples(pcm)?;
let (language, keywords, word_timestamps) = audio.raw();
let capacity = clamp(out.len());
let rc = unsafe {
cactus_sys::needle_transcribe(
pcm.as_ptr(),
samples,
language,
keywords,
word_timestamps,
out.as_mut_ptr().cast::<c_char>(),
capacity,
)
};
Ok(rc)
}
pub(crate) fn embed_len(&mut self, input: Input<'_>) -> Result<c_int> {
let (text, pcm, samples) = input.raw()?;
let rc = unsafe { cactus_sys::needle_embed(text, pcm, samples, ptr::null_mut(), 0) };
Ok(rc)
}
pub(crate) fn embed(&mut self, input: Input<'_>, out: &mut [f32]) -> Result<c_int> {
let (text, pcm, samples) = input.raw()?;
let capacity = clamp(out.len());
let rc =
unsafe { cactus_sys::needle_embed(text, pcm, samples, out.as_mut_ptr(), capacity) };
Ok(rc)
}
pub(crate) fn reset(&mut self) {
unsafe { cactus_sys::needle_reset() };
}
}
#[derive(Debug)]
pub(crate) struct Slot {
kind: Kind,
}
impl Slot {
pub(crate) fn take(kind: Kind) -> Result<Self> {
let mut guard = lock();
let taken = &mut guard.0.taken[kind.index()];
if *taken {
return Err(Error::EngineBusy);
}
*taken = true;
Ok(Slot { kind })
}
pub(crate) fn load(&self, guard: &mut Guard, bytes: &[u8]) -> Result<()> {
let mine = self.kind;
let other = mine.other();
let wanted = fingerprint(bytes);
let expected = Error::WrongModel {
expected: mine.model(),
};
if guard.0.loaded[mine.index()] == Some(wanted) {
return Ok(());
}
if guard.0.loaded[other.index()] == Some(wanted) {
return Err(expected);
}
if guard.0.loaded[mine.index()].is_some() {
return Err(Error::WeightsAlreadyLoaded);
}
let before = guard.models();
if guard.load(bytes) < 0 {
return Err(Error::Load);
}
let after = guard.models();
let gained = after & !before;
let is_mine = if gained & mine.bit() != 0 {
true
} else if gained & other.bit() != 0 {
false
} else {
after & mine.bit() != 0 && after & other.bit() == 0
};
if is_mine {
guard.0.loaded[mine.index()] = Some(wanted);
Ok(())
} else {
guard.0.loaded[other.index()].get_or_insert(wanted);
Err(expected)
}
}
}
impl Drop for Slot {
fn drop(&mut self) {
lock().0.taken[self.kind.index()] = false;
}
}
fn fingerprint(bytes: &[u8]) -> u64 {
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ bytes.len() as u64;
let mut words = bytes.chunks_exact(8);
for word in &mut words {
let mut buffer = [0u8; 8];
buffer.copy_from_slice(word);
hash = (hash ^ u64::from_le_bytes(buffer)).wrapping_mul(PRIME);
}
for byte in words.remainder() {
hash = (hash ^ u64::from(*byte)).wrapping_mul(PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn different_archives_fingerprint_differently() {
assert_ne!(fingerprint(b"needle-a"), fingerprint(b"needle-b"));
assert_ne!(fingerprint(b"needle"), fingerprint(b"needle\0"));
assert_eq!(fingerprint(b"needle"), fingerprint(b"needle"));
}
#[test]
fn a_slot_is_exclusive_until_dropped() {
let slot = Slot::take(Kind::Speech).expect("nothing else takes the speech slot");
assert!(matches!(Slot::take(Kind::Speech), Err(Error::EngineBusy)));
drop(slot);
drop(Slot::take(Kind::Speech).expect("the slot was freed"));
}
#[test]
fn kinds_map_to_distinct_bits_and_models() {
assert_eq!(Kind::Text.bit() & Kind::Speech.bit(), 0);
assert_eq!(Kind::Text.other(), Kind::Speech);
assert_eq!(Kind::Speech.model(), Model::Whistle);
assert_eq!(Kind::Text.model(), Model::Needle);
}
#[test]
fn input_puts_exactly_one_pointer_in_play() {
let (text, pcm, samples) = Input::Text(c"kitchen")
.raw()
.expect("text is never too long");
assert!(!text.is_null() && pcm.is_null() && samples == 0);
let (text, pcm, samples) = Input::Audio(&[]).raw().expect("an empty clip is short");
assert!(text.is_null() && !pcm.is_null() && samples == 0);
}
#[test]
fn a_clip_over_thirty_seconds_never_reaches_the_engine() {
let clip = vec![0.0_f32; MAX_SAMPLES + 1];
let error = Input::Audio(&clip).raw().expect_err("too long");
assert!(matches!(error, Error::AudioTooLong { samples } if samples == MAX_SAMPLES + 1));
assert!(Input::Audio(&clip[..MAX_SAMPLES]).raw().is_ok());
}
#[test]
fn lengths_clamp_to_an_int() {
assert_eq!(clamp(usize::MAX), c_int::MAX);
assert_eq!(clamp(7), 7);
}
#[test]
fn encoded_audio_passes_nulls_for_unset_fields() {
let (language, keywords, words) = EncodedAudio::default().raw();
assert!(language.is_null() && keywords.is_null() && words == 0);
let audio = EncodedAudio::new(Some(c"de".to_owned()), None, true);
let (language, keywords, words) = audio.raw();
assert!(!language.is_null() && keywords.is_null() && words == 1);
}
}