use sicada::arc::{Arc, ArcLabel, ArcStateId};
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;
pub fn ctc_topo<A: Arc>(num_symbols: usize, label_offset: i64) -> Result<VectorFst<A>, OpenFstError>
where
A::Weight: Weight,
{
if num_symbols < 2 {
return Err(OpenFstError::InvalidOperation(format!(
"ctc_topo: {num_symbols} symbols is not enough for a blank and something else"
)));
}
let label_of = |column: usize| -> Result<A::Label, OpenFstError> {
A::Label::from_i64(label_offset + column as i64).ok_or_else(|| {
OpenFstError::InvalidOperation(format!(
"ctc_topo: label {} does not fit the arc's label type",
label_offset + column as i64
))
})
};
if label_offset < 1 {
return Err(OpenFstError::InvalidOperation(
"ctc_topo: column 0 would be epsilon, which consumes no frame".into(),
));
}
let mut fst: VectorFst<A> = VectorFst::new();
fst.reserve_states(num_symbols);
for _ in 0..num_symbols {
fst.add_state();
}
fst.set_start(A::StateId::from_usize(0));
let blank = label_of(0)?;
for last in 0..num_symbols {
let from = A::StateId::from_usize(last);
fst.set_final(from, A::Weight::one());
fst.add_arc(
from,
A::new(
blank,
A::Label::epsilon(),
A::Weight::one(),
A::StateId::from_usize(0),
),
);
for symbol in 1..num_symbols {
let label = label_of(symbol)?;
let says = if symbol == last {
A::Label::epsilon()
} else {
label
};
fst.add_arc(
from,
A::new(
label,
says,
A::Weight::one(),
A::StateId::from_usize(symbol),
),
);
}
}
fst.properties(K_FST_PROPERTIES, true);
Ok(fst)
}
pub fn collapse(columns: &[usize]) -> Vec<usize> {
let mut out: Vec<usize> = Vec::with_capacity(columns.len());
let mut previous = usize::MAX;
for &column in columns {
if column != previous && column != 0 {
out.push(column);
}
previous = column;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use sicada::arc::StdArc;
use sicada::fst::ExpandedFst;
use sicada::fsts::vector_fst::StdVectorFst;
use sicada::properties::{K_I_DETERMINISTIC, K_NO_I_EPSILONS};
use crate::compact::{DeterminizeLatticeOptions, determinize_lattice};
use crate::dense::DenseFst;
use crate::frontier::DecodeOptions;
use crate::lattice::{LatticeDecodeOptions, lattice_decode};
use crate::nbest::n_best;
use crate::viterbi::viterbi_decode;
const SYMBOLS: usize = 4;
fn topo() -> StdVectorFst {
ctc_topo(SYMBOLS, 1).expect("a topology")
}
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 columns_of(labels: &[i32]) -> Vec<usize> {
labels.iter().map(|label| (label - 1) as usize).collect()
}
#[test]
fn it_is_deterministic_on_the_frames_it_reads() {
let fst = topo();
assert_eq!(fst.num_states(), SYMBOLS);
let props = fst.properties(K_I_DETERMINISTIC | K_NO_I_EPSILONS, true);
assert_ne!(props & K_I_DETERMINISTIC, 0, "two arcs read the same frame");
assert_ne!(props & K_NO_I_EPSILONS, 0, "an arc reads no frame");
for state in fst.states() {
assert_eq!(fst.num_arcs(state), SYMBOLS, "one arc per column");
}
}
#[test]
fn it_collapses_exactly_as_the_rule_says() {
let graph = topo();
for length in 1..=5usize {
let mut columns = vec![0usize; length];
loop {
let scores = certain(&columns);
let dense = DenseFst::<StdArc>::new(&scores, length, SYMBOLS).unwrap();
let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
.unwrap()
.expect("a path");
assert_eq!(
columns_of(&decoded.labels),
collapse(&columns),
"for the alignment {columns:?}"
);
let mut place = 0;
loop {
if place == length {
break;
}
columns[place] += 1;
if columns[place] < SYMBOLS {
break;
}
columns[place] = 0;
place += 1;
}
if place == length {
break;
}
}
}
}
#[test]
fn a_blank_is_what_lets_a_symbol_repeat() {
let graph = topo();
let held = certain(&[1, 1, 1]);
let dense = DenseFst::<StdArc>::new(&held, 3, SYMBOLS).unwrap();
let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
.unwrap()
.unwrap();
assert_eq!(columns_of(&decoded.labels), vec![1], "one long symbol");
let separated = certain(&[1, 0, 1]);
let dense = DenseFst::<StdArc>::new(&separated, 3, SYMBOLS).unwrap();
let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
.unwrap()
.unwrap();
assert_eq!(columns_of(&decoded.labels), vec![1, 1], "two of them");
}
#[test]
fn the_whole_pipeline_agrees_with_the_rule() {
let graph = topo();
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 lattice = lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive())
.unwrap()
.expect("a lattice");
let compact = determinize_lattice(&lattice, &DeterminizeLatticeOptions::default()).unwrap();
let best = n_best(&compact, 2).unwrap();
assert_eq!(best.len(), 2);
assert_eq!(columns_of(&best[0].words), vec![1], "one long symbol");
assert_eq!(columns_of(&best[1].words), vec![1, 1], "two of them");
assert!((best[0].cost() - 0.0).abs() < 1e-6, "{}", best[0].cost());
assert!((best[1].cost() - 1.0).abs() < 1e-6, "{}", best[1].cost());
assert_eq!(best[0].alignment().len(), 3, "one label per frame");
assert_eq!(columns_of(best[0].alignment()), vec![1, 1, 1]);
assert_eq!(columns_of(best[1].alignment()), vec![1, 0, 1]);
}
#[test]
fn an_alphabet_with_nothing_in_it_is_refused() {
assert!(ctc_topo::<StdArc>(1, 1).is_err());
assert!(ctc_topo::<StdArc>(4, 0).is_err(), "column 0 on epsilon");
}
#[test]
fn collapsing_is_runs_first_then_blanks() {
assert_eq!(collapse(&[0, 1, 1, 0, 1, 2]), vec![1, 1, 2]);
assert_eq!(collapse(&[]), Vec::<usize>::new());
assert_eq!(collapse(&[0, 0, 0]), Vec::<usize>::new());
assert_eq!(collapse(&[2, 2, 2]), vec![2]);
}
}