#[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 AlignerError {
#[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; \
it sends `waveform` 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; a stateful graph needs an `MLState` on every prediction"
)]
UnsatisfiableState(String),
#[error("alignment seam construction failed: {0}")]
Seam(#[from] asry::emissions::EmissionsError),
#[error(
"the contract's front end ({}-sample receptive field, {}-sample stride) makes {} frames \
of the model's {}-sample window, but the model declares {}: the geometry is not this \
model's",
.0.geometry().receptive_field(),
.0.geometry().stride(),
.0.derived(),
.0.window(),
.0.declared()
)]
FrameCountMismatch(FrameCountMismatch),
#[error(
"the contract names id {} the CTC blank, but the vocabulary's {} entries hold ids 0..{}; \
the blank must be one of the table's own ids",
.0.blank(),
.0.entries(),
.0.entries()
)]
BlankOutOfVocabulary(BlankOutOfVocabulary),
#[error("the contract's tokenization cannot be honoured: {0}")]
Tokenization(TokenizationError),
#[error(
"the seam reserves the columns {:?} where the contract declares {:?} non-lexical: a declared \
column the seam does not reserve can be scored for a wildcard, and a reserved column the \
contract calls lexical is never spelled",
.0.reserved(),
.0.declared()
)]
ReservedSetMismatch(ReservedSetMismatch),
#[error(
"a {}-class head's log-probabilities cannot be checked for normalization: fp16 rounding over \
that many classes can move a genuine frame's logsumexp by up to {:.3}, too near an \
unnormalized frame's; the check separates the two only up to {} classes. State the head's \
output as logits: asry then normalizes it, and normalizing a log-softmax output again \
changes nothing",
.0.vocab_size(),
crate::audio::align::encode::log_prob_sum_tolerance(.0.vocab_size_nonzero()),
.0.widest()
)]
UnprovableNormalization(UnprovableNormalization),
#[error(
"the vocabulary names {} classes but the model's CTC head scores {} per frame; \
pair the model with the vocabulary that ships beside it",
.0.vocabulary(),
.0.model()
)]
VocabularyMismatch(VocabularyMismatch),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VocabularyMismatch {
vocabulary: usize,
model: usize,
}
impl VocabularyMismatch {
#[inline(always)]
pub const fn new(vocabulary: usize, model: usize) -> Self {
Self { vocabulary, model }
}
#[inline(always)]
pub const fn vocabulary(&self) -> usize {
self.vocabulary
}
#[inline(always)]
pub const fn model(&self) -> usize {
self.model
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReservedSetMismatch {
declared: Vec<usize>,
reserved: Vec<usize>,
}
impl ReservedSetMismatch {
#[must_use]
pub const fn new(declared: Vec<usize>, reserved: Vec<usize>) -> Self {
Self { declared, reserved }
}
#[inline]
pub fn declared(&self) -> &[usize] {
&self.declared
}
#[inline]
pub fn reserved(&self) -> &[usize] {
&self.reserved
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameCountMismatch {
geometry: crate::audio::align::acoustic::AcousticGeometry,
window: usize,
declared: usize,
}
impl FrameCountMismatch {
#[inline(always)]
pub const fn new(
geometry: crate::audio::align::acoustic::AcousticGeometry,
window: usize,
declared: usize,
) -> Self {
Self {
geometry,
window,
declared,
}
}
#[inline(always)]
pub const fn geometry(&self) -> crate::audio::align::acoustic::AcousticGeometry {
self.geometry
}
#[inline(always)]
pub const fn window(&self) -> usize {
self.window
}
#[inline(always)]
pub const fn declared(&self) -> usize {
self.declared
}
#[inline(always)]
pub const fn derived(&self) -> usize {
self.geometry.frames(self.window)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnprovableNormalization {
vocab_size: core::num::NonZeroUsize,
widest: usize,
}
impl UnprovableNormalization {
#[inline(always)]
pub const fn new(vocab_size: core::num::NonZeroUsize, widest: usize) -> Self {
Self { vocab_size, widest }
}
#[inline(always)]
pub const fn vocab_size(&self) -> usize {
self.vocab_size.get()
}
#[inline(always)]
pub const fn vocab_size_nonzero(&self) -> core::num::NonZeroUsize {
self.vocab_size
}
#[inline(always)]
pub const fn widest(&self) -> usize {
self.widest
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlankOutOfVocabulary {
blank: u32,
entries: usize,
}
impl BlankOutOfVocabulary {
#[inline(always)]
pub const fn new(blank: u32, entries: usize) -> Self {
Self { blank, entries }
}
#[inline(always)]
pub const fn blank(&self) -> u32 {
self.blank
}
#[inline(always)]
pub const fn entries(&self) -> usize {
self.entries
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum GeometryError {
#[error(
"the front end takes {0} Hz audio, and asry's seam analyses 16000 Hz audio only: its spans, \
its clock and its strides count 16 kHz samples"
)]
SampleRate(u32),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TokenizationError {
#[error(
"the model delimits words with {0:?}, and a contract states the `|` delimiter, the space, or \
none"
)]
UnsupportedDelimiter(String),
#[error(
"the table spells the whitespace token {0:?}, which the contract does not state as its word \
delimiter: asry splits words at whitespace and never looks one up, so its column would \
never be read, and beside a stated `|` it does not say which of the two delimits the words"
)]
WhitespaceToken(String),
#[error("the table does not spell the word delimiter the contract states")]
DelimiterMissing,
#[error(
"the contract states a word delimiter, but the normalizer delimits no words: the model's \
delimiter frames between words would be read as the letters beside them"
)]
DelimiterUnused,
#[error(
"the normalizer delimits words, and the contract says the model has no word delimiter to \
put between them"
)]
DelimiterRequired,
#[error(
"the contract spells letters in upper case, but the table also spells {0:?}: asry looks \
every ASCII letter up in upper case and would never read its column"
)]
UpperWithLowercase(char),
#[error(
"the contract looks letters up as written, but the table's letters are uppercase only: an \
upper-case table, which `LetterCase::Upper` states"
)]
ProjectedAsWritten,
#[error(
"the table spells the lexical token {0:?} as other than one Unicode scalar value: asry \
looks a text up one character at a time and would never read this token's column; declare \
it a special if it is not a letter, or use a tokenizer-driven seam for a model that truly \
tokenizes in wider units"
)]
NotCharacterLevel(String),
#[error(
"the contract declares the table's empty token, id {0}, a special, and nothing else reserves \
its column: the `tokenizers` crate drops an empty added token when it parses the tokenizer \
document, so asry would not reserve the column and a wildcard could be scored there"
)]
EmptySpecial(usize),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VocabularyError {
#[error(transparent)]
Read(VocabularyRead),
#[error("a vocabulary is a JSON object mapping each token to its id: {0}")]
Parse(String),
#[error(
"the vocabulary names the token {0:?} twice; a repeated key leaves its column to whichever \
JSON reader reads it"
)]
DuplicateToken(String),
#[error(
"no token has id {}: a vocabulary of {} entries names every id in 0..{} exactly once, \
one per class of its model's CTC head",
.0.id(),
.0.entries(),
.0.entries()
)]
MissingId(MissingId),
#[error("the vocabulary names no token; a CTC head has at least one class, its blank")]
Empty,
}
#[derive(Debug, thiserror::Error)]
#[error("failed to read the vocabulary `{path}`: {source}")]
pub struct VocabularyRead {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
}
impl VocabularyRead {
#[inline(always)]
pub const fn new(path: std::path::PathBuf, source: std::io::Error) -> Self {
Self { path, source }
}
#[inline(always)]
pub fn path(&self) -> &std::path::Path {
&self.path
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MissingId {
id: usize,
entries: usize,
}
impl MissingId {
#[inline(always)]
pub const fn new(id: usize, entries: usize) -> Self {
Self { id, entries }
}
#[inline(always)]
pub const fn id(&self) -> usize {
self.id
}
#[inline(always)]
pub const fn entries(&self) -> usize {
self.entries
}
}
#[derive(Debug, Clone)]
pub struct InputTooLong {
got: usize,
max: usize,
}
impl InputTooLong {
#[inline(always)]
pub const fn new(got: usize, max: usize) -> Self {
Self { got, max }
}
#[inline(always)]
pub const fn got(&self) -> usize {
self.got
}
#[inline(always)]
pub const fn max(&self) -> usize {
self.max
}
}
#[derive(Debug, Clone)]
pub struct CorruptEmissions {
compute: crate::ComputeUnits,
band: crate::audio::align::acoustic::SentinelBand,
min: f32,
cells: usize,
total: usize,
}
impl CorruptEmissions {
#[inline(always)]
pub const fn new(
compute: crate::ComputeUnits,
band: crate::audio::align::acoustic::SentinelBand,
min: f32,
cells: usize,
total: usize,
) -> Self {
Self {
compute,
band,
min,
cells,
total,
}
}
#[inline(always)]
pub const fn compute(&self) -> crate::ComputeUnits {
self.compute
}
#[inline(always)]
pub const fn band(&self) -> crate::audio::align::acoustic::SentinelBand {
self.band
}
#[inline(always)]
pub const fn min(&self) -> f32 {
self.min
}
#[inline(always)]
pub const fn cells(&self) -> usize {
self.cells
}
#[inline(always)]
pub const fn total(&self) -> usize {
self.total
}
}
#[derive(Debug, Clone)]
pub struct UnnormalizedEmissions {
compute: crate::ComputeUnits,
row: usize,
logsumexp: f64,
tolerance: f64,
}
impl UnnormalizedEmissions {
#[inline(always)]
pub const fn new(
compute: crate::ComputeUnits,
row: usize,
logsumexp: f64,
tolerance: f64,
) -> Self {
Self {
compute,
row,
logsumexp,
tolerance,
}
}
#[inline(always)]
pub const fn compute(&self) -> crate::ComputeUnits {
self.compute
}
#[inline(always)]
pub const fn row(&self) -> usize {
self.row
}
#[inline(always)]
pub const fn logsumexp(&self) -> f64 {
self.logsumexp
}
#[inline(always)]
pub const fn tolerance(&self) -> f64 {
self.tolerance
}
}
#[derive(Debug, Clone)]
pub struct DecisionLanguage {
requested: asry::Lang,
found: asry::Lang,
}
impl DecisionLanguage {
#[inline(always)]
pub const fn new(requested: asry::Lang, found: asry::Lang) -> Self {
Self { requested, found }
}
#[inline(always)]
pub const fn requested(&self) -> &asry::Lang {
&self.requested
}
#[inline(always)]
pub const fn found(&self) -> &asry::Lang {
&self.found
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ForeignResolution {
made_by: crate::audio::align::registry::SetId,
asked: crate::audio::align::registry::SetId,
}
impl ForeignResolution {
#[inline(always)]
pub const fn new(
made_by: crate::audio::align::registry::SetId,
asked: crate::audio::align::registry::SetId,
) -> Self {
Self { made_by, asked }
}
#[inline(always)]
pub const fn made_by(&self) -> crate::audio::align::registry::SetId {
self.made_by
}
#[inline(always)]
pub const fn asked(&self) -> crate::audio::align::registry::SetId {
self.asked
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MisroutedResolution {
route: crate::audio::align::registry::AlignmentBinding,
decided: bool,
}
impl MisroutedResolution {
#[inline(always)]
pub const fn new(route: crate::audio::align::registry::AlignmentBinding, decided: bool) -> Self {
Self { route, decided }
}
#[inline(always)]
pub const fn route(&self) -> &crate::audio::align::registry::AlignmentBinding {
&self.route
}
#[inline(always)]
pub const fn decided(&self) -> bool {
self.decided
}
const fn shape(&self) -> &'static str {
if self.decided {
"holds an aligner's decisions"
} else {
"was decided where no aligner read the text"
}
}
}
#[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)]
pub struct Refusal {
events: Vec<RefusedOov>,
}
impl Refusal {
#[inline(always)]
pub const fn new(events: Vec<RefusedOov>) -> Self {
Self { events }
}
#[inline(always)]
pub fn events(&self) -> &[RefusedOov] {
&self.events
}
pub(crate) fn under(self, language: &asry::Lang) -> Self {
Self {
events: self
.events
.into_iter()
.map(|event| event.under(language))
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefusedOov {
kind: asry::emissions::OovKind,
char: Option<char>,
char_index: usize,
word_index: usize,
language: asry::Lang,
}
impl RefusedOov {
pub(crate) fn detected(event: &asry::emissions::OovEvent) -> Self {
Self {
kind: event.kind().clone(),
char: event.char(),
char_index: event.char_index(),
word_index: event.word_index(),
language: event.language().clone(),
}
}
fn under(self, language: &asry::Lang) -> Self {
Self {
language: language.clone(),
..self
}
}
#[inline(always)]
pub const fn kind(&self) -> &asry::emissions::OovKind {
&self.kind
}
#[inline(always)]
pub const fn char(&self) -> Option<char> {
self.char
}
#[inline(always)]
pub const fn char_index(&self) -> usize {
self.char_index
}
#[inline(always)]
pub const fn word_index(&self) -> usize {
self.word_index
}
#[inline(always)]
pub const fn language(&self) -> &asry::Lang {
&self.language
}
}
impl core::fmt::Display for Refusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.events.is_empty() {
return f.write_str("no position");
}
for (at, event) in self.events.iter().enumerate() {
if at > 0 {
f.write_str(", ")?;
}
match event.char() {
Some(symbol) => write!(f, "{symbol:?} (word {})", event.word_index())?,
None => write!(f, "a boundary mark (word {})", event.word_index())?,
}
}
Ok(())
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum AlignError {
#[error(transparent)]
Alignment(#[from] asry::emissions::EmissionsError),
#[error("the OOV policy refused this chunk at {0}; no word timings were produced")]
Refused(Refusal),
#[error("no alignment path for this chunk: {0}")]
NoAlignmentPath(asry::emissions::EmissionsFailure),
#[error(transparent)]
Span(#[from] asry::emissions::SpanError),
#[error("prediction failed: {0}")]
Prediction(#[from] crate::PredictionError),
#[error("tensor failed: {0}")]
Tensor(#[from] crate::TensorError),
#[error("input exceeds encoder window: {} samples > {} samples", .0.got(), .0.max())]
InputTooLong(InputTooLong),
#[error(
"encoder emissions are not log-probabilities: {} of {} cells are at or below {} \
(min = {}), in the band ({:?}) the model's contract says it emits in place of a \
log-probability — for the staged base960h, a saturated fp16 `log(0)`. The encoder was \
scheduled on {:?}: an fp16 `softmax` then `log` tail underflows to it on the Apple Neural \
Engine (the staged base960h's does, and its word timings shift by hundreds of \
milliseconds). Load the encoder on \
`coremlit::audio::align::encode::DEFAULT_ENCODER_COMPUTE` (the default) — or re-convert \
the model with a fused `log_softmax` tail.",
.0.cells(),
.0.total(),
.0.band().ceiling(),
.0.min(),
.0.band(),
.0.compute(),
)]
CorruptEmissions(CorruptEmissions),
#[error(
"the encoder's emissions came back with shape {:?}, not the declared {:?}; a tensor of \
any other shape cannot be read as frames of vocabulary classes",
.0.got(),
.0.expected()
)]
OutputShape(OutputShape),
#[error(
"encoder emissions are not normalized log-probabilities: frame {} has logsumexp \
{} over the vocab axis (tolerance ±{}), but a CTC log-probability frame \
sums to 1 in probability space so its logsumexp is 0. This emission tensor carries raw \
logits or another un-normalized distribution, not the log-softmaxed output alignkit's \
encoder contract requires — most likely the model artifact was swapped for a raw-logit CTC \
head (the standard wav2vec2 export). The encoder was scheduled on {:?}.",
.0.row(),
.0.logsumexp(),
.0.tolerance(),
.0.compute()
)]
UnnormalizedEmissions(UnnormalizedEmissions),
#[error(
"the OOV decisions were made for language {:?} but the chunk is being aligned for \
{:?}; decide the detection of the language you request (the one \
`AlignmentSet::detect_oov` was asked for)",
.0.found(),
.0.requested()
)]
DecisionLanguage(DecisionLanguage),
#[error(
"this resolution was made by {}, and {} was asked to apply it: OOV decisions answer the \
alignment set whose detection they were decided from; detect and decide the text with the \
set that aligns it",
.0.made_by(),
.0.asked()
)]
ForeignResolution(ForeignResolution),
#[error(
"this resolution {}, and this request takes the route {:?}: a resolution applies only on \
the route its detection took",
.0.shape(),
.0.route()
)]
MisroutedResolution(MisroutedResolution),
#[error(
"no aligner registered for language {0:?}, no `Any` fallback, and the registry miss \
policy is `Error`"
)]
LanguageUnsupported(asry::Lang),
}
#[cfg(test)]
mod tests;