use std::fmt;
use super::options::TranscribeOptions;
use super::transcript::Transcript;
use super::weights::Weights;
use super::{FRAME_SAMPLES, frame_count, validate};
use crate::error::{Error, Result};
use crate::ffi::{self, EncodedAudio, Input, Kind, Slot};
const TRANSCRIPT_CAPACITY: usize = 256 * 1024;
fn reason(detail: String) -> String {
if detail.is_empty() {
"the engine gave no reason".to_owned()
} else {
detail
}
}
#[derive(Debug, Clone)]
pub struct WhistleBuilder {
weights: Weights,
options: TranscribeOptions,
}
impl WhistleBuilder {
#[must_use]
pub fn options(mut self, options: TranscribeOptions) -> Self {
self.options = options;
self
}
pub fn build(self) -> Result<Whistle> {
let audio = self.options.encode()?;
let slot = Slot::take(Kind::Speech)?;
let mut guard = ffi::lock();
slot.load(&mut guard, self.weights.as_bytes())?;
drop(guard);
drop(self.weights);
Ok(Whistle {
_slot: slot,
options: self.options,
audio,
out: Vec::new(),
width: None,
})
}
}
pub struct Whistle {
_slot: Slot,
options: TranscribeOptions,
audio: EncodedAudio,
out: Vec<u8>,
width: Option<usize>,
}
impl Whistle {
#[must_use]
pub fn builder(weights: Weights) -> WhistleBuilder {
WhistleBuilder {
weights,
options: TranscribeOptions::new(),
}
}
pub fn transcribe(&mut self, pcm: &[f32]) -> Result<Transcript> {
transcribe(&mut self.out, pcm, &self.audio)
}
pub fn transcribe_with_options(
&mut self,
pcm: &[f32],
options: &TranscribeOptions,
) -> Result<Transcript> {
let audio = options.encode()?;
transcribe(&mut self.out, pcm, &audio)
}
pub fn embed(&mut self, pcm: &[f32]) -> Result<Vec<f32>> {
validate(pcm)?;
let mut guard = ffi::lock();
let wanted = guard.embed_len(Input::Audio(pcm))?;
let Ok(len) = usize::try_from(wanted) else {
return Err(Error::EmbedAudio {
detail: reason(guard.last_error()),
});
};
let mut embedding = vec![0.0_f32; len];
let written = guard.embed(Input::Audio(pcm), &mut embedding)?;
if written != wanted {
let detail = if written < 0 {
reason(guard.last_error())
} else {
format!("the engine wrote {written} floats after asking for {wanted}")
};
return Err(Error::EmbedAudio { detail });
}
Ok(embedding)
}
pub fn embedding_width(&mut self) -> Result<usize> {
if let Some(width) = self.width {
return Ok(width);
}
let frame = [0.0_f32; FRAME_SAMPLES];
let mut guard = ffi::lock();
let rc = guard.embed_len(Input::Audio(&frame))?;
let Ok(count) = usize::try_from(rc) else {
return Err(Error::EmbedAudio {
detail: reason(guard.last_error()),
});
};
drop(guard);
let frames = frame_count(FRAME_SAMPLES);
if count == 0 || count % frames != 0 {
return Err(Error::EmbedAudio {
detail: format!("the engine sized {FRAME_SAMPLES} samples at {count} floats"),
});
}
let width = count / frames;
self.width = Some(width);
Ok(width)
}
#[inline]
#[must_use]
pub const fn options(&self) -> &TranscribeOptions {
&self.options
}
}
fn transcribe(out: &mut Vec<u8>, pcm: &[f32], audio: &EncodedAudio) -> Result<Transcript> {
validate(pcm)?;
out.resize(TRANSCRIPT_CAPACITY, 0);
out[0] = 0;
let mut guard = ffi::lock();
let rc = guard.transcribe(pcm, audio, out)?;
if rc < 0 {
return Err(Error::Transcribe {
detail: reason(guard.last_error()),
});
}
drop(guard);
let capacity = out.len();
let end = out.iter().position(|byte| *byte == 0).unwrap_or(capacity);
if end >= capacity - 1 {
return Err(Error::Truncated { capacity });
}
String::from_utf8_lossy(&out[..end]).parse()
}
impl fmt::Debug for Whistle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Whistle")
.field("options", &self.options)
.field("embedding_width", &self.width)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_whistle_is_send_and_sync() {
fn assert_bounds<T: Send + Sync>() {}
assert_bounds::<Whistle>();
assert_bounds::<WhistleBuilder>();
}
#[test]
fn a_bad_keyword_fails_the_build_before_the_engine_is_touched() {
let weights = Weights::from_bytes(vec![0x84, 0x2A, 0xE1, 0x05]).expect("the tag");
let error = Whistle::builder(weights)
.options(TranscribeOptions::new().with_keyword("a\nb"))
.build()
.expect_err("a line break inside a keyword");
assert!(matches!(error, Error::InvalidKeyword { .. }));
}
#[test]
fn a_reason_is_never_empty() {
assert_eq!(reason(String::new()), "the engine gave no reason");
assert_eq!(
reason("audio limit is 30 s".to_owned()),
"audio limit is 30 s"
);
}
#[test]
fn the_buffer_is_the_size_upstream_uses() {
assert_eq!(TRANSCRIPT_CAPACITY, 262_144);
}
}