1use std::ops::Range;
42
43use sicada::arc::{Arc, ArcLabel, ArcStateId};
44use sicada::data_structures::bit_set::DenseBitSet;
45use sicada::error::OpenFstError;
46use sicada::fst::{Fst, MutableFst};
47use sicada::fsts::vector_fst::VectorFst;
48use sicada::properties::K_FST_PROPERTIES;
49use sicada::weight::Weight;
50
51use crate::dense::{DenseFst, FromScore};
52use crate::trellis::{Path, ReversibleTrellis, Step, Trellis, best_path};
53
54const SOUNDS: [bool; 4] = [false, true, true, false];
56
57#[derive(Debug, Clone, PartialEq)]
64pub struct AlignChain {
65 phones: Vec<u32>,
67 skips: Vec<f32>,
69 blank: u32,
70}
71
72impl AlignChain {
73 pub fn new(phones: impl Into<Vec<u32>>) -> Self {
80 let phones = phones.into();
81 Self {
82 skips: vec![f32::INFINITY; phones.len()],
83 phones,
84 blank: 0,
85 }
86 }
87
88 pub fn with_skip_costs(mut self, costs: &[f32]) -> Result<Self, OpenFstError> {
104 if costs.len() != self.phones.len() {
105 return Err(OpenFstError::InvalidOperation(format!(
106 "AlignChain: {} skip costs for {} phones",
107 costs.len(),
108 self.phones.len()
109 )));
110 }
111 if let Some(bad) = costs.iter().position(|cost| cost.is_nan() || *cost < 0.0) {
112 return Err(OpenFstError::InvalidOperation(format!(
113 "AlignChain: the skip cost at position {bad} is {}, and a skip that pays for \
114 itself would drop the reference rather than align it",
115 costs[bad]
116 )));
117 }
118 self.skips.copy_from_slice(costs);
119 Ok(self)
120 }
121
122 pub fn with_uniform_skip_cost(self, cost: f32) -> Result<Self, OpenFstError> {
128 let costs = vec![cost; self.phones.len()];
129 self.with_skip_costs(&costs)
130 }
131
132 pub fn with_blank(mut self, column: u32) -> Self {
134 self.blank = column;
135 self
136 }
137
138 #[inline(always)]
140 pub fn num_phones(&self) -> usize {
141 self.phones.len()
142 }
143
144 #[inline(always)]
146 pub fn is_empty(&self) -> bool {
147 self.phones.is_empty()
148 }
149
150 #[inline(always)]
152 pub fn phones(&self) -> &[u32] {
153 &self.phones
154 }
155
156 #[inline(always)]
158 pub fn skip_costs(&self) -> &[f32] {
159 &self.skips
160 }
161
162 #[inline(always)]
164 pub fn blank(&self) -> u32 {
165 self.blank
166 }
167
168 pub fn against<'a, A>(
181 &'a self,
182 dense: &'a DenseFst<'a, A>,
183 ) -> Result<ChainTrellis<'a, A>, OpenFstError>
184 where
185 A: Arc,
186 A::Weight: FromScore,
187 {
188 self.check_columns(dense.num_symbols())?;
189 Ok(ChainTrellis { chain: self, dense })
190 }
191
192 pub const HOLD_BLANK: u8 = 0;
195 pub const HOLD_PHONE: u8 = 1;
197 pub const COMMIT: u8 = 2;
199 pub const SKIP: u8 = 3;
201
202 #[inline(always)]
211 pub const fn sounds(code: u8) -> bool {
212 SOUNDS[code as usize]
213 }
214
215 #[inline(always)]
218 fn column(&self, position: Option<usize>) -> u32 {
219 match position {
220 Some(p) => self.phones[p],
221 None => self.blank,
222 }
223 }
224
225 pub(crate) fn check_columns(&self, num_symbols: usize) -> Result<(), OpenFstError> {
229 let named = std::iter::once((None, self.blank)).chain(
230 self.phones
231 .iter()
232 .enumerate()
233 .map(|(p, &column)| (Some(p), column)),
234 );
235 for (position, column) in named {
236 if column as usize >= num_symbols {
237 let what = match position {
238 Some(p) => format!("position {p}"),
239 None => "the blank".to_string(),
240 };
241 return Err(OpenFstError::InvalidOperation(format!(
242 "AlignChain: {what} is column {column}, which a {num_symbols}-symbol acoustic \
243 matrix does not have"
244 )));
245 }
246 }
247 Ok(())
248 }
249
250 pub fn to_fst<A: Arc>(&self, label_offset: i64) -> Result<VectorFst<A>, OpenFstError>
275 where
276 A::Weight: FromScore,
277 {
278 if label_offset < 1 {
279 return Err(OpenFstError::InvalidOperation(
280 "AlignChain::to_fst: column 0 would be epsilon, which consumes no frame".into(),
281 ));
282 }
283 let fits = |value: i64, what: &str| -> Result<A::Label, OpenFstError> {
284 A::Label::from_i64(value).ok_or_else(|| {
285 OpenFstError::InvalidOperation(format!(
286 "AlignChain::to_fst: {what} {value} does not fit the arc's label type"
287 ))
288 })
289 };
290 let input = |column: u32| fits(label_offset + column as i64, "input label");
291 let n = self.phones.len();
292 let sounds = |position: Option<usize>| {
295 let value = match position {
296 Some(p) => p as i64 + 1,
297 None => n as i64 + 1,
298 };
299 fits(value, "output label")
300 };
301
302 let mut fst: VectorFst<A> = VectorFst::new();
303 fst.reserve_states(n + 1);
304 for _ in 0..=n {
305 fst.add_state();
306 }
307 fst.set_start(A::StateId::from_usize(0));
308 fst.set_final(A::StateId::from_usize(n), A::Weight::one());
309
310 let blank = input(self.blank)?;
311 let silent = sounds(None)?;
312 for i in 0..=n {
313 let from = A::StateId::from_usize(i);
314 let to = A::StateId::from_usize((i + 1).min(n));
315
316 fst.add_arc(from, A::new(blank, silent, A::Weight::one(), from));
317 if i > 0 {
318 let held = input(self.phones[i - 1])?;
319 fst.add_arc(
320 from,
321 A::new(held, sounds(Some(i - 1))?, A::Weight::one(), from),
322 );
323 }
324 if i < n {
325 let next = input(self.phones[i])?;
326 fst.add_arc(from, A::new(next, sounds(Some(i))?, A::Weight::one(), to));
327 let cost = self.skips[i];
331 if cost.is_finite() {
332 fst.add_arc(from, A::new(blank, silent, A::Weight::from_cost(cost), to));
333 }
334 }
335 }
336
337 fst.properties(K_FST_PROPERTIES, true);
338 Ok(fst)
339 }
340}
341
342#[derive(Debug, Clone, PartialEq)]
347pub struct Alignment {
348 sounding: Vec<u32>,
354 num_phones: usize,
355 cost: f32,
356}
357
358impl Alignment {
359 #[inline(always)]
361 pub fn num_frames(&self) -> usize {
362 self.sounding.len()
363 }
364
365 #[inline(always)]
367 pub fn num_phones(&self) -> usize {
368 self.num_phones
369 }
370
371 #[inline(always)]
378 pub fn sounding(&self, frame: usize) -> Option<usize> {
379 (self.sounding[frame] as usize).checked_sub(1)
380 }
381
382 pub fn frames(&self) -> impl ExactSizeIterator<Item = Option<usize>> + '_ {
384 self.sounding.iter().map(|&k| (k as usize).checked_sub(1))
385 }
386
387 #[inline(always)]
390 pub fn cost(&self) -> f32 {
391 self.cost
392 }
393
394 pub fn spans(&self) -> Vec<Option<Range<usize>>> {
400 let mut spans = vec![None; self.num_phones];
401 for (frame, &sounding) in self.sounding.iter().enumerate() {
402 let Some(position) = (sounding as usize).checked_sub(1) else {
403 continue;
404 };
405 match &mut spans[position] {
406 slot @ None => *slot = Some(frame..frame + 1),
407 Some(span) => span.end = frame + 1,
408 }
409 }
410 spans
411 }
412
413 pub fn group_spans(&self, sizes: &[usize]) -> Result<Vec<Option<Range<usize>>>, OpenFstError> {
428 let total: usize = sizes.iter().sum();
429 if total != self.num_phones {
430 return Err(OpenFstError::InvalidOperation(format!(
431 "Alignment: groups of {total} phones for a {}-phone reference",
432 self.num_phones
433 )));
434 }
435 let spans = self.spans();
436 let mut grouped = Vec::with_capacity(sizes.len());
437 let mut at = 0;
438 for &size in sizes {
439 let mut group: Option<Range<usize>> = None;
440 for span in spans[at..at + size].iter().flatten() {
441 group = Some(match group {
442 None => span.clone(),
443 Some(so_far) => so_far.start..span.end,
444 });
445 }
446 grouped.push(group);
447 at += size;
448 }
449 Ok(grouped)
450 }
451
452 pub fn skipped(&self) -> Vec<usize> {
454 let mut sounded = DenseBitSet::new_empty(self.num_phones);
455 for &sounding in &self.sounding {
456 if let Some(position) = (sounding as usize).checked_sub(1) {
457 sounded.insert(position);
458 }
459 }
460 (0..self.num_phones)
461 .filter(|&position| !sounded.contains(position))
462 .collect()
463 }
464
465 pub fn acoustic_costs<'a, A>(
477 &'a self,
478 chain: &'a AlignChain,
479 dense: &'a DenseFst<'a, A>,
480 ) -> impl ExactSizeIterator<Item = f32> + 'a
481 where
482 A: Arc + 'a,
483 A::Weight: FromScore,
484 {
485 self.sounding.iter().enumerate().map(move |(frame, &k)| {
486 let column = chain.column((k as usize).checked_sub(1));
487 dense.frame(frame)[column as usize]
488 })
489 }
490
491 pub fn mean_acoustic_cost<A>(&self, chain: &AlignChain, dense: &DenseFst<'_, A>) -> f32
498 where
499 A: Arc,
500 A::Weight: FromScore,
501 {
502 if self.sounding.is_empty() {
503 return 0.0;
504 }
505 let total: f64 = self
506 .sounding
507 .iter()
508 .enumerate()
509 .map(|(frame, &k)| {
510 let column = chain.column((k as usize).checked_sub(1));
511 dense.frame(frame)[column as usize] as f64
512 })
513 .sum();
514 (total / self.sounding.len() as f64) as f32
515 }
516
517 pub fn from_path(chain: &AlignChain, path: &Path) -> Result<Self, OpenFstError> {
530 let mut sounding = Vec::with_capacity(path.num_frames());
531 for (frame, (&code, &position)) in path.codes().iter().zip(path.positions()).enumerate() {
532 let sounds = *SOUNDS.get(code as usize).ok_or_else(|| {
533 OpenFstError::InvalidOperation(format!(
534 "Alignment: transition {code} at frame {frame} is not one of the chain's four"
535 ))
536 })?;
537 sounding.push(if sounds { position } else { 0 });
538 }
539 Ok(Self {
540 sounding,
541 num_phones: chain.phones.len(),
542 cost: path.cost(),
543 })
544 }
545
546 pub fn from_output_labels<L: ArcLabel>(
560 chain: &AlignChain,
561 labels: &[L],
562 cost: f32,
563 ) -> Result<Self, OpenFstError> {
564 let num_phones = chain.phones.len();
565 let silent = num_phones as i64 + 1;
566 let mut sounding = Vec::with_capacity(labels.len());
567 for (frame, label) in labels.iter().enumerate() {
568 let value = label.to_i64().unwrap_or(-1);
569 if value == silent {
570 sounding.push(0);
571 } else if value >= 1 && value < silent {
572 sounding.push(value as u32);
573 } else {
574 return Err(OpenFstError::InvalidOperation(format!(
575 "Alignment: output label {value} at frame {frame} names no position of a \
576 {num_phones}-phone reference"
577 )));
578 }
579 }
580 Ok(Self {
581 sounding,
582 num_phones,
583 cost,
584 })
585 }
586}
587
588#[derive(Debug, Clone, Copy)]
596pub struct ChainTrellis<'a, A: Arc> {
597 chain: &'a AlignChain,
598 dense: &'a DenseFst<'a, A>,
599}
600
601impl<A: Arc> ChainTrellis<'_, A> {
602 #[inline(always)]
604 pub fn chain(&self) -> &AlignChain {
605 self.chain
606 }
607}
608
609impl<A: Arc> Trellis<4> for ChainTrellis<'_, A>
610where
611 A::Weight: FromScore,
612{
613 type Frame<'f>
614 = &'f [f32]
615 where
616 Self: 'f;
617
618 #[inline(always)]
619 fn num_frames(&self) -> usize {
620 self.dense.num_frames()
621 }
622
623 #[inline(always)]
624 fn num_positions(&self) -> usize {
625 self.chain.phones.len()
626 }
627
628 #[inline(always)]
629 fn frame(&self, frame: usize) -> &[f32] {
630 self.dense.frame(frame)
631 }
632
633 #[inline(always)]
636 fn steps_into(&self, frame: &[f32], position: usize) -> [Step; 4] {
637 let blank = Step::new(0, frame[self.chain.blank as usize]);
638 if position == 0 {
639 return [blank, Step::ABSENT, Step::ABSENT, Step::ABSENT];
640 }
641 let phone = frame[self.chain.phones[position - 1] as usize];
642 [
643 blank,
644 Step::new(0, phone),
645 Step::new(1, phone),
646 Step::new(1, self.chain.skips[position - 1] + blank.cost),
647 ]
648 }
649}
650
651impl<A: Arc> ReversibleTrellis<4> for ChainTrellis<'_, A>
652where
653 A::Weight: FromScore,
654{
655 #[inline(always)]
659 fn steps_out_of(&self, frame: &[f32], position: usize) -> [Step; 4] {
660 let blank = Step::new(0, frame[self.chain.blank as usize]);
661 let hold = if position > 0 {
662 Step::new(0, frame[self.chain.phones[position - 1] as usize])
663 } else {
664 Step::ABSENT
665 };
666 let (commit, skip) = if position < self.chain.phones.len() {
667 (
668 Step::new(1, frame[self.chain.phones[position] as usize]),
669 Step::new(1, self.chain.skips[position] + blank.cost),
670 )
671 } else {
672 (Step::ABSENT, Step::ABSENT)
673 };
674 [blank, hold, commit, skip]
675 }
676}
677
678#[inline(always)]
680pub(crate) fn column_read(chain: &AlignChain, code: u8, position: usize) -> u32 {
681 if SOUNDS[code as usize] {
682 chain.phones[position - 1]
683 } else {
684 chain.blank
685 }
686}
687
688pub fn align<A>(
706 chain: &AlignChain,
707 dense: &DenseFst<'_, A>,
708) -> Result<Option<Alignment>, OpenFstError>
709where
710 A: Arc,
711 A::Weight: FromScore,
712{
713 let trellis = chain.against(dense)?;
714 let Some(path) = best_path(&trellis)? else {
715 return Ok(None);
716 };
717 Alignment::from_path(chain, &path).map(Some)
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723 use sicada::arc::StdArc;
724 use sicada::fst::ExpandedFst;
725 use sicada::fsts::vector_fst::StdVectorFst;
726
727 use crate::compact::{DeterminizeLatticeOptions, determinize_lattice};
728 use crate::frontier::DecodeOptions;
729 use crate::lattice::{LatticeDecodeOptions, lattice_decode};
730 use crate::nbest::n_best;
731 use crate::trellis::axioms;
732 use crate::viterbi::viterbi_decode;
733
734 const SYMBOLS: usize = 4;
736
737 fn certain(columns: &[usize]) -> Vec<f32> {
739 let mut scores = vec![10.0; columns.len() * SYMBOLS];
740 for (frame, &column) in columns.iter().enumerate() {
741 scores[frame * SYMBOLS + column] = 0.0;
742 }
743 scores
744 }
745
746 fn recomputed_cost(
753 alignment: &Alignment,
754 chain: &AlignChain,
755 dense: &DenseFst<'_, StdArc>,
756 ) -> f32 {
757 let acoustic: f32 = alignment.acoustic_costs(chain, dense).sum();
758 let skipped: f32 = alignment
759 .skipped()
760 .into_iter()
761 .map(|position| chain.skip_costs()[position])
762 .sum();
763 acoustic + skipped
764 }
765
766 fn by_decoding(chain: &AlignChain, dense: &DenseFst<'_, StdArc>) -> Option<Alignment> {
773 let fst: StdVectorFst = chain.to_fst(1).expect("a chain FST");
774 let decoded =
775 viterbi_decode(&fst, dense, &DecodeOptions::exhaustive()).expect("a decode")?;
776 Some(
777 Alignment::from_output_labels(chain, &decoded.labels, decoded.weight.0)
778 .expect("labels from this chain"),
779 )
780 }
781
782 #[test]
783 fn a_phone_owns_the_frames_that_sound_it() {
784 let scores = certain(&[1, 1, 0, 2]);
786 let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
787 let chain = AlignChain::new(vec![1, 2]);
788
789 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
790 assert_eq!(
791 alignment.frames().collect::<Vec<_>>(),
792 vec![Some(0), Some(0), None, Some(1)]
793 );
794 assert_eq!(alignment.spans(), vec![Some(0..2), Some(3..4)]);
795 assert!(alignment.skipped().is_empty());
796 assert!(alignment.cost().abs() < 1e-6, "{}", alignment.cost());
797 }
798
799 #[test]
802 fn a_group_of_phones_spans_its_first_sounding_frame_to_its_last() {
803 let scores = certain(&[1, 0, 2, 0, 3, 0]);
806 let dense = DenseFst::<StdArc>::new(&scores, 6, SYMBOLS).unwrap();
807 let chain = AlignChain::new(vec![1, 2, 3, 2])
808 .with_uniform_skip_cost(1.0)
809 .unwrap();
810 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
811
812 assert_eq!(alignment.skipped(), vec![3]);
813 assert_eq!(
815 alignment.group_spans(&[2, 2]).unwrap(),
816 vec![Some(0..3), Some(4..5)]
817 );
818 assert_eq!(
820 alignment.group_spans(&[3, 1]).unwrap(),
821 vec![Some(0..5), None]
822 );
823 assert_eq!(alignment.group_spans(&[4]).unwrap(), vec![Some(0..5)]);
824
825 let err = alignment.group_spans(&[2, 1]).unwrap_err();
826 assert!(format!("{err}").contains("groups of 3 phones"), "{err}");
827 }
828
829 #[test]
832 fn a_blank_frame_belongs_to_no_phone() {
833 let scores = certain(&[1, 0, 0, 0, 0, 0, 0, 0, 0]);
835 let dense = DenseFst::<StdArc>::new(&scores, 9, SYMBOLS).unwrap();
836 let chain = AlignChain::new(vec![1]);
837
838 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
839 assert_eq!(
840 alignment.spans(),
841 vec![Some(0..1)],
842 "the phone must not swallow the silence after it"
843 );
844 }
845
846 #[test]
847 fn an_empty_reference_leaves_every_frame_sounding_nothing() {
848 let scores = certain(&[1, 2, 0]);
849 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
850 let chain = AlignChain::new(vec![]);
851
852 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
853 assert!(alignment.frames().all(|sounding| sounding.is_none()));
854 assert_eq!(alignment.spans(), vec![]);
855 assert!(
857 (alignment.cost() - 20.0).abs() < 1e-6,
858 "{}",
859 alignment.cost()
860 );
861 }
862
863 #[test]
864 fn a_reference_longer_than_the_audio_aligns_to_nothing() {
865 let scores = certain(&[1, 2]);
866 let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
867 let chain = AlignChain::new(vec![1, 2, 3]);
868 assert_eq!(align(&chain, &dense).unwrap(), None);
869
870 let chain = chain.with_uniform_skip_cost(0.0).unwrap();
872 assert_eq!(align(&chain, &dense).unwrap(), None);
873 }
874
875 #[test]
876 fn a_phone_the_model_has_no_column_for_is_reported() {
877 let scores = certain(&[1]);
878 let dense = DenseFst::<StdArc>::new(&scores, 1, SYMBOLS).unwrap();
879
880 let err = align(&AlignChain::new(vec![9]), &dense).unwrap_err();
881 assert!(format!("{err}").contains("position 0 is column 9"), "{err}");
882
883 let err = align(&AlignChain::new(vec![1]).with_blank(7), &dense).unwrap_err();
884 assert!(format!("{err}").contains("the blank is column 7"), "{err}");
885 }
886
887 #[test]
894 fn a_phone_with_no_evidence_is_given_up_only_when_that_is_cheaper() {
895 let scores = [
899 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,
903 ];
904 let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
905 let reference = vec![1, 2];
906
907 let cheap = AlignChain::new(reference.clone())
909 .with_skip_costs(&[6.0, 1.0])
910 .unwrap();
911 let alignment = align(&cheap, &dense).unwrap().expect("an alignment");
912 assert_eq!(alignment.skipped(), vec![1]);
913 assert_eq!(alignment.spans()[0], Some(0..2));
914 assert_eq!(alignment.spans()[1], None);
915 assert!(
916 (alignment.cost() - 1.0).abs() < 1e-6,
917 "{}",
918 alignment.cost()
919 );
920
921 let dear = AlignChain::new(reference)
923 .with_skip_costs(&[6.0, 5.0])
924 .unwrap();
925 let alignment = align(&dear, &dense).unwrap().expect("an alignment");
926 assert!(alignment.skipped().is_empty());
927 assert_eq!(alignment.spans(), vec![Some(0..2), Some(2..3)]);
928 assert!(
929 (alignment.cost() - 3.0).abs() < 1e-6,
930 "{}",
931 alignment.cost()
932 );
933 }
934
935 #[test]
938 fn a_skip_that_only_ties_does_not_happen() {
939 let scores = [
941 10.0, 0.0, 10.0, 10.0, 0.0, 10.0, 4.0, 10.0,
943 ];
944 let dense = DenseFst::<StdArc>::new(&scores, 2, SYMBOLS).unwrap();
945 let reference = vec![1, 2];
946
947 let tied = AlignChain::new(reference.clone())
948 .with_skip_costs(&[9.0, 4.0])
949 .unwrap();
950 let alignment = align(&tied, &dense).unwrap().expect("an alignment");
951 assert!(
952 alignment.skipped().is_empty(),
953 "a tie has to keep the reference"
954 );
955 assert_eq!(alignment.spans(), vec![Some(0..1), Some(1..2)]);
956 assert!(
957 (alignment.cost() - 4.0).abs() < 1e-6,
958 "{}",
959 alignment.cost()
960 );
961
962 let under = AlignChain::new(reference)
964 .with_skip_costs(&[9.0, 3.9])
965 .unwrap();
966 let alignment = align(&under, &dense).unwrap().expect("an alignment");
967 assert_eq!(alignment.skipped(), vec![1]);
968 }
969
970 #[test]
971 fn a_skip_cost_that_pays_for_itself_is_refused() {
972 let chain = AlignChain::new(vec![1, 2]);
973 let err = chain.clone().with_skip_costs(&[1.0, -1.0]).unwrap_err();
974 assert!(format!("{err}").contains("position 1"), "{err}");
975 assert!(chain.clone().with_skip_costs(&[f32::NAN, 1.0]).is_err());
976 assert!(
977 chain.clone().with_skip_costs(&[1.0]).is_err(),
978 "wrong count"
979 );
980 assert!(chain.with_uniform_skip_cost(-0.5).is_err());
981 }
982
983 #[test]
984 fn the_alignment_recovers_what_each_frame_paid() {
985 let scores = certain(&[1, 0, 2]);
986 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
987 let chain = AlignChain::new(vec![1, 2]);
988 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
989
990 assert_eq!(
991 alignment.acoustic_costs(&chain, &dense).collect::<Vec<_>>(),
992 vec![0.0, 0.0, 0.0]
993 );
994 assert_eq!(alignment.mean_acoustic_cost(&chain, &dense), 0.0);
995
996 let wrong = AlignChain::new(vec![3, 3]);
998 let alignment = align(&wrong, &dense).unwrap().expect("an alignment");
999 assert!(
1000 alignment.mean_acoustic_cost(&wrong, &dense) > 5.0,
1001 "an unrelated reference has to be visible in the per-frame cost"
1002 );
1003 }
1004
1005 struct Rng(u64);
1007
1008 impl Rng {
1009 fn next(&mut self) -> u64 {
1010 self.0 ^= self.0 << 13;
1011 self.0 ^= self.0 >> 7;
1012 self.0 ^= self.0 << 17;
1013 self.0
1014 }
1015
1016 fn below(&mut self, n: usize) -> usize {
1017 (self.next() % n as u64) as usize
1018 }
1019
1020 fn cost(&mut self) -> f32 {
1023 self.below(1 << 20) as f32 / 4096.0
1024 }
1025 }
1026
1027 fn by_brute_force(
1032 chain: &AlignChain,
1033 dense: &DenseFst<'_, StdArc>,
1034 num_frames: usize,
1035 ) -> Option<f32> {
1036 fn walk(
1037 chain: &AlignChain,
1038 dense: &DenseFst<'_, StdArc>,
1039 num_frames: usize,
1040 frame: usize,
1041 position: usize,
1042 cost: f32,
1043 best: &mut Option<f32>,
1044 ) {
1045 if frame == num_frames {
1046 if position == chain.num_phones() && best.is_none_or(|so_far| cost < so_far) {
1047 *best = Some(cost);
1048 }
1049 return;
1050 }
1051 let scores = dense.frame(frame);
1052 let blank = scores[chain.blank() as usize];
1053 let mut step = |position, extra: f32| {
1054 walk(
1055 chain,
1056 dense,
1057 num_frames,
1058 frame + 1,
1059 position,
1060 cost + extra,
1061 best,
1062 )
1063 };
1064 step(position, blank);
1065 if position > 0 {
1066 step(position, scores[chain.phones()[position - 1] as usize]);
1067 }
1068 if position < chain.num_phones() {
1069 step(position + 1, scores[chain.phones()[position] as usize]);
1070 let skip = chain.skip_costs()[position];
1071 if skip.is_finite() {
1072 step(position + 1, skip + blank);
1073 }
1074 }
1075 }
1076
1077 let mut best = None;
1078 walk(chain, dense, num_frames, 0, 0, 0.0, &mut best);
1079 best
1080 }
1081
1082 #[test]
1084 fn it_agrees_with_enumerating_every_alignment() {
1085 let mut rng = Rng(0x1234_5678_9ABC_DEF1);
1086 let mut compared = 0;
1087
1088 for round in 0..200 {
1089 let num_frames = 1 + rng.below(7);
1090 let num_phones = rng.below(4);
1091 let phones: Vec<u32> = (0..num_phones)
1092 .map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
1093 .collect();
1094 let chain = AlignChain::new(phones);
1095 let chain = if rng.below(2) == 0 {
1097 chain
1098 .with_uniform_skip_cost(rng.below(1 << 12) as f32 / 512.0)
1099 .unwrap()
1100 } else {
1101 chain
1102 };
1103
1104 let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
1105 let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
1106
1107 let expected = by_brute_force(&chain, &dense, num_frames);
1108 let alignment = align(&chain, &dense).unwrap();
1109
1110 match (expected, alignment) {
1111 (None, None) => {}
1112 (Some(expected), Some(alignment)) => {
1113 compared += 1;
1114 assert!(
1115 (alignment.cost() - expected).abs() < 1e-3,
1116 "round {round}: aligner {} against every path's best {expected}",
1117 alignment.cost()
1118 );
1119 assert!(
1121 (recomputed_cost(&alignment, &chain, &dense) - alignment.cost()).abs()
1122 < 1e-3,
1123 "round {round}: the traceback does not add up to the cost"
1124 );
1125 assert_eq!(alignment.num_frames(), num_frames);
1126 }
1127 (expected, alignment) => {
1128 panic!("round {round}: brute force {expected:?}, aligner {alignment:?}")
1129 }
1130 }
1131 }
1132
1133 assert!(compared > 150, "only {compared} rounds had an alignment");
1134 }
1135
1136 #[test]
1140 fn it_agrees_with_decoding_the_chain_as_an_fst() {
1141 let mut rng = Rng(0xFEED_FACE_1234_5678);
1142 let mut compared = 0;
1143
1144 for round in 0..200 {
1145 let num_frames = 1 + rng.below(40);
1146 let num_phones = rng.below(12);
1147 let phones: Vec<u32> = (0..num_phones)
1148 .map(|_| 1 + rng.below(SYMBOLS - 1) as u32)
1149 .collect();
1150 let chain = AlignChain::new(phones);
1151 let chain = if rng.below(2) == 0 {
1152 chain
1153 .with_uniform_skip_cost(rng.below(1 << 12) as f32 / 512.0)
1154 .unwrap()
1155 } else {
1156 chain
1157 };
1158
1159 let scores: Vec<f32> = (0..num_frames * SYMBOLS).map(|_| rng.cost()).collect();
1160 let dense = DenseFst::<StdArc>::new(&scores, num_frames, SYMBOLS).unwrap();
1161
1162 let expected = by_decoding(&chain, &dense);
1163 let alignment = align(&chain, &dense).unwrap();
1164
1165 match (expected, alignment) {
1166 (None, None) => {}
1167 (Some(expected), Some(alignment)) => {
1168 compared += 1;
1169 assert!(
1170 (alignment.cost() - expected.cost()).abs() < 1e-2,
1171 "round {round}: aligner {} against the decoder {}",
1172 alignment.cost(),
1173 expected.cost()
1174 );
1175 assert!(
1176 (recomputed_cost(&alignment, &chain, &dense) - alignment.cost()).abs()
1177 < 1e-2,
1178 "round {round}: the traceback does not add up to the cost"
1179 );
1180 assert_eq!(expected.num_frames(), num_frames, "one label per frame");
1181 }
1182 (expected, alignment) => {
1183 panic!("round {round}: decoder {expected:?}, aligner {alignment:?}")
1184 }
1185 }
1186 }
1187
1188 assert!(compared > 150, "only {compared} rounds had an alignment");
1189 }
1190
1191 #[test]
1194 fn the_chain_decodes_to_alternative_alignments() {
1195 let scores = [
1199 9.0, 0.0, 9.0, 9.0, 1.0, 0.0, 9.0, 9.0, 9.0, 0.0, 9.0, 9.0,
1202 ];
1203 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
1204 let chain = AlignChain::new(vec![1]);
1205 let fst: StdVectorFst = chain.to_fst(1).unwrap();
1206
1207 let lattice = lattice_decode(&fst, &dense, &LatticeDecodeOptions::exhaustive())
1208 .unwrap()
1209 .expect("a lattice");
1210 let compact = determinize_lattice(&lattice, &DeterminizeLatticeOptions::default()).unwrap();
1211 let answers = n_best(&compact, 2).unwrap();
1212 assert_eq!(answers.len(), 2);
1213
1214 let best = Alignment::from_output_labels(&chain, &answers[0].words, answers[0].cost())
1215 .expect("an alignment");
1216 assert_eq!(best.spans(), vec![Some(0..3)], "the phone held throughout");
1217 assert_eq!(
1218 align(&chain, &dense).unwrap().unwrap().spans(),
1219 best.spans(),
1220 "and it is what the exact aligner returns"
1221 );
1222
1223 let second = Alignment::from_output_labels(&chain, &answers[1].words, answers[1].cost())
1224 .expect("an alignment");
1225 assert_eq!(
1226 second.frames().collect::<Vec<_>>(),
1227 vec![Some(0), None, Some(0)]
1228 );
1229 assert!((second.cost() - best.cost() - 1.0).abs() < 1e-5);
1230 }
1231
1232 #[test]
1233 fn labels_from_another_chain_are_reported() {
1234 let chain = AlignChain::new(vec![1, 2]);
1235 assert!(Alignment::from_output_labels(&chain, &[1i32, 3], 0.0).is_ok());
1237 let err = Alignment::from_output_labels(&chain, &[1i32, 4], 0.0).unwrap_err();
1238 assert!(format!("{err}").contains("names no position"), "{err}");
1239 assert!(Alignment::from_output_labels(&chain, &[0i32], 0.0).is_err());
1240 }
1241
1242 #[test]
1243 fn a_chain_fst_puts_its_columns_where_the_matrix_has_them() {
1244 let chain = AlignChain::new(vec![1, 2])
1245 .with_uniform_skip_cost(1.0)
1246 .unwrap();
1247 let fst: StdVectorFst = chain.to_fst(1).unwrap();
1248 assert_eq!(fst.num_states(), 3);
1249 assert_eq!(fst.num_arcs(0), 3);
1252 assert_eq!(fst.num_arcs(1), 4);
1253 assert_eq!(fst.num_arcs(2), 2);
1254 assert!(
1255 fst.states()
1256 .all(|s| fst.arcs(s).all(|arc| arc.ilabel() != 0)),
1257 "every arc has to consume a frame"
1258 );
1259
1260 let fst: StdVectorFst = AlignChain::new(vec![1, 2]).to_fst(1).unwrap();
1262 assert_eq!(fst.num_arcs(0), 2);
1263 assert!(AlignChain::new(vec![1]).to_fst::<StdArc>(0).is_err());
1264 }
1265
1266 #[test]
1270 fn the_chain_obeys_the_trellis_contract() {
1271 let chain = AlignChain::new(vec![1, 2, 1])
1272 .with_skip_costs(&[1.0, 2.0, f32::INFINITY])
1273 .unwrap();
1274 let scores: Vec<f32> = (0..4 * SYMBOLS).map(|i| i as f32 / 3.0).collect();
1275 let dense = DenseFst::<StdArc>::new(&scores, 4, SYMBOLS).unwrap();
1276 axioms::check(&chain.against(&dense).unwrap());
1277
1278 let rigid = AlignChain::new(vec![1, 2, 1]);
1281 axioms::check(&rigid.against(&dense).unwrap());
1282 axioms::check(&AlignChain::new(vec![]).against(&dense).unwrap());
1283 }
1284
1285 #[test]
1289 fn a_reference_as_long_as_the_audio_has_one_alignment() {
1290 let scores = certain(&[1, 2, 3]);
1291 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
1292 let chain = AlignChain::new(vec![1, 2, 3]);
1293
1294 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
1295 assert_eq!(
1296 alignment.frames().collect::<Vec<_>>(),
1297 vec![Some(0), Some(1), Some(2)]
1298 );
1299 assert!(alignment.cost().abs() < 1e-6);
1300
1301 let scores = [0.0, 10.0, 10.0, 10.0].repeat(3);
1303 let dense = DenseFst::<StdArc>::new(&scores, 3, SYMBOLS).unwrap();
1304 let alignment = align(&chain, &dense).unwrap().expect("an alignment");
1305 assert_eq!(
1306 alignment.frames().collect::<Vec<_>>(),
1307 vec![Some(0), Some(1), Some(2)]
1308 );
1309 }
1310}