sicada_decode/lib.rs
1//! CPU decoding and forced alignment for acoustic-model score matrices.
2//!
3//! It sits outside `sicada` proper because `sicada` is a port of OpenFst's
4//! library, while the algorithms here belong to the speech-decoding layer.
5//!
6//! - [`dense`] reads the acoustic model's `T × V` score matrix as an FST, so
7//! that composing a decoding graph against it is an ordinary composition.
8//! - [`viterbi`] walks that composition one frame at a time without building
9//! it, which is how a decoder works.
10//! - [`lattice`] does the same but keeps the alternatives, so a second pass has
11//! something to rescore. [`lattice_weight`] is the semiring its arcs carry,
12//! which holds the graph cost and the acoustic cost apart.
13//! - [`compact`] collapses the alignments, so each word sequence appears once
14//! with the best one. [`compact_lattice_weight`] is the semiring that makes
15//! that possible: a cost with the frames it spanned attached.
16//! - [`nbest`] reads the answers back out, and rescales the two halves against
17//! each other without decoding again.
18//! - [`ctc`] builds the graph side for a CTC model, which is where a decoder
19//! with no language model starts.
20//! - [`align`](mod@align) covers the other half of the same model's use. When the
21//! transcript is already known the graph is a single chain, and the only
22//! question is which frames each phone occupies. That case is small enough to
23//! solve exactly, so it has no beam. [`occupancy`](mod@occupancy) walks the
24//! same chain in the log semiring, for the soft answer that a single path
25//! cannot give.
26//! - [`trellis`] is the solver those two are built on, and the piece to use
27//! when the chain is not the shape you want. Supply the transitions into a cell
28//! (how many there are, what they cost, what they mean) and the band, the
29//! packed traceback and the forward-backward come with it.
30//!
31//! # Decoding a CTC model
32//!
33//! The whole pipeline, from a score matrix to the answers. The scores here are
34//! made up; a real one comes out of the acoustic model as negative log
35//! probabilities, `T × V` row-major, with the blank in column 0.
36//!
37//! ```
38//! use sicada::arc::StdArc;
39//! use sicada_decode::{
40//! DecodeOptions, DenseFst, LatticeDecodeOptions, PrunedDeterminizeOptions, ctc_topo,
41//! determinize_lattice_pruned, lattice_decode, n_best, viterbi_decode,
42//! };
43//!
44//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! // Blank plus three tokens, four frames. Column 0 is the blank.
46//! let (frames, symbols) = (4, 4);
47//! let scores = vec![
48//! 9.0, 0.0, 9.0, 9.0, // token 1
49//! 9.0, 0.0, 9.0, 9.0, // token 1 again, held rather than repeated
50//! 0.0, 9.0, 9.0, 9.0, // blank
51//! 9.0, 0.0, 9.0, 9.0, // token 1, and the blank makes it a second one
52//! ];
53//!
54//! let graph = ctc_topo::<StdArc>(symbols, 1)?;
55//! let dense = DenseFst::<StdArc>::new(&scores, frames, symbols)?;
56//!
57//! // The transcript, and nothing else.
58//! let best = viterbi_decode(&graph, &dense, &DecodeOptions::default())?
59//! .expect("the beam kept a path");
60//! // Labels are columns offset by one, which is where `ctc_topo` put them.
61//! let columns: Vec<i32> = best.labels.iter().map(|label| label - 1).collect();
62//! assert_eq!(columns, vec![1, 1]);
63//!
64//! // Or the alternatives too, for a second pass to rescore.
65//! let lattice = lattice_decode(&graph, &dense, &LatticeDecodeOptions::default())?
66//! .expect("the beam kept a path");
67//! let compact = determinize_lattice_pruned(&lattice, &PrunedDeterminizeOptions::default())?;
68//! for answer in n_best(&compact.lattice, 3)? {
69//! let columns: Vec<i32> = answer.words.iter().map(|label| label - 1).collect();
70//! // Each answer knows which frames produced it.
71//! assert_eq!(answer.alignment().len(), frames);
72//! let _ = (columns, answer.cost());
73//! }
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! # Aligning a transcript that is already known
79//!
80//! Here the answer is given and only the timing is wanted, so there is no
81//! topology and no lattice: a single chain, solved exactly.
82//!
83//! ```
84//! use sicada::arc::StdArc;
85//! use sicada_decode::{AlignChain, DenseFst, align, occupancy};
86//!
87//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
88//! // Six frames over a blank and three phones. Column 0 is the blank.
89//! let (frames, symbols) = (6, 4);
90//! let scores = vec![
91//! 9.0, 0.0, 9.0, 9.0, // phone 1
92//! 9.0, 0.0, 9.0, 9.0, // still phone 1
93//! 0.0, 9.0, 9.0, 9.0, // silence
94//! 9.0, 9.0, 0.0, 9.0, // phone 2
95//! 0.0, 9.0, 9.0, 9.0, // silence
96//! 0.0, 9.0, 9.0, 9.0, // silence
97//! ];
98//!
99//! // The reference: phones as *columns*, in order. Labels do not come into it.
100//! let chain = AlignChain::new(vec![1, 2]);
101//! let dense = DenseFst::<StdArc>::new(&scores, frames, symbols)?;
102//!
103//! let alignment = align(&chain, &dense)?.expect("the reference fits");
104//! // Each phone gets the frames that sound it, and the blank frames belong to
105//! // nobody, so the last phone does not swallow the silence after it.
106//! assert_eq!(alignment.spans(), vec![Some(0..2), Some(3..4)]);
107//! assert!(alignment.skipped().is_empty());
108//!
109//! // The mean per-frame cost is the warning that the reference is not what
110//! // was said. Here it is low, because the reference is correct.
111//! assert!(alignment.mean_acoustic_cost(&chain, &dense) < 0.1);
112//!
113//! // The same chain in the log semiring, when the soft answer is wanted.
114//! let spread = occupancy(&chain, &dense)?.expect("the reference fits");
115//! assert!((spread.expected_durations()[0] - 2.0).abs() < 0.01);
116//! # Ok(())
117//! # }
118//! ```
119//!
120//! The references are Kaldi (`decoder/lattice-faster-decoder.*`,
121//! `fstext/lattice-weight.h`) and k2.
122
123pub mod align;
124pub mod compact;
125pub mod compact_lattice_weight;
126pub mod ctc;
127pub mod dense;
128mod frontier;
129pub mod lattice;
130pub mod lattice_weight;
131pub mod nbest;
132pub mod occupancy;
133pub mod trellis;
134pub mod viterbi;
135
136pub use align::{AlignChain, Alignment, ChainTrellis, align};
137pub use compact::{
138 CompactLattice, DeterminizeLatticeOptions, PrunedDeterminizeOptions, PrunedLattice,
139 determinize_lattice, determinize_lattice_pruned, to_compact,
140};
141pub use compact_lattice_weight::{CompactLatticeArc, CompactLatticeWeight};
142pub use ctc::{collapse, ctc_topo};
143pub use dense::{DenseFst, FromScore};
144pub use frontier::DecodeOptions;
145pub use lattice::{Lattice, LatticeDecodeOptions, lattice_decode};
146pub use lattice_weight::{LatticeArc, LatticeWeight, LatticeWeight64};
147pub use nbest::{Hypothesis, n_best, scale};
148pub use occupancy::{Occupancy, occupancy};
149pub use trellis::{Path, ReversibleTrellis, Step, Transition, Trellis, best_path, posteriors};
150pub use viterbi::{Decoded, viterbi_decode};