use smol_str::SmolStr;
use super::encode::{LogProbsError, LogProbsShapeError, LogProbsValueError};
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct EmissionsFailure {
message: SmolStr,
}
impl EmissionsFailure {
#[must_use]
pub const fn new(message: SmolStr) -> Self {
Self { message }
}
#[must_use]
pub fn message(&self) -> &SmolStr {
&self.message
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EmissionsError {
#[error(transparent)]
Shape(LogProbsShapeError),
#[error(transparent)]
Value(LogProbsValueError),
#[error("numeric failure: {0}")]
Numeric(EmissionsFailure),
#[error("invalid configuration: {0}")]
Config(EmissionsFailure),
#[error("encoder stride mismatch: {0}")]
StrideMismatch(EmissionsFailure),
#[error("encoder vocab dim mismatch: {0}")]
VocabMismatch(EmissionsFailure),
#[error("prepared chunk belongs to a different aligner: {0}")]
AlignerMismatch(EmissionsFailure),
#[error("non-finite audio: {0}")]
NonFiniteAudio(EmissionsFailure),
#[error("normalization failed: {0}")]
Normalization(EmissionsFailure),
#[error("tokenization failed: {0}")]
Tokenization(EmissionsFailure),
#[error("semantic out-of-vocabulary: {0}")]
SemanticOutOfVocab(EmissionsFailure),
#[error("no alignment path: {0}")]
NoAlignmentPath(EmissionsFailure),
#[error("path budget exceeded: {0}")]
PathBudget(EmissionsFailure),
#[error("aborted before completing: {0}")]
Aborted(EmissionsFailure),
}
impl From<LogProbsError> for EmissionsError {
fn from(err: LogProbsError) -> Self {
match err {
LogProbsError::Shape(e) => Self::Shape(e),
LogProbsError::Value(e) => Self::Value(e),
}
}
}
impl EmissionsError {
pub(crate) fn into_work_failure(
self,
language: &crate::types::Lang,
) -> crate::types::WorkFailure {
use crate::types::{AlignmentError, AlignmentFailure, WorkFailure};
let failure = |message: SmolStr| AlignmentFailure::new(message, language.clone());
let inner = match self {
Self::Shape(e) => AlignmentError::ModelInference(failure(e.to_string().into())),
Self::Value(e) => AlignmentError::ModelInference(failure(e.to_string().into())),
Self::Numeric(f)
| Self::Config(f)
| Self::StrideMismatch(f)
| Self::VocabMismatch(f)
| Self::AlignerMismatch(f)
| Self::NonFiniteAudio(f) => AlignmentError::ModelInference(failure(f.message)),
Self::Tokenization(f) => AlignmentError::Tokenization(failure(f.message)),
Self::Normalization(f) => AlignmentError::Normalization(failure(f.message)),
Self::SemanticOutOfVocab(f) => AlignmentError::SemanticOutOfVocab(failure(f.message)),
Self::NoAlignmentPath(f) | Self::PathBudget(f) => {
AlignmentError::NoAlignmentPath(failure(f.message))
}
Self::Aborted(f) => AlignmentError::Aborted(failure(f.message)),
};
WorkFailure::Alignment(inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_is_backend_neutral() {
let f = || EmissionsFailure::new("diagnostic detail".into());
let cases = [
EmissionsError::Numeric(f()),
EmissionsError::Config(f()),
EmissionsError::Tokenization(f()),
EmissionsError::SemanticOutOfVocab(f()),
EmissionsError::NoAlignmentPath(f()),
EmissionsError::PathBudget(f()),
EmissionsError::Aborted(f()),
];
for case in &cases {
let s = case.to_string();
assert!(s.contains("diagnostic detail"), "message dropped: {s}");
assert!(!s.contains("ORT"), "leaked ORT: {s}");
assert!(!s.contains("worker"), "leaked worker: {s}");
assert!(!s.contains("pool"), "leaked pool: {s}");
assert!(!s.contains("Event::Error"), "leaked Event::Error: {s}");
assert!(
!s.contains("ASR text preserved"),
"leaked ASR-text framing: {s}"
);
}
}
#[test]
fn lifts_logprobs_error() {
use super::super::encode::LogProbsTV;
let Err(shape) = LogProbsTV::new(2, 0, Vec::new()) else {
panic!("v == 0 must be a shape error");
};
assert!(matches!(
EmissionsError::from(shape),
EmissionsError::Shape(_)
));
let Err(value) = LogProbsTV::new(1, 1, vec![1.0_f32]) else {
panic!("a positive log-prob must be a value error");
};
assert!(matches!(
EmissionsError::from(value),
EmissionsError::Value(_)
));
}
}