#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractMismatch {
feature: &'static str,
expected: String,
actual: String,
}
impl ContractMismatch {
#[inline(always)]
pub const fn new(feature: &'static str, expected: String, actual: String) -> Self {
Self {
feature,
expected,
actual,
}
}
#[inline(always)]
pub const fn feature(&self) -> &'static str {
self.feature
}
#[inline(always)]
pub fn expected(&self) -> &str {
&self.expected
}
#[inline(always)]
pub fn actual(&self) -> &str {
&self.actual
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ModelError {
#[error("failed to load model: {0}")]
Load(#[from] crate::LoadError),
#[error(
"model contract mismatch on `{}`: expected {}, got {}",
.0.feature(),
.0.expected(),
.0.actual()
)]
ContractMismatch(ContractMismatch),
#[error(
"model declares a required input `{0}` that this door never supplies, \
so every prediction would fail"
)]
UnsatisfiableInput(String),
#[error(
"model declares the state buffer `{0}`, and this door predicts through \
the stateless API"
)]
UnsatisfiableState(String),
}
pub(crate) fn contract_violation(
violation: crate::model::contract::ContractViolation,
) -> ModelError {
use crate::model::contract::Rendered;
match violation.rendered() {
Rendered::UnsatisfiableInput(name) => ModelError::UnsatisfiableInput(name),
Rendered::UnsatisfiableState(name) => ModelError::UnsatisfiableState(name),
Rendered::Feature(feature) => ModelError::ContractMismatch(ContractMismatch::new(
feature.feature(),
feature.clone().expected(),
feature.actual(),
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InputLength {
got: usize,
expected: usize,
}
impl InputLength {
#[inline(always)]
pub const fn new(got: usize, expected: usize) -> Self {
Self { got, expected }
}
#[inline(always)]
pub const fn got(&self) -> usize {
self.got
}
#[inline(always)]
pub const fn expected(&self) -> usize {
self.expected
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputShape {
got: Vec<usize>,
expected: Vec<usize>,
}
impl OutputShape {
#[inline(always)]
pub const fn new(got: Vec<usize>, expected: Vec<usize>) -> Self {
Self { got, expected }
}
#[inline(always)]
pub fn got(&self) -> &[usize] {
&self.got
}
#[inline(always)]
pub fn expected(&self) -> &[usize] {
&self.expected
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InferError {
#[error("prediction failed: {0}")]
Prediction(#[from] crate::PredictionError),
#[error("tensor failed: {0}")]
Tensor(#[from] crate::TensorError),
#[error("output contains a non-finite value at index {0}")]
NonFiniteOutput(usize),
#[error("input length mismatch: expected {}, got {}", .0.expected(), .0.got())]
InputLength(InputLength),
#[error("output shape mismatch: expected {:?}, got {:?}", .0.expected(), .0.got())]
OutputShape(OutputShape),
#[error("input contains a non-finite value at index {0}")]
NonFiniteInput(usize),
#[error(
"input value at index {0} is finite in f32 but overflows the model's f16 input \
domain (|x| > f16::MAX)"
)]
F16OverflowInput(usize),
#[error("mask has no active (true) frame")]
EmptyMask,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]
pub enum ExtractionPart {
#[display("raw_embeddings")]
RawEmbeddings,
#[display("segmentations")]
Segmentations,
#[display("count")]
Count,
#[display("num_chunks")]
NumChunks,
#[display("num_frames_per_chunk")]
NumFramesPerChunk,
#[display("chunks_sw")]
ChunksSw,
#[display("frames_sw")]
FramesSw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtractionLenMismatch {
part: ExtractionPart,
got: usize,
expected: usize,
}
impl ExtractionLenMismatch {
#[inline(always)]
pub const fn part(&self) -> ExtractionPart {
self.part
}
#[inline(always)]
pub const fn got(&self) -> usize {
self.got
}
#[inline(always)]
pub const fn expected(&self) -> usize {
self.expected
}
pub(crate) const fn new(part: ExtractionPart, got: usize, expected: usize) -> Self {
Self {
part,
got,
expected,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtractionGeometryOverflow {
part: ExtractionPart,
num_chunks: usize,
num_frames_per_chunk: usize,
}
impl ExtractionGeometryOverflow {
#[inline(always)]
pub const fn part(&self) -> ExtractionPart {
self.part
}
#[inline(always)]
pub const fn num_chunks(&self) -> usize {
self.num_chunks
}
#[inline(always)]
pub const fn num_frames_per_chunk(&self) -> usize {
self.num_frames_per_chunk
}
pub(crate) const fn new(
part: ExtractionPart,
num_chunks: usize,
num_frames_per_chunk: usize,
) -> Self {
Self {
part,
num_chunks,
num_frames_per_chunk,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InvalidSlidingWindow {
part: ExtractionPart,
window: crate::audio::speaker::window::SlidingWindow,
}
impl InvalidSlidingWindow {
#[inline(always)]
pub const fn part(&self) -> ExtractionPart {
self.part
}
#[inline(always)]
pub const fn window(&self) -> crate::audio::speaker::window::SlidingWindow {
self.window
}
pub(crate) const fn new(
part: ExtractionPart,
window: crate::audio::speaker::window::SlidingWindow,
) -> Self {
Self { part, window }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NonBinarySegmentation {
index: usize,
value: f64,
}
impl NonBinarySegmentation {
#[inline(always)]
pub const fn index(&self) -> usize {
self.index
}
#[inline(always)]
pub const fn value(&self) -> f64 {
self.value
}
#[inline(always)]
pub const fn slot(&self) -> usize {
self.index % crate::audio::speaker::segment::SEG_NUM_SLOTS
}
pub(crate) const fn new(index: usize, value: f64) -> Self {
Self { index, value }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ActiveSlotWithoutEmbedding {
chunk: usize,
slot: usize,
}
impl ActiveSlotWithoutEmbedding {
#[inline(always)]
pub const fn chunk(&self) -> usize {
self.chunk
}
#[inline(always)]
pub const fn slot(&self) -> usize {
self.slot
}
pub(crate) const fn new(chunk: usize, slot: usize) -> Self {
Self { chunk, slot }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CountNotSegmentationDerived {
frame: usize,
got: u8,
expected: u8,
}
impl CountNotSegmentationDerived {
#[inline(always)]
pub const fn frame(&self) -> usize {
self.frame
}
#[inline(always)]
pub const fn got(&self) -> u8 {
self.got
}
#[inline(always)]
pub const fn expected(&self) -> u8 {
self.expected
}
pub(crate) const fn new(frame: usize, got: u8, expected: u8) -> Self {
Self {
frame,
got,
expected,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ChunkPlacementMismatch {
chunk: usize,
aggregated: i64,
reconstructed: i64,
}
impl ChunkPlacementMismatch {
#[inline(always)]
pub const fn chunk(&self) -> usize {
self.chunk
}
#[inline(always)]
pub const fn aggregated(&self) -> i64 {
self.aggregated
}
#[inline(always)]
pub const fn reconstructed(&self) -> i64 {
self.reconstructed
}
pub(crate) const fn new(chunk: usize, aggregated: i64, reconstructed: i64) -> Self {
Self {
chunk,
aggregated,
reconstructed,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CollapsedFrameCenter {
frame: usize,
center: f64,
previous: f64,
}
impl CollapsedFrameCenter {
#[inline(always)]
pub const fn frame(&self) -> usize {
self.frame
}
#[inline(always)]
pub const fn center(&self) -> f64 {
self.center
}
#[inline(always)]
pub const fn previous(&self) -> f64 {
self.previous
}
pub(crate) const fn new(frame: usize, center: f64, previous: f64) -> Self {
Self {
frame,
center,
previous,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UncoveredLastChunk {
start_frame: i64,
required: usize,
got: usize,
}
impl UncoveredLastChunk {
#[inline(always)]
pub const fn start_frame(&self) -> i64 {
self.start_frame
}
#[inline(always)]
pub const fn required(&self) -> usize {
self.required
}
#[inline(always)]
pub const fn got(&self) -> usize {
self.got
}
pub(crate) const fn new(start_frame: i64, required: usize, got: usize) -> Self {
Self {
start_frame,
required,
got,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StepSamplesExceedsWindow {
step: u32,
window: usize,
}
impl StepSamplesExceedsWindow {
#[inline(always)]
pub const fn new(step: u32, window: usize) -> Self {
Self { step, window }
}
#[inline(always)]
pub const fn step(&self) -> u32 {
self.step
}
#[inline(always)]
pub const fn window(&self) -> usize {
self.window
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UnsupportedStepSamples {
step: u32,
required: u32,
}
impl UnsupportedStepSamples {
#[inline(always)]
pub const fn new(step: u32, required: u32) -> Self {
Self { step, required }
}
#[inline(always)]
pub const fn step(&self) -> u32 {
self.step
}
#[inline(always)]
pub const fn required(&self) -> u32 {
self.required
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FrameCountMismatch {
segmenter: usize,
embedder: usize,
}
impl FrameCountMismatch {
#[inline(always)]
pub const fn new(segmenter: usize, embedder: usize) -> Self {
Self {
segmenter,
embedder,
}
}
#[inline(always)]
pub const fn segmenter(&self) -> usize {
self.segmenter
}
#[inline(always)]
pub const fn embedder(&self) -> usize {
self.embedder
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ExtractError {
#[error("model error: {0}")]
Model(#[from] ModelError),
#[error("infer error: {0}")]
Infer(#[from] InferError),
#[error("samples is empty")]
EmptySamples,
#[error("step_samples must be > 0")]
ZeroStepSamples,
#[error(
"step_samples ({}) must not exceed SEG_CHUNK_SAMPLES ({})",
.0.step(),
.0.window()
)]
StepSamplesExceedsWindow(StepSamplesExceedsWindow),
#[error("onset ({0}) must be finite in (0.0, 1.0]")]
OnsetOutOfRange(f32),
#[error(
"step_samples ({}) is not supported by this source: its window stride is fixed at \
{} by the model graph",
.0.step(),
.0.required()
)]
UnsupportedStepSamples(UnsupportedStepSamples),
#[error(
"segmenter frame count ({}) does not match embedder mask frame count ({})",
.0.segmenter(),
.0.embedder()
)]
FrameCountMismatch(FrameCountMismatch),
#[error("extraction parts: {0} must be non-zero")]
ZeroExtractionDimension(ExtractionPart),
#[error(
"extraction parts: {} has length {} but the declared geometry requires {}",
.0.part(),
.0.got(),
.0.expected()
)]
ExtractionLenMismatch(ExtractionLenMismatch),
#[error(
"extraction parts: the length {} requires overflows usize (num_chunks {}, \
num_frames_per_chunk {}, num_speakers {})",
.0.part(),
.0.num_chunks(),
.0.num_frames_per_chunk(),
crate::audio::speaker::segment::SEG_NUM_SLOTS
)]
ExtractionGeometryOverflow(ExtractionGeometryOverflow),
#[error(
"extraction parts: {} is not a usable timing grid (start {}, duration {}, step {}) — \
start must be finite and duration/step must be finite and > 0",
.0.part(),
.0.window().start(),
.0.window().duration(),
.0.window().step()
)]
InvalidSlidingWindow(InvalidSlidingWindow),
#[error(
"chunk {} is placed at output frame {} by the count aggregation but at \
frame {} by diaric's reconstruction — the two grids must agree, or the count selects \
frames the activations never reach",
.0.chunk(),
.0.aggregated(),
.0.reconstructed()
)]
MisalignedChunkPlacement(ChunkPlacementMismatch),
#[error(
"extraction parts: output frame {}'s center ({:e}) must be finite and strictly later than \
the previous frame's ({:e}) — this frames_sw collapses adjacent frame centers, so a span \
closes at duration zero",
.0.frame(),
.0.center(),
.0.previous()
)]
CollapsedFrameCenter(CollapsedFrameCenter),
#[error(
"extraction geometry: the derived output grid is {} frames, but the last chunk starts at \
frame {} and needs {} — diaric::reconstruct would drop every cell past the end",
.0.got(),
.0.start_frame(),
.0.required()
)]
UncoveredLastChunk(UncoveredLastChunk),
#[error(
"extraction parts: {} declares a step ({:e}) that narrows to {:e} in f32 — the online speech \
duration is an f32 product, so the step must stay finite and > 0 through that narrowing",
.0.part(),
.0.window().step(),
.0.window().step() as f32
)]
FrameStepNotRepresentableInF32(InvalidSlidingWindow),
#[error(
"extraction parts: segmentations[{}] is {} (slot {}) — every cell must be exactly 0.0 or \
1.0, because the offline backend sums these magnitudes where the online backend counts \
nonzero frames",
.0.index(),
.0.value(),
.0.slot()
)]
NonBinarySegmentation(NonBinarySegmentation),
#[error(
"chunk {} slot {} has an active segmentations column but its raw_embeddings \
row cannot reach the clustering both backends run (non-finite, L2 norm below \
PLDA's 0.01 floor, a norm the online engine's f32 narrowing turns into inf, \
or a centered norm PLDA's projection refuses)",
.0.chunk(),
.0.slot()
)]
ActiveSlotWithoutEmbedding(ActiveSlotWithoutEmbedding),
#[error(
"diaric's PLDA transform could not be built, so an active slot's raw \
embedding cannot be validated against the projection the offline backend runs"
)]
PldaTransformUnavailable,
#[error(
"extraction parts: count[{}] is {} but the supplied segmentations derive {} at that output \
frame",
.0.frame(),
.0.got(),
.0.expected()
)]
CountNotSegmentationDerived(CountNotSegmentationDerived),
#[error(
"extraction parts: raw_embeddings[{}] is not finite (chunk {}, slot {}, dimension {}); the \
offline backend rejects the whole matrix while the online backend never reads an inactive \
slot's row",
.0,
.0 / (crate::audio::speaker::segment::SEG_NUM_SLOTS * crate::audio::speaker::embed::EMBEDDING_DIM),
(.0 / crate::audio::speaker::embed::EMBEDDING_DIM) % crate::audio::speaker::segment::SEG_NUM_SLOTS,
.0 % crate::audio::speaker::embed::EMBEDDING_DIM
)]
NonFiniteRawEmbedding(usize),
#[error(
"extraction parts: the declared geometry derives {} output frames, above the \
MAX_OUTPUT_FRAMES cap ({})",
.0,
crate::audio::speaker::extract::MAX_OUTPUT_FRAMES
)]
OutputFrameCountTooLarge(usize),
#[error(
"extraction geometry: the declared chunk grid derives {} bytes of extraction \
tensors, above the MAX_EXTRACTION_TENSOR_BYTES cap ({})",
.0,
crate::audio::speaker::extract::MAX_EXTRACTION_TENSOR_BYTES
)]
ExtractionTensorBytesTooLarge(usize),
#[error(
"extraction geometry: the declared chunk grid is {} chunks, above the \
MAX_EXTRACTION_CHUNKS cap ({})",
.0,
crate::audio::speaker::extract::MAX_EXTRACTION_CHUNKS
)]
ExtractionChunkCountTooLarge(usize),
#[error(
"num_output_frames overflows usize (chunk_duration / frame_step too large \
to represent or saturated past usize::MAX)"
)]
OutputFrameCountOverflow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProfileLength {
got: usize,
expected: usize,
}
impl ProfileLength {
#[inline(always)]
pub const fn new(got: usize, expected: usize) -> Self {
Self { got, expected }
}
#[inline(always)]
pub const fn got(&self) -> usize {
self.got
}
#[inline(always)]
pub const fn expected(&self) -> usize {
self.expected
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ScoringMismatch {
side: crate::audio::speaker::calibrate::Scoring,
other: crate::audio::speaker::calibrate::Scoring,
}
impl ScoringMismatch {
#[inline(always)]
pub const fn new(
side: crate::audio::speaker::calibrate::Scoring,
other: crate::audio::speaker::calibrate::Scoring,
) -> Self {
Self { side, other }
}
#[inline(always)]
pub const fn side(&self) -> crate::audio::speaker::calibrate::Scoring {
self.side
}
#[inline(always)]
pub const fn other(&self) -> crate::audio::speaker::calibrate::Scoring {
self.other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CalibrationMismatch {
expected: crate::audio::speaker::calibrate::CalibrationId,
enrolled: crate::audio::speaker::calibrate::CalibrationId,
probe: crate::audio::speaker::calibrate::CalibrationId,
}
impl CalibrationMismatch {
#[inline(always)]
pub const fn new(
expected: crate::audio::speaker::calibrate::CalibrationId,
enrolled: crate::audio::speaker::calibrate::CalibrationId,
probe: crate::audio::speaker::calibrate::CalibrationId,
) -> Self {
Self {
expected,
enrolled,
probe,
}
}
#[inline(always)]
pub const fn expected(&self) -> crate::audio::speaker::calibrate::CalibrationId {
self.expected
}
#[inline(always)]
pub const fn enrolled(&self) -> crate::audio::speaker::calibrate::CalibrationId {
self.enrolled
}
#[inline(always)]
pub const fn probe(&self) -> crate::audio::speaker::calibrate::CalibrationId {
self.probe
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CohortSelection {
selected: usize,
considered: usize,
}
impl CohortSelection {
#[inline(always)]
pub const fn new(selected: usize, considered: usize) -> Self {
Self {
selected,
considered,
}
}
#[inline(always)]
pub const fn selected(&self) -> usize {
self.selected
}
#[inline(always)]
pub const fn considered(&self) -> usize {
self.considered
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
#[non_exhaustive]
pub enum ScoreNormRefusal {
#[error(
"no usable cohort score: the cohort was empty, or excluding this speaker's own entries \
removed every member"
)]
EmptyCohort,
#[error(
"only {} of {} cohort score(s) survived selection; AS-Norm needs at least {}",
.0.selected(),
.0.considered(),
diaric::score_norm::MIN_COHORT_SCORES
)]
CohortTooSmall(CohortSelection),
#[error(
"the {} selected of {} cohort score(s) do not spread past the configured `min_deviation` \
floor; the cohort does not discriminate",
.0.selected(),
.0.considered()
)]
DegenerateCohort(CohortSelection),
#[error("the configured `min_deviation` floor is not finite and greater than zero")]
InvalidMinDeviation,
#[error("a score handed to the AS-Norm arithmetic was not finite")]
NonFiniteScore,
#[error("a value the AS-Norm arithmetic produced was not finite")]
NonFiniteResult,
#[error(
"the two sides' z-scores cancel, so the calibrated score would be made of its own rounding \
rather than of the trial"
)]
ZScoreCancellation,
}
impl ScoreNormRefusal {
pub(crate) fn translate(e: diaric::score_norm::Error, considered: usize) -> Self {
use diaric::score_norm::Error as Refused;
match e {
Refused::EmptyCohort => Self::EmptyCohort,
Refused::CohortTooSmall(too_small) => {
Self::CohortTooSmall(CohortSelection::new(too_small.available(), considered))
}
Refused::DegenerateDeviation(degenerate) => {
Self::DegenerateCohort(CohortSelection::new(degenerate.selected(), considered))
}
Refused::InvalidMinDeviation(_) => Self::InvalidMinDeviation,
Refused::NonFiniteScore(_) => Self::NonFiniteScore,
Refused::NonFiniteResult(_) => Self::NonFiniteResult,
Refused::ZScoreCancellation(_) => Self::ZScoreCancellation,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CalibrateError {
#[error(
"voice profile: raw embedding row is {} elements, expected {}",
.0.got(),
.0.expected()
)]
ProfileLength(ProfileLength),
#[error("voice profile: the prepared {0:?} vector has no usable direction")]
DegenerateProfile(crate::audio::speaker::calibrate::Scoring),
#[error("voice profile: plda: {0}")]
Plda(#[from] diaric::plda::Error),
#[error(
"diaric's PLDA transform could not be built, so a raw embedding cannot be \
projected into the space `Scoring::PldaCosine` scores in"
)]
PldaTransformUnavailable,
#[error(
"voice profile: a {:?} profile cannot be scored against a {:?} one",
.0.side(),
.0.other()
)]
ScoringMismatch(ScoringMismatch),
#[error(
"AS-Norm: the enrolment side was taken under {:?} and the probe side under \
{:?}, but this trial is {:?}'s; two z-scores are commensurable only when \
both sides came from ONE calibration",
.0.enrolled(),
.0.probe(),
.0.expected()
)]
CalibrationMismatch(CalibrationMismatch),
#[error("voice profile: {0}")]
ScoreNorm(#[from] ScoreNormRefusal),
#[error(
"AS-Norm: this speaker token was minted by a different cohort, so it names nothing to \
exclude here; a side taken under it would score this speaker against their own entries"
)]
ForeignSpeaker,
}
#[cfg(test)]
mod tests;