use std::ops::Range;
use sicada::arc::{Arc, ArcLabel, ArcStateId};
use sicada::data_structures::bit_set::DenseBitSet;
use sicada::error::OpenFstError;
use sicada::fst::{Fst, MutableFst};
use sicada::fsts::vector_fst::VectorFst;
use sicada::properties::K_FST_PROPERTIES;
use sicada::weight::Weight;
use crate::dense::{DenseFst, FromScore};
use crate::trellis::{Path, ReversibleTrellis, Step, Trellis, best_path};
const SOUNDS: [bool; 4] = [false, true, true, false];
#[derive(Debug, Clone, PartialEq)]
pub struct AlignChain {
phones: Vec<u32>,
skips: Vec<f32>,
blank: u32,
}
impl AlignChain {
pub fn new(phones: impl Into<Vec<u32>>) -> Self {
let phones = phones.into();
Self {
skips: vec![f32::INFINITY; phones.len()],
phones,
blank: 0,
}
}
pub fn with_skip_costs(mut self, costs: &[f32]) -> Result<Self, OpenFstError> {
if costs.len() != self.phones.len() {
return Err(OpenFstError::InvalidOperation(format!(
"AlignChain: {} skip costs for {} phones",
costs.len(),
self.phones.len()
)));
}
if let Some(bad) = costs.iter().position(|cost| cost.is_nan() || *cost < 0.0) {
return Err(OpenFstError::InvalidOperation(format!(
"AlignChain: the skip cost at position {bad} is {}, and a skip that pays for \
itself would drop the reference rather than align it",
costs[bad]
)));
}
self.skips.copy_from_slice(costs);
Ok(self)
}
pub fn with_uniform_skip_cost(self, cost: f32) -> Result<Self, OpenFstError> {
let costs = vec![cost; self.phones.len()];
self.with_skip_costs(&costs)
}
pub fn with_blank(mut self, column: u32) -> Self {
self.blank = column;
self
}
#[inline(always)]
pub fn num_phones(&self) -> usize {
self.phones.len()
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.phones.is_empty()
}
#[inline(always)]
pub fn phones(&self) -> &[u32] {
&self.phones
}
#[inline(always)]
pub fn skip_costs(&self) -> &[f32] {
&self.skips
}
#[inline(always)]
pub fn blank(&self) -> u32 {
self.blank
}
pub fn against<'a, A>(
&'a self,
dense: &'a DenseFst<'a, A>,
) -> Result<ChainTrellis<'a, A>, OpenFstError>
where
A: Arc,
A::Weight: FromScore,
{
self.check_columns(dense.num_symbols())?;
Ok(ChainTrellis { chain: self, dense })
}
pub const HOLD_BLANK: u8 = 0;
pub const HOLD_PHONE: u8 = 1;
pub const COMMIT: u8 = 2;
pub const SKIP: u8 = 3;
#[inline(always)]
pub const fn sounds(code: u8) -> bool {
SOUNDS[code as usize]
}
#[inline(always)]
fn column(&self, position: Option<usize>) -> u32 {
match position {
Some(p) => self.phones[p],
None => self.blank,
}
}
pub(crate) fn check_columns(&self, num_symbols: usize) -> Result<(), OpenFstError> {
let named = std::iter::once((None, self.blank)).chain(
self.phones
.iter()
.enumerate()
.map(|(p, &column)| (Some(p), column)),
);
for (position, column) in named {
if column as usize >= num_symbols {
let what = match position {
Some(p) => format!("position {p}"),
None => "the blank".to_string(),
};
return Err(OpenFstError::InvalidOperation(format!(
"AlignChain: {what} is column {column}, which a {num_symbols}-symbol acoustic \
matrix does not have"
)));
}
}
Ok(())
}
pub fn to_fst<A: Arc>(&self, label_offset: i64) -> Result<VectorFst<A>, OpenFstError>
where
A::Weight: FromScore,
{
if label_offset < 1 {
return Err(OpenFstError::InvalidOperation(
"AlignChain::to_fst: column 0 would be epsilon, which consumes no frame".into(),
));
}
let fits = |value: i64, what: &str| -> Result<A::Label, OpenFstError> {
A::Label::from_i64(value).ok_or_else(|| {
OpenFstError::InvalidOperation(format!(
"AlignChain::to_fst: {what} {value} does not fit the arc's label type"
))
})
};
let input = |column: u32| fits(label_offset + column as i64, "input label");
let n = self.phones.len();
let sounds = |position: Option<usize>| {
let value = match position {
Some(p) => p as i64 + 1,
None => n as i64 + 1,
};
fits(value, "output label")
};
let mut fst: VectorFst<A> = VectorFst::new();
fst.reserve_states(n + 1);
for _ in 0..=n {
fst.add_state();
}
fst.set_start(A::StateId::from_usize(0));
fst.set_final(A::StateId::from_usize(n), A::Weight::one());
let blank = input(self.blank)?;
let silent = sounds(None)?;
for i in 0..=n {
let from = A::StateId::from_usize(i);
let to = A::StateId::from_usize((i + 1).min(n));
fst.add_arc(from, A::new(blank, silent, A::Weight::one(), from));
if i > 0 {
let held = input(self.phones[i - 1])?;
fst.add_arc(
from,
A::new(held, sounds(Some(i - 1))?, A::Weight::one(), from),
);
}
if i < n {
let next = input(self.phones[i])?;
fst.add_arc(from, A::new(next, sounds(Some(i))?, A::Weight::one(), to));
let cost = self.skips[i];
if cost.is_finite() {
fst.add_arc(from, A::new(blank, silent, A::Weight::from_cost(cost), to));
}
}
}
fst.properties(K_FST_PROPERTIES, true);
Ok(fst)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Alignment {
sounding: Vec<u32>,
num_phones: usize,
cost: f32,
}
impl Alignment {
#[inline(always)]
pub fn num_frames(&self) -> usize {
self.sounding.len()
}
#[inline(always)]
pub fn num_phones(&self) -> usize {
self.num_phones
}
#[inline(always)]
pub fn sounding(&self, frame: usize) -> Option<usize> {
(self.sounding[frame] as usize).checked_sub(1)
}
pub fn frames(&self) -> impl ExactSizeIterator<Item = Option<usize>> + '_ {
self.sounding.iter().map(|&k| (k as usize).checked_sub(1))
}
#[inline(always)]
pub fn cost(&self) -> f32 {
self.cost
}
pub fn spans(&self) -> Vec<Option<Range<usize>>> {
let mut spans = vec![None; self.num_phones];
for (frame, &sounding) in self.sounding.iter().enumerate() {
let Some(position) = (sounding as usize).checked_sub(1) else {
continue;
};
match &mut spans[position] {
slot @ None => *slot = Some(frame..frame + 1),
Some(span) => span.end = frame + 1,
}
}
spans
}
pub fn group_spans(&self, sizes: &[usize]) -> Result<Vec<Option<Range<usize>>>, OpenFstError> {
let total: usize = sizes.iter().sum();
if total != self.num_phones {
return Err(OpenFstError::InvalidOperation(format!(
"Alignment: groups of {total} phones for a {}-phone reference",
self.num_phones
)));
}
let spans = self.spans();
let mut grouped = Vec::with_capacity(sizes.len());
let mut at = 0;
for &size in sizes {
let mut group: Option<Range<usize>> = None;
for span in spans[at..at + size].iter().flatten() {
group = Some(match group {
None => span.clone(),
Some(so_far) => so_far.start..span.end,
});
}
grouped.push(group);
at += size;
}
Ok(grouped)
}
pub fn skipped(&self) -> Vec<usize> {
let mut sounded = DenseBitSet::new_empty(self.num_phones);
for &sounding in &self.sounding {
if let Some(position) = (sounding as usize).checked_sub(1) {
sounded.insert(position);
}
}
(0..self.num_phones)
.filter(|&position| !sounded.contains(position))
.collect()
}
pub fn acoustic_costs<'a, A>(
&'a self,
chain: &'a AlignChain,
dense: &'a DenseFst<'a, A>,
) -> impl ExactSizeIterator<Item = f32> + 'a
where
A: Arc + 'a,
A::Weight: FromScore,
{
self.sounding.iter().enumerate().map(move |(frame, &k)| {
let column = chain.column((k as usize).checked_sub(1));
dense.frame(frame)[column as usize]
})
}
pub fn mean_acoustic_cost<A>(&self, chain: &AlignChain, dense: &DenseFst<'_, A>) -> f32
where
A: Arc,
A::Weight: FromScore,
{
if self.sounding.is_empty() {
return 0.0;
}
let total: f64 = self
.sounding
.iter()
.enumerate()
.map(|(frame, &k)| {
let column = chain.column((k as usize).checked_sub(1));
dense.frame(frame)[column as usize] as f64
})
.sum();
(total / self.sounding.len() as f64) as f32
}
pub fn from_path(chain: &AlignChain, path: &Path) -> Result<Self, OpenFstError> {
let mut sounding = Vec::with_capacity(path.num_frames());
for (frame, (&code, &position)) in path.codes().iter().zip(path.positions()).enumerate() {
let sounds = *SOUNDS.get(code as usize).ok_or_else(|| {
OpenFstError::InvalidOperation(format!(
"Alignment: transition {code} at frame {frame} is not one of the chain's four"
))
})?;
sounding.push(if sounds { position } else { 0 });
}
Ok(Self {
sounding,
num_phones: chain.phones.len(),
cost: path.cost(),
})
}
pub fn from_output_labels<L: ArcLabel>(
chain: &AlignChain,
labels: &[L],
cost: f32,
) -> Result<Self, OpenFstError> {
let num_phones = chain.phones.len();
let silent = num_phones as i64 + 1;
let mut sounding = Vec::with_capacity(labels.len());
for (frame, label) in labels.iter().enumerate() {
let value = label.to_i64().unwrap_or(-1);
if value == silent {
sounding.push(0);
} else if value >= 1 && value < silent {
sounding.push(value as u32);
} else {
return Err(OpenFstError::InvalidOperation(format!(
"Alignment: output label {value} at frame {frame} names no position of a \
{num_phones}-phone reference"
)));
}
}
Ok(Self {
sounding,
num_phones,
cost,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChainTrellis<'a, A: Arc> {
chain: &'a AlignChain,
dense: &'a DenseFst<'a, A>,
}
impl<A: Arc> ChainTrellis<'_, A> {
#[inline(always)]
pub fn chain(&self) -> &AlignChain {
self.chain
}
}
impl<A: Arc> Trellis<4> for ChainTrellis<'_, A>
where
A::Weight: FromScore,
{
type Frame<'f>
= &'f [f32]
where
Self: 'f;
#[inline(always)]
fn num_frames(&self) -> usize {
self.dense.num_frames()
}
#[inline(always)]
fn num_positions(&self) -> usize {
self.chain.phones.len()
}
#[inline(always)]
fn frame(&self, frame: usize) -> &[f32] {
self.dense.frame(frame)
}
#[inline(always)]
fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
let blank = Step::new(0, frame[self.chain.blank as usize]);
if position == 0 {
return [blank, Step::ABSENT, Step::ABSENT, Step::ABSENT];
}
let phone = frame[self.chain.phones[position - 1] as usize];
[
blank,
Step::new(0, phone),
Step::new(1, phone),
Step::new(1, self.chain.skips[position - 1] + blank.cost),
]
}
}
impl<A: Arc> ReversibleTrellis<4> for ChainTrellis<'_, A>
where
A::Weight: FromScore,
{
#[inline(always)]
fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
let blank = Step::new(0, frame[self.chain.blank as usize]);
let hold = if position > 0 {
Step::new(0, frame[self.chain.phones[position - 1] as usize])
} else {
Step::ABSENT
};
let (commit, skip) = if position < self.chain.phones.len() {
(
Step::new(1, frame[self.chain.phones[position] as usize]),
Step::new(1, self.chain.skips[position] + blank.cost),
)
} else {
(Step::ABSENT, Step::ABSENT)
};
[blank, hold, commit, skip]
}
}
#[inline(always)]
pub(crate) fn column_read(chain: &AlignChain, code: u8, position: usize) -> u32 {
if SOUNDS[code as usize] {
chain.phones[position - 1]
} else {
chain.blank
}
}
pub fn align<A>(
chain: &AlignChain,
dense: &DenseFst<'_, A>,
) -> Result<Option<Alignment>, OpenFstError>
where
A: Arc,
A::Weight: FromScore,
{
let trellis = chain.against(dense)?;
let Some(path) = best_path(&trellis)? else {
return Ok(None);
};
Alignment::from_path(chain, &path).map(Some)
}
#[cfg(test)]
mod tests {
use super::*;
use sicada::arc::StdArc;
use sicada::fst::ExpandedFst;
use sicada::fsts::vector_fst::StdVectorFst;
use crate::compact::{DeterminizeLatticeOptions, determinize_lattice};
use crate::frontier::DecodeOptions;
use crate::lattice::{LatticeDecodeOptions, lattice_decode};
use crate::nbest::n_best;
use crate::trellis::axioms;
use crate::viterbi::viterbi_decode;
const SYMBOLS: usize = 4;
fn certain(columns: &[usize]) -> Vec<f32> {
let mut scores = vec![10.0; columns.len() * SYMBOLS];
for (frame, &column) in columns.iter().enumerate() {
scores[frame * SYMBOLS + column] = 0.0;
}
scores
}
fn recomputed_cost(
alignment: &Alignment,
chain: &AlignChain,
dense: &DenseFst<'_, StdArc>,
) -> f32 {
let acoustic: f32 = alignment.acoustic_costs(chain, dense).sum();
let skipped: f32 = alignment
.skipped()
.into_iter()
.map(|position| chain.skip_costs()[position])
.sum();
acoustic + skipped
}
fn by_decoding(chain: &AlignChain, dense: &DenseFst<'_, StdArc>) -> Option<Alignment> {
let fst: StdVectorFst = chain.to_fst(1).expect("a chain FST");
let decoded =
viterbi_decode(&fst, dense, &DecodeOptions::exhaustive()).expect("a decode")?;
Some(
Alignment::from_output_labels(chain, &decoded.labels, decoded.weight.0)
.expect("labels from this chain"),
)
}
#[test]
fn a_phone_owns_the_frames_that_sound_it() {
let scores = certain(&[1, 1, 0, 2]);
let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1, 2]);
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(
alignment.frames().collect::<Vec<_>>(),
vec![Some(0), Some(0), None, Some(1)]
);
assert_eq!(alignment.spans(), vec![Some(0..2), Some(3..4)]);
assert!(alignment.skipped().is_empty());
assert!(alignment.cost().abs() < 1e-6, "{}", alignment.cost());
}
#[test]
fn a_group_of_phones_spans_its_first_sounding_frame_to_its_last() {
let scores = certain(&[1, 0, 2, 0, 3, 0]);
let dense = DenseFst::<StdArc>::new(&scores, 6, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1, 2, 3, 2])
.with_uniform_skip_cost(1.0)
.unwrap();
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(alignment.skipped(), vec![3]);
assert_eq!(
alignment.group_spans(&[2, 2]).unwrap(),
vec![Some(0..3), Some(4..5)]
);
assert_eq!(
alignment.group_spans(&[3, 1]).unwrap(),
vec![Some(0..5), None]
);
assert_eq!(alignment.group_spans(&[4]).unwrap(), vec![Some(0..5)]);
let err = alignment.group_spans(&[2, 1]).unwrap_err();
assert!(format!("{err}").contains("groups of 3 phones"), "{err}");
}
#[test]
fn a_blank_frame_belongs_to_no_phone() {
let scores = certain(&[1, 0, 0, 0, 0, 0, 0, 0, 0]);
let dense = DenseFst::<StdArc>::new(&scores, 9, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1]);
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(
alignment.spans(),
vec![Some(0..1)],
"the phone must not swallow the silence after it"
);
}
#[test]
fn an_empty_reference_leaves_every_frame_sounding_nothing() {
let scores = certain(&[1, 2, 0]);
let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![]);
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert!(alignment.frames().all(|sounding| sounding.is_none()));
assert_eq!(alignment.spans(), vec![]);
assert!(
(alignment.cost() - 20.0).abs() < 1e-6,
"{}",
alignment.cost()
);
}
#[test]
fn a_reference_longer_than_the_audio_aligns_to_nothing() {
let scores = certain(&[1, 2]);
let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1, 2, 3]);
assert_eq!(align(&chain, &dense).unwrap(), None);
let chain = chain.with_uniform_skip_cost(0.0).unwrap();
assert_eq!(align(&chain, &dense).unwrap(), None);
}
#[test]
fn a_phone_the_model_has_no_column_for_is_reported() {
let scores = certain(&[1]);
let dense = DenseFst::<StdArc>::new(&scores, 1, SYMBOLS).unwrap();
let err = align(&AlignChain::new(vec![9]), &dense).unwrap_err();
assert!(format!("{err}").contains("position 0 is column 9"), "{err}");
let err = align(&AlignChain::new(vec![1]).with_blank(7), &dense).unwrap_err();
assert!(format!("{err}").contains("the blank is column 7"), "{err}");
}
#[test]
fn a_phone_with_no_evidence_is_given_up_only_when_that_is_cheaper() {
let scores = [
10.0, 0.0, 10.0, 10.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 3.0, 10.0, 0.0, 10.0, 10.0, 10.0,
];
let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
let reference = vec![1, 2];
let cheap = AlignChain::new(reference.clone())
.with_skip_costs(&[6.0, 1.0])
.unwrap();
let alignment = align(&cheap, &dense).unwrap().expect("an alignment");
assert_eq!(alignment.skipped(), vec![1]);
assert_eq!(alignment.spans()[0], Some(0..2));
assert_eq!(alignment.spans()[1], None);
assert!(
(alignment.cost() - 1.0).abs() < 1e-6,
"{}",
alignment.cost()
);
let dear = AlignChain::new(reference)
.with_skip_costs(&[6.0, 5.0])
.unwrap();
let alignment = align(&dear, &dense).unwrap().expect("an alignment");
assert!(alignment.skipped().is_empty());
assert_eq!(alignment.spans(), vec![Some(0..2), Some(2..3)]);
assert!(
(alignment.cost() - 3.0).abs() < 1e-6,
"{}",
alignment.cost()
);
}
#[test]
fn a_skip_that_only_ties_does_not_happen() {
let scores = [
10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 4.0, 10.0,
];
let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
let reference = vec![1, 2];
let tied = AlignChain::new(reference.clone())
.with_skip_costs(&[9.0, 4.0])
.unwrap();
let alignment = align(&tied, &dense).unwrap().expect("an alignment");
assert!(
alignment.skipped().is_empty(),
"a tie has to keep the reference"
);
assert_eq!(alignment.spans(), vec![Some(0..1), Some(1..2)]);
assert!(
(alignment.cost() - 4.0).abs() < 1e-6,
"{}",
alignment.cost()
);
let under = AlignChain::new(reference)
.with_skip_costs(&[9.0, 3.9])
.unwrap();
let alignment = align(&under, &dense).unwrap().expect("an alignment");
assert_eq!(alignment.skipped(), vec![1]);
}
#[test]
fn a_skip_cost_that_pays_for_itself_is_refused() {
let chain = AlignChain::new(vec![1, 2]);
let err = chain.clone().with_skip_costs(&[1.0, -1.0]).unwrap_err();
assert!(format!("{err}").contains("position 1"), "{err}");
assert!(chain.clone().with_skip_costs(&[f32::NAN, 1.0]).is_err());
assert!(
chain.clone().with_skip_costs(&[1.0]).is_err(),
"wrong count"
);
assert!(chain.with_uniform_skip_cost(-0.5).is_err());
}
#[test]
fn the_alignment_recovers_what_each_frame_paid() {
let scores = certain(&[1, 0, 2]);
let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1, 2]);
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(
alignment.acoustic_costs(&chain, &dense).collect::<Vec<_>>(),
vec![0.0, 0.0, 0.0]
);
assert_eq!(alignment.mean_acoustic_cost(&chain, &dense), 0.0);
let wrong = AlignChain::new(vec![3, 3]);
let alignment = align(&wrong, &dense).unwrap().expect("an alignment");
assert!(
alignment.mean_acoustic_cost(&wrong, &dense) > 5.0,
"an unrelated reference has to be visible in the per-frame cost"
);
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn cost(&mut self) -> f32 {
self.below(1 << 20) as f32 / 4096.0
}
}
fn by_brute_force(
chain: &AlignChain,
dense: &DenseFst<'_, StdArc>,
num_frames: usize,
) -> Option<f32> {
fn walk(
chain: &AlignChain,
dense: &DenseFst<'_, StdArc>,
num_frames: usize,
frame: usize,
position: usize,
cost: f32,
best: &mut Option<f32>,
) {
if frame == num_frames {
if position == chain.num_phones() && best.is_none_or(|so_far| cost < so_far) {
*best = Some(cost);
}
return;
}
let scores = dense.frame(frame);
let blank = scores[chain.blank() as usize];
let mut step = |position, extra: f32| {
walk(
chain,
dense,
num_frames,
frame + 1,
position,
cost + extra,
best,
)
};
step(position, blank);
if position > 0 {
step(position, scores[chain.phones()[position - 1] as usize]);
}
if position < chain.num_phones() {
step(position + 1, scores[chain.phones()[position] as usize]);
let skip = chain.skip_costs()[position];
if skip.is_finite() {
step(position + 1, skip + blank);
}
}
}
let mut best = None;
walk(chain, dense, num_frames, 0, 0, 0.0, &mut best);
best
}
#[test]
fn it_agrees_with_enumerating_every_alignment() {
let mut rng = Rng(0x1234_5678_9ABC_DEF1);
let mut compared = 0;
for round in 0..200 {
let num_frames = 1 + rng.below(7);
let num_phones = rng.below(4);
let phones: Vec<u32> = (0..num_phones)
.map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
.collect();
let chain = AlignChain::new(phones);
let chain = if rng.below(2) == 0 {
chain
.with_uniform_skip_cost(rng.below(1 << 12) as f32 / 512.0)
.unwrap()
} else {
chain
};
let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
let expected = by_brute_force(&chain, &dense, num_frames);
let alignment = align(&chain, &dense).unwrap();
match (expected, alignment) {
(None, None) => {}
(Some(expected), Some(alignment)) => {
compared += 1;
assert!(
(alignment.cost() - expected).abs() < 1e-3,
"round {round}: aligner {} against every path's best {expected}",
alignment.cost()
);
assert!(
(recomputed_cost(&alignment, &chain, &dense) - alignment.cost()).abs()
< 1e-3,
"round {round}: the traceback does not add up to the cost"
);
assert_eq!(alignment.num_frames(), num_frames);
}
(expected, alignment) => {
panic!("round {round}: brute force {expected:?}, aligner {alignment:?}")
}
}
}
assert!(compared > 150, "only {compared} rounds had an alignment");
}
#[test]
fn it_agrees_with_decoding_the_chain_as_an_fst() {
let mut rng = Rng(0xFEED_FACE_1234_5678);
let mut compared = 0;
for round in 0..200 {
let num_frames = 1 + rng.below(40);
let num_phones = rng.below(12);
let phones: Vec<u32> = (0..num_phones)
.map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
.collect();
let chain = AlignChain::new(phones);
let chain = if rng.below(2) == 0 {
chain
.with_uniform_skip_cost(rng.below(1 << 12) as f32 / 512.0)
.unwrap()
} else {
chain
};
let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
let expected = by_decoding(&chain, &dense);
let alignment = align(&chain, &dense).unwrap();
match (expected, alignment) {
(None, None) => {}
(Some(expected), Some(alignment)) => {
compared += 1;
assert!(
(alignment.cost() - expected.cost()).abs() < 1e-2,
"round {round}: aligner {} against the decoder {}",
alignment.cost(),
expected.cost()
);
assert!(
(recomputed_cost(&alignment, &chain, &dense) - alignment.cost()).abs()
< 1e-2,
"round {round}: the traceback does not add up to the cost"
);
assert_eq!(expected.num_frames(), num_frames, "one label per frame");
}
(expected, alignment) => {
panic!("round {round}: decoder {expected:?}, aligner {alignment:?}")
}
}
}
assert!(compared > 150, "only {compared} rounds had an alignment");
}
#[test]
fn the_chain_decodes_to_alternative_alignments() {
let scores = [
9.0, 0.0, 9.0, 9.0, 1.0, 0.0, 9.0, 9.0, 9.0, 0.0, 9.0, 9.0,
];
let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1]);
let fst: StdVectorFst = chain.to_fst(1).unwrap();
let lattice = lattice_decode(&fst, &dense, &LatticeDecodeOptions::exhaustive())
.unwrap()
.expect("a lattice");
let compact = determinize_lattice(&lattice, &DeterminizeLatticeOptions::default()).unwrap();
let answers = n_best(&compact, 2).unwrap();
assert_eq!(answers.len(), 2);
let best = Alignment::from_output_labels(&chain, &answers[0].words, answers[0].cost())
.expect("an alignment");
assert_eq!(best.spans(), vec![Some(0..3)], "the phone held throughout");
assert_eq!(
align(&chain, &dense).unwrap().unwrap().spans(),
best.spans(),
"and it is what the exact aligner returns"
);
let second = Alignment::from_output_labels(&chain, &answers[1].words, answers[1].cost())
.expect("an alignment");
assert_eq!(
second.frames().collect::<Vec<_>>(),
vec![Some(0), None, Some(0)]
);
assert!((second.cost() - best.cost() - 1.0).abs() < 1e-5);
}
#[test]
fn labels_from_another_chain_are_reported() {
let chain = AlignChain::new(vec![1, 2]);
assert!(Alignment::from_output_labels(&chain, &[1i32, 3], 0.0).is_ok());
let err = Alignment::from_output_labels(&chain, &[1i32, 4], 0.0).unwrap_err();
assert!(format!("{err}").contains("names no position"), "{err}");
assert!(Alignment::from_output_labels(&chain, &[0i32], 0.0).is_err());
}
#[test]
fn a_chain_fst_puts_its_columns_where_the_matrix_has_them() {
let chain = AlignChain::new(vec![1, 2])
.with_uniform_skip_cost(1.0)
.unwrap();
let fst: StdVectorFst = chain.to_fst(1).unwrap();
assert_eq!(fst.num_states(), 3);
assert_eq!(fst.num_arcs(0), 3);
assert_eq!(fst.num_arcs(1), 4);
assert_eq!(fst.num_arcs(2), 2);
assert!(
fst.states()
.all(|s| fst.arcs(s).all(|arc| arc.ilabel() != 0)),
"every arc has to consume a frame"
);
let fst: StdVectorFst = AlignChain::new(vec![1, 2]).to_fst(1).unwrap();
assert_eq!(fst.num_arcs(0), 2);
assert!(AlignChain::new(vec![1]).to_fst::<StdArc>(0).is_err());
}
#[test]
fn the_chain_obeys_the_trellis_contract() {
let chain = AlignChain::new(vec![1, 2, 1])
.with_skip_costs(&[1.0, 2.0, f32::INFINITY])
.unwrap();
let scores: Vec<f32> = (0..4 * SYMBOLS).map(|i| i as f32 / 3.0).collect();
let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
axioms::check(&chain.against(&dense).unwrap());
let rigid = AlignChain::new(vec![1, 2, 1]);
axioms::check(&rigid.against(&dense).unwrap());
axioms::check(&AlignChain::new(vec![]).against(&dense).unwrap());
}
#[test]
fn a_reference_as_long_as_the_audio_has_one_alignment() {
let scores = certain(&[1, 2, 3]);
let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
let chain = AlignChain::new(vec![1, 2, 3]);
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(
alignment.frames().collect::<Vec<_>>(),
vec![Some(0), Some(1), Some(2)]
);
assert!(alignment.cost().abs() < 1e-6);
let scores = [0.0, 10.0, 10.0, 10.0].repeat(3);
let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
let alignment = align(&chain, &dense).unwrap().expect("an alignment");
assert_eq!(
alignment.frames().collect::<Vec<_>>(),
vec![Some(0), Some(1), Some(2)]
);
}
}