use core::{num::NonZeroU16, time::Duration};
use std::{path::Path, time::Instant};
use half::f16;
use windit::{
TailPolicy,
plan::{Span, WindowOptions, WindowPlan},
};
use super::{
artifacts::{ArtifactLabel, Part, stamp::Stamp},
config::HIDDEN_SIZE,
error::{
Allocation, AllocationSite, Artifact, AudioTooShort, AudioWindowTooLong, ContractRefusal,
Error, LoadRefusal, PredictionFailure, Result, WinditError,
},
media::{AUDIO_ROWS, AudioTokens},
options::{AudioOptions, DerivationTerms},
prewarm::{PrewarmReport, Warmed},
};
use crate::{
DataType, Model, MultiArray,
model::contract::{Checked, Dim, FeatureContract, LoadContract, StateContract},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]
#[display("{}", self.as_str())]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum AudioPrecision {
Fp16,
Fp32,
}
impl AudioPrecision {
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Fp16 => "fp16",
Self::Fp32 => "fp32",
}
}
#[inline]
pub const fn package_stem(&self) -> &'static str {
match self {
Self::Fp16 => "EmbeddingGemma2Audio",
Self::Fp32 => "EmbeddingGemma2AudioFp32",
}
}
}
pub(crate) const WINDOW_SAMPLES: usize = 160_000;
pub(crate) const PAD_LEFT: usize = 160;
pub(crate) const WAVEFORM_SAMPLES: usize = 160_160;
pub(crate) const FRAMES: usize = 1_000;
const FRAME_SAMPLES: usize = 321;
const FRAME_HOP: usize = 160;
pub(crate) const MIN_SAMPLES: usize = FRAME_SAMPLES - PAD_LEFT;
#[inline]
pub(crate) const fn valid_frames(samples: usize) -> usize {
if PAD_LEFT + samples < FRAME_SAMPLES {
0
} else {
let frames = (PAD_LEFT + samples - FRAME_SAMPLES) / FRAME_HOP + 1;
if frames < FRAMES { frames } else { FRAMES }
}
}
#[inline]
pub(crate) const fn token_count(samples: usize) -> usize {
valid_frames(samples).div_ceil(4)
}
pub(crate) fn fill_waveform(window: &[f32], waveform: &mut [f32]) {
let (pad, rest) = waveform.split_at_mut(PAD_LEFT);
pad.fill(0.0);
let (body, tail) = rest.split_at_mut(window.len());
body.copy_from_slice(window);
tail.fill(0.0);
}
pub(crate) fn fill_mask(frames: usize, mask: &mut [f16]) {
let (valid, rest) = mask.split_at_mut(frames);
valid.fill(f16::ONE);
rest.fill(f16::ZERO);
}
pub(crate) fn plan(samples: usize, options: &AudioOptions) -> Result<Vec<Span>> {
if samples < MIN_SAMPLES {
return Err(Error::AudioTooShort(AudioTooShort::new(
samples,
MIN_SAMPLES,
)));
}
let mut geometry = WindowOptions::new(WINDOW_SAMPLES)
.with_hop(options.hop_samples().get() as usize)
.with_tail(TailPolicy::KeepWithCoverage);
if let Some(max) = options.max_windows() {
geometry = geometry.with_max_windows(max.get() as usize);
}
let mut spans = WindowPlan::spans(&geometry, samples).map_err(|e| match e {
WinditError::AllocFailed { elements } => Error::Allocation(Allocation::new(
AllocationSite::AudioPlan,
elements.saturating_mul(size_of::<Span>()),
)),
e => Error::Windowing(e),
})?;
if spans.len() > 1
&& spans
.last()
.is_some_and(|last| last.len() < options.min_tail_samples() as usize)
{
spans.pop();
}
Ok(spans)
}
fn contract() -> LoadContract {
LoadContract::new(
vec![
FeatureContract::new(
"waveform",
DataType::F32,
vec![Dim::Exactly(1), Dim::Exactly(WAVEFORM_SAMPLES)],
),
FeatureContract::new(
"frame_mask",
DataType::F16,
vec![Dim::Exactly(1), Dim::Exactly(FRAMES)],
),
],
vec![FeatureContract::new(
"audio_tokens",
DataType::F16,
vec![
Dim::Exactly(1),
Dim::Exactly(AUDIO_ROWS),
Dim::Exactly(HIDDEN_SIZE),
],
)],
StateContract::None,
)
}
#[derive(Debug)]
pub struct AudioEncoder {
model: Checked,
options: AudioOptions,
label: ArtifactLabel,
waveform: MultiArray,
frame_mask: MultiArray,
load: Duration,
}
impl AudioEncoder {
pub const SAMPLE_RATE_HZ: u32 = 16_000;
pub const WINDOW_SAMPLES: usize = WINDOW_SAMPLES;
pub fn new(dir: impl AsRef<Path>, options: &AudioOptions) -> Result<Self> {
let dir = dir.as_ref();
let artifact = Artifact::AudioBundle(options.precision());
let view = Stamp::read(dir)?.part(dir, Part::Audio, options.precision())?;
view.expect(options.expect_artifacts())?;
let started = Instant::now();
let model = Model::load_with(view.bundle(), &options.coreml())
.map_err(|e| Error::Load(LoadRefusal::new(artifact, None, e)))?;
let model = Checked::new(model, &contract())
.map_err(|violation| ContractRefusal::of(artifact, None, violation))?;
let load = started.elapsed();
Ok(Self {
model,
options: options.clone(),
label: view.label(),
waveform: MultiArray::zeros(&[1, WAVEFORM_SAMPLES], DataType::F32)?,
frame_mask: MultiArray::zeros(&[1, FRAMES], DataType::F16)?,
load,
})
}
pub fn prewarm(&mut self) -> Result<PrewarmReport> {
let started = Instant::now();
self.encode_window(&[0.0; MIN_SAMPLES])?;
let mut report = PrewarmReport::default();
report.push(Warmed::new(None, self.load, started.elapsed()));
Ok(report)
}
pub fn plan(&self, samples: usize) -> Result<Vec<Span>> {
plan(samples, &self.options)
}
pub fn encode_window(&mut self, window: &[f32]) -> Result<AudioTokens> {
if window.len() < MIN_SAMPLES {
return Err(Error::AudioTooShort(AudioTooShort::new(
window.len(),
MIN_SAMPLES,
)));
}
if window.len() > WINDOW_SAMPLES {
return Err(Error::AudioWindowTooLong(AudioWindowTooLong::new(
window.len(),
WINDOW_SAMPLES,
)));
}
fill_waveform(window, self.waveform.as_slice_mut::<f32>()?);
fill_mask(
valid_frames(window.len()),
self.frame_mask.as_slice_mut::<f16>()?,
);
let artifact = Artifact::AudioBundle(self.options.precision());
let failure = |e| Error::Prediction(PredictionFailure::new(artifact, None, e));
let mut outputs = self
.model
.predict_with(&[
("waveform", &self.waveform),
("frame_mask", &self.frame_mask),
])
.map_err(failure)?;
let tokens = outputs.take("audio_tokens").ok_or_else(|| {
failure(crate::PredictionError::MissingOutput(
"audio_tokens".to_owned(),
))
})?;
let valid = u16::try_from(token_count(window.len()))
.ok()
.and_then(NonZeroU16::new)
.expect("161 to 160,000 samples make 1 to 250 tokens");
Ok(AudioTokens::new(tokens, valid))
}
#[inline]
pub const fn precision(&self) -> AudioPrecision {
self.options.precision()
}
#[inline]
pub const fn options(&self) -> &AudioOptions {
&self.options
}
#[inline]
pub fn derivation_terms(&self) -> DerivationTerms<'_> {
DerivationTerms::audio(self.label, &self.options)
}
}
#[cfg(test)]
mod tests;