use super::ScorePooling;
pub type Result<T> = core::result::Result<T, Error>;
pub use windit::WinditError;
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error("log-probability at index {index} is {value}, which is not a value <= 0")]
pub struct InvalidLogProbability {
index: usize,
value: f32,
}
impl InvalidLogProbability {
pub(crate) const fn new(index: usize, value: f32) -> Self {
Self { index, value }
}
#[inline]
pub const fn index(&self) -> usize {
self.index
}
#[inline]
pub const fn value(&self) -> f32 {
self.value
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("model contract mismatch on `{feature}`: expected {expected}, got {actual}")]
pub struct ContractMismatch {
feature: &'static str,
expected: String,
actual: String,
}
impl ContractMismatch {
pub(crate) fn new(feature: &'static str, expected: String, actual: String) -> Self {
Self {
feature,
expected,
actual,
}
}
#[inline]
pub const fn feature(&self) -> &'static str {
self.feature
}
#[inline]
pub fn expected(&self) -> &str {
&self.expected
}
#[inline]
pub fn actual(&self) -> &str {
&self.actual
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("output shape mismatch: expected {expected:?}, got {got:?}")]
pub struct OutputShape {
got: Vec<usize>,
expected: Vec<usize>,
}
impl OutputShape {
pub(crate) fn new(got: Vec<usize>, expected: Vec<usize>) -> Self {
Self { got, expected }
}
#[inline]
pub fn got(&self) -> &[usize] {
&self.got
}
#[inline]
pub fn expected(&self) -> &[usize] {
&self.expected
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"audio has {samples} samples ({frames} mel frames), outside the model's accepted \
{min_frames}..={max_frames} frames ({min_samples}..={max_samples} samples at 16 kHz)"
)]
pub struct FrameCountOutOfRange {
samples: usize,
frames: usize,
min_frames: usize,
max_frames: usize,
min_samples: usize,
max_samples: usize,
}
impl FrameCountOutOfRange {
#[must_use]
pub const fn for_samples(samples: usize) -> Self {
Self {
samples,
frames: super::frame_count(samples),
min_frames: super::MIN_FRAMES,
max_frames: super::MAX_FRAMES,
min_samples: super::MIN_SAMPLES,
max_samples: super::MAX_SAMPLES,
}
}
#[inline]
pub const fn samples(&self) -> usize {
self.samples
}
#[inline]
pub const fn frames(&self) -> usize {
self.frames
}
#[inline]
pub const fn min_frames(&self) -> usize {
self.min_frames
}
#[inline]
pub const fn max_frames(&self) -> usize {
self.max_frames
}
#[inline]
pub const fn min_samples(&self) -> usize {
self.min_samples
}
#[inline]
pub const fn max_samples(&self) -> usize {
self.max_samples
}
#[inline]
pub const fn is_too_short(&self) -> bool {
self.frames < self.min_frames
}
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[error(
"{pooling:?} pooling produced a row whose probabilities sum to {mass}, not 1, \
so it is not a distribution"
)]
pub struct NotADistribution {
pooling: ScorePooling,
mass: f64,
}
impl NotADistribution {
pub(crate) const fn new(pooling: ScorePooling, mass: f64) -> Self {
Self { pooling, mass }
}
#[inline]
pub const fn pooling(&self) -> ScorePooling {
self.pooling
}
#[inline]
pub const fn mass(&self) -> f64 {
self.mass
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("failed to load model: {0}")]
Load(#[from] crate::LoadError),
#[error("prediction failed: {0}")]
Prediction(#[from] crate::PredictionError),
#[error("tensor failed: {0}")]
Tensor(#[from] crate::TensorError),
#[error(transparent)]
ContractMismatch(#[from] ContractMismatch),
#[error(
"model declares a required input `{0}` that this door never supplies; \
it sends `mel_features` and nothing else, so every prediction would fail"
)]
UnsatisfiableInput(String),
#[error(
"model declares the state buffer `{0}`, and this door predicts through the \
stateless API"
)]
UnsatisfiableState(String),
#[error(transparent)]
OutputShape(#[from] OutputShape),
#[error(transparent)]
FrameCountOutOfRange(#[from] FrameCountOutOfRange),
#[error("audio input contains a non-finite sample at index {0}")]
NonFiniteInput(usize),
#[error("model output contains a non-finite log-probability at index {0}")]
NonFiniteOutput(usize),
#[error("model emitted a positive score: {0}")]
PositiveOutput(InvalidLogProbability),
#[error("windowing failed: {0}")]
Windowing(#[from] WinditError),
#[error("cannot aggregate an empty window list")]
EmptyWindows,
#[error(
"{0:?} pooling left no probability mass: every language pooled to probability \
zero, so the result is not a distribution and its ranking would be arbitrary"
)]
ZeroMassAggregate(ScorePooling),
#[error(
"window {0} has no finite largest log-probability, so no shift makes the row a \
distribution: it is -inf throughout, which rules every language out, or it holds \
a +inf, which is not a log-probability row at all, or it holds a NaN, which sits \
under no bound"
)]
UnnormalizableWindow(usize),
#[error(transparent)]
NotADistribution(#[from] NotADistribution),
#[error("expected a row of exactly {n} log-probabilities, got {0}", n = super::NUM_LANGUAGES)]
LanguageCountMismatch(usize),
#[error(transparent)]
InvalidLogProbability(#[from] InvalidLogProbability),
#[error("language index {0} has no roster entry")]
UnknownLanguageIndex(usize),
}
#[cfg(test)]
mod tests;