#![allow(clippy::type_complexity)]
use super::*;
fn assert_send<T: Send>() {}
#[test]
fn align_work_item_is_send() {
assert_send::<AlignWorkItem>();
}
#[test]
fn data_dependent_failures_are_recoverable() {
let make = |variant: fn(AlignmentFailure) -> AlignmentError| {
WorkFailure::Alignment(variant(AlignmentFailure::new(
SmolStr::new(""),
crate::types::Lang::En,
)))
};
let recoverable: [(&str, fn(AlignmentFailure) -> AlignmentError); 2] = [
("NoAlignmentPath", AlignmentError::NoAlignmentPath),
("EmptyText", AlignmentError::EmptyText),
];
for (name, ctor) in recoverable {
let f = make(ctor);
assert!(
alignment_failure_is_recoverable(&f),
"{name} must preserve ASR text",
);
}
}
#[test]
#[cfg_attr(
not(asry_w2v_en),
ignore = "needs the English wav2vec2 fixture: ASRY_FETCH_W2V=en cargo test --features alignment"
)]
fn too_short_chunk_recovers_to_empty_result() {
use crate::runner::aligner::{
AlignerKey, AlignmentSetBuilder, EnglishNormalizer, test_fixtures::english_aligner,
};
const ASR_TEXT: &str = "hello world";
let set = AlignmentSetBuilder::new()
.with_fallback(AlignmentFallback::Error)
.register(
AlignerKey::Lang(Lang::En),
english_aligner(Box::new(EnglishNormalizer::new())),
)
.build();
assert!(
matches!(set.lookup(&Lang::En), AlignmentLookup::Hit { .. }),
"the recovery under test lives on the Hit path; a Miss would prove nothing"
);
let job = AlignWorkItem::new(
AlignmentRequest::for_test(
ChunkId::from_raw(0),
core::num::NonZeroU64::new(1).expect("1 != 0"),
Arc::from(vec![0.0_f32; 200]),
SmolStr::new(ASR_TEXT),
Lang::En,
Vec::new(),
),
Arc::new(AtomicBool::new(false)),
);
let run_options = RunOptions::new().expect("RunOptions::new");
let resolution = set
.detect_oov(&job)
.expect("detect_oov")
.decide(crate::core::default_oov_policy);
assert_eq!(
job.text().as_str(),
ASR_TEXT,
"the input work item carries the ASR text (input sanity, not the preservation proof)"
);
let completion = run_one_alignment(&set, job, resolution, &run_options);
let result = completion.report().expect(
"`NoAlignmentPath` is classified recoverable, so the pool must absorb it into the unit's \
outcome, naming the failure. A failed completion here would turn a chunk carrying a \
perfectly good ASR transcript into Event::Error — alignment is best-effort, never \
destructive.",
);
assert!(
matches!(
result.units().collect::<Vec<_>>().as_slice(),
[(
crate::core::AlignmentUnit::Whole,
UnitAlignment::Unaligned(UnalignedCause::Failed(AlignmentError::NoAlignmentPath(_)))
)]
),
"the whole text's one outcome names the recovered failure, never a bare empty list; got \
{result:?}"
);
}
#[test]
fn backend_alignment_failures_stay_fatal() {
let make = |variant: fn(AlignmentFailure) -> AlignmentError| {
WorkFailure::Alignment(variant(AlignmentFailure::new(SmolStr::new(""), Lang::En)))
};
let fatal: [(&str, fn(AlignmentFailure) -> AlignmentError); 3] = [
("ModelInference", AlignmentError::ModelInference),
("Tokenization", AlignmentError::Tokenization),
("Normalization", AlignmentError::Normalization),
];
for (name, ctor) in fatal {
let f = make(ctor);
assert!(
!alignment_failure_is_recoverable(&f),
"{name} signals a backend/config bug; must propagate",
);
}
}
#[test]
fn liveness_and_registry_failures_stay_fatal() {
use core::time::Duration;
use crate::types::{AsrError, AsrFailure, Lang, WorkerKind};
assert!(!alignment_failure_is_recoverable(&WorkFailure::WorkerHang(
WorkerHangTimeout::new(WorkerKind::Alignment, Duration::from_secs(30))
)));
assert!(!alignment_failure_is_recoverable(
&WorkFailure::LanguageUnsupported(LanguageUnsupportedForAlignment::new(Lang::En))
));
assert!(!alignment_failure_is_recoverable(&WorkFailure::Asr(
AsrError::AllTemperaturesExhausted(AsrFailure::new(SmolStr::new("")))
)));
}
#[test]
fn bounds_source_counters_accumulate_distribution() {
use crate::align::BoundsSource;
let mut c = BoundsSourceCounters::default();
c.observe_bounds(BoundsSource::Dtw);
c.observe_bounds(BoundsSource::Dtw);
c.observe_bounds(BoundsSource::Segment);
c.observe_bounds(BoundsSource::Wholeclip);
c.observe_unaligned();
c.observe_unaligned();
assert_eq!(c.runs_total(), 4);
assert_eq!(c.runs_dtw(), 2);
assert_eq!(c.runs_segment(), 1);
assert_eq!(c.runs_wholeclip(), 1);
assert_eq!(c.runs_unaligned(), 2);
}
#[test]
fn bounds_source_counters_default_is_zero() {
let c = BoundsSourceCounters::default();
assert_eq!(c.runs_total(), 0);
assert_eq!(c.runs_dtw(), 0);
assert_eq!(c.runs_segment(), 0);
assert_eq!(c.runs_wholeclip(), 0);
assert_eq!(c.runs_unaligned(), 0);
}
#[test]
fn run_audio_slice_segment_bounds_clamp_to_chunk_length() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
100,
300,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 1_600);
assert_eq!(hi, 4_800);
}
#[test]
fn run_audio_slice_wholeclip_uses_full_chunk() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
i64::MIN,
i64::MAX,
0,
BoundsSource::Wholeclip,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 16_000);
}
#[test]
fn run_audio_slice_inverted_bounds_collapse_to_empty_slice() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
500,
100,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 0);
}
#[test]
fn run_audio_slice_negative_t0_collapses_to_empty_slice() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
-10,
100,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 0);
assert_eq!(hi, 0);
}
#[test]
fn run_audio_slice_out_of_chunk_t0_collapses_to_empty_slice_at_end() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
5_000,
6_000,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 0);
assert_eq!(lo, 16_000);
assert_eq!(hi, 16_000);
}
#[test]
fn run_audio_slice_ignores_chunk_first_sample_in_stream() {
use crate::align::{BoundsSource, Run};
use smol_str::SmolStr;
let r = Run::new(
Lang::En,
SmolStr::new("hi"),
100,
500,
0,
BoundsSource::Segment,
);
let (lo, hi) = run_audio_slice(&r, 16_000, 1_000_000_000);
assert_eq!(lo, 1600);
assert_eq!(hi, 8000);
}
#[test]
fn clip_sub_segments_offsets_into_run_local_space() {
use core::num::NonZeroI32;
let tb = mediatime::Timebase::new(1, NonZeroI32::new(16_000).unwrap());
let subs = vec![
TimeRange::new(2_000, 3_000, tb),
TimeRange::new(800, 2_400, tb),
TimeRange::new(8_000, 9_000, tb),
];
let out = clip_sub_segments(&subs, 1_600, 4_800, &Lang::En).expect("ok");
assert_eq!(out.len(), 2);
assert_eq!(out[0].start_pts(), 400);
assert_eq!(out[0].end_pts(), 1_400);
assert_eq!(out[1].start_pts(), 0);
assert_eq!(out[1].end_pts(), 800);
}
#[test]
fn sort_words_by_pts_orders_overlapping_runs() {
use crate::core::sort_words_by_pts;
use core::num::NonZeroI32;
use mediatime::Timebase;
let tb = Timebase::new(1, NonZeroI32::new(16_000).unwrap());
let mk = |start: i64, end: i64, text: &str| {
crate::types::Word::new(SmolStr::new(text), TimeRange::new(start, end, tb), 1.0)
};
let mut words = vec![
mk(8000, 9000, "world"),
mk(0, 1000, "hello"),
mk(4000, 5000, "there"),
];
sort_words_by_pts(&mut words);
let texts: Vec<&str> = words.iter().map(|w| w.text()).collect();
assert_eq!(texts, vec!["hello", "there", "world"]);
let mut prev = i64::MIN;
for w in &words {
let s = w.range().start_pts();
assert!(
s >= prev,
"word starts must be monotone; got {s} after {prev}"
);
prev = s;
}
}
#[test]
fn sort_words_by_pts_breaks_ties_by_end_pts() {
use crate::core::sort_words_by_pts;
use core::num::NonZeroI32;
use mediatime::Timebase;
let tb = Timebase::new(1, NonZeroI32::new(16_000).unwrap());
let mk = |start: i64, end: i64, text: &str| {
crate::types::Word::new(SmolStr::new(text), TimeRange::new(start, end, tb), 1.0)
};
let mut words = vec![mk(0, 2000, "longer"), mk(0, 1000, "shorter")];
sort_words_by_pts(&mut words);
assert_eq!(words[0].text(), "shorter");
assert_eq!(words[1].text(), "longer");
}
#[test]
fn check_abort_between_runs_returns_timeout_when_flag_set() {
let started = Instant::now();
let flag = AtomicBool::new(true);
let result = check_abort_between_runs(&flag, started);
assert!(
matches!(result, Err(WorkFailure::WorkerHang(_))),
"abort flag set → expected WorkerHangTimeout(Alignment); got {result:?}",
);
}
#[test]
fn semantic_oov_is_recoverable() {
use crate::types::Lang;
let f = WorkFailure::Alignment(AlignmentError::SemanticOutOfVocab(AlignmentFailure::new(
SmolStr::new("pronounced symbol"),
Lang::En,
)));
assert!(
alignment_failure_is_recoverable(&f),
"SemanticOutOfVocab must recover so ASR text isn't lost",
);
}
#[test]
fn tokenization_failed_stays_fatal() {
use crate::types::Lang;
let f = WorkFailure::Alignment(AlignmentError::Tokenization(AlignmentFailure::new(
SmolStr::new(""),
Lang::En,
)));
assert!(
!alignment_failure_is_recoverable(&f),
"TokenizationFailed signals a tokenizer/model mismatch; must stay fatal",
);
}
#[test]
fn check_abort_between_runs_passes_through_when_flag_clear() {
let started = Instant::now();
let flag = AtomicBool::new(false);
assert!(check_abort_between_runs(&flag, started).is_ok());
}
#[test]
fn a_per_run_job_whose_runs_do_not_reproduce_its_text_is_refused() {
use crate::align::BoundsSource;
let run = |text: &str| {
Run::new(
Lang::En,
SmolStr::new(text),
0,
1_000,
0,
BoundsSource::Segment,
)
};
let text = "hello 4, & 50%.";
assert!(validate_runs_reproduce_text(&[], text, &Lang::En).is_ok());
assert!(
validate_runs_reproduce_text(&[run("hello"), run(" 4, & 50%.")], text, &Lang::En).is_ok()
);
for (runs, text) in [
(vec![run("hello")], text),
(vec![run("hello"), run(" 4")], text),
(vec![run("hello"), run(" 4, 50%.")], text),
(vec![run("hello"), run(" 4, & 50%")], text),
(vec![run("a bc")], "ab c"),
(vec![run("don't")], "dont"),
] {
match validate_runs_reproduce_text(&runs, text, &Lang::En) {
Err(WorkFailure::Alignment(AlignmentError::Tokenization(failure))) => assert!(
failure.message().contains("do not reproduce"),
"{}",
failure.message()
),
other => panic!("{text:?}: expected a Tokenization refusal; got {other:?}"),
}
}
}
fn per_run_job(chunk: u64, runs: Vec<Run>) -> AlignWorkItem {
let text: String = runs.iter().map(Run::text).collect();
AlignWorkItem::new(
AlignmentRequest::for_test(
ChunkId::from_raw(chunk),
core::num::NonZeroU64::new(1).expect("1 != 0"),
Arc::from(vec![0.0_f32; 1_600]),
SmolStr::new(text),
Lang::Ko,
runs,
),
Arc::new(AtomicBool::new(false)),
)
}
fn korean_run(text: &str, source_segment_idx: i32) -> Run {
Run::new(
Lang::Ko,
SmolStr::new(text),
0,
100,
source_segment_idx,
crate::align::BoundsSource::Segment,
)
}
fn empty_registry(fallback: AlignmentFallback) -> AlignmentSet {
crate::runner::aligner::AlignmentSetBuilder::new()
.with_fallback(fallback)
.build()
}
#[test]
fn a_unit_no_aligner_can_read_is_resolved_policy_first() {
use crate::core::{
AlignmentUnit, OovDetection, OovEvent, OovKind, default_oov_policy, fail_closed_all_policy,
};
let set = empty_registry(AlignmentFallback::SkipChunk);
let job = per_run_job(0, vec![korean_run(" 4", 0)]);
let decided = |policy: fn(&OovEvent) -> OovDecision| {
let detection = set.detect_oov(&job).expect("detect_oov");
let [unit] = detection.units() else {
panic!("one run, one unit");
};
assert_eq!(
unit.events().to_vec(),
vec![OovEvent::new(OovKind::NotInspected, 0, 0, Lang::Ko)],
"a unit no aligner can read is one NotInspected event, never an empty list"
);
detection
.decide(policy)
.into_units_for(&job, set.id())
.expect("this job, this set")
.pop()
.expect("one unit")
};
let refused = decided(fail_closed_all_policy);
let wildcard = decided(default_oov_policy);
for fallback in [AlignmentFallback::SkipChunk, AlignmentFallback::Error] {
assert!(
matches!(
resolve_not_inspected(&refused, fallback, &Lang::Ko),
Ok(UnalignedCause::Refused)
),
"{fallback:?}: FailClosed refuses before any fallback"
);
}
assert!(matches!(
resolve_not_inspected(&wildcard, AlignmentFallback::SkipChunk, &Lang::Ko),
Ok(UnalignedCause::Skipped)
));
assert!(matches!(
resolve_not_inspected(&wildcard, AlignmentFallback::Error, &Lang::Ko),
Err(WorkFailure::LanguageUnsupported(_))
));
let read = OovDetection::of_unit(
AlignmentUnit::Run(0),
Lang::Ko,
vec![OovEvent::new(OovKind::Symbol('4'), 1, 0, Lang::Ko)],
Some(core::num::NonZeroU64::new(9).expect("9 != 0")),
)
.decide(fail_closed_all_policy);
assert!(matches!(
resolve_not_inspected(&read, AlignmentFallback::SkipChunk, &Lang::Ko),
Err(WorkFailure::Alignment(AlignmentError::Tokenization(_)))
));
}
#[test]
fn a_resolution_applies_only_to_the_job_it_was_detected_for() {
use crate::core::{AlignmentUnit, fail_closed_all_policy, wildcard_all_policy};
let set = empty_registry(AlignmentFallback::SkipChunk);
let layout = || vec![korean_run(" 4", 0), korean_run(" 4", 1)];
let first = per_run_job(7, layout());
let replayed_into = per_run_job(7, layout());
let units = set
.detect_oov(&first)
.expect("detect_oov")
.decide(wildcard_all_policy)
.into_units_for(&first, set.id())
.expect("the job's own resolution");
assert_eq!(
units.iter().map(|unit| unit.unit()).collect::<Vec<_>>(),
[AlignmentUnit::Run(0), AlignmentUnit::Run(1)]
);
let resolution = set
.detect_oov(&first)
.expect("detect_oov")
.decide(fail_closed_all_policy);
assert_eq!(resolution.chunk_id(), replayed_into.chunk_id());
match resolution.into_units_for(&replayed_into, set.id()) {
Err(WorkFailure::Alignment(AlignmentError::Tokenization(failure))) => assert!(
failure.message().contains("detected for another job"),
"{}",
failure.message()
),
other => panic!("a resolution replayed into another job must be refused; got {other:?}"),
}
}
#[test]
fn a_registry_swapped_between_detection_and_dispatch_is_refused() {
use crate::core::default_oov_policy;
for fallback in [AlignmentFallback::SkipChunk, AlignmentFallback::Error] {
let a = empty_registry(fallback);
let b = empty_registry(fallback);
let job = per_run_job(0, vec![korean_run(" 4", 0)]);
let from_a = a
.detect_oov(&job)
.expect("detect_oov")
.decide(default_oov_policy);
match from_a.into_units_for(&job, b.id()) {
Err(WorkFailure::Alignment(AlignmentError::Tokenization(failure))) => assert!(
failure.message().contains("through another AlignmentSet"),
"{fallback:?}: {}",
failure.message()
),
other => panic!("{fallback:?}: A's resolution is no resolution for B; got {other:?}"),
}
}
}
fn transcriber_awaiting_alignment(
text: &str,
runs: Vec<Run>,
) -> (crate::core::Transcriber, AlignmentRequest) {
use core::num::NonZeroI32;
use crate::{
core::{AsrResult, Command, Transcriber, TranscriberOptions},
types::VadSegment,
};
let mut t = Transcriber::new(TranscriberOptions::default().with_word_alignment(true));
let tb = mediatime::Timebase::new(1, NonZeroI32::new(16_000).expect("16000 != 0"));
t.handle_samples(mediatime::Timestamp::new(0, tb), &vec![0.1_f32; 16_000])
.expect("samples");
t.handle_vad_segment(VadSegment::new(0, 16_000))
.expect("a VAD segment over the audio");
t.handle_eof().expect("eof");
let Some(Command::Asr { chunk_id, .. }) = t.poll_command() else {
panic!("the chunk asks for ASR");
};
t.handle_asr(
chunk_id,
AsrResult::new(SmolStr::new(text), Lang::Ko, -0.5, 0.05, 0.0).with_runs(runs),
)
.expect("a non-empty ASR result under word alignment");
let Some(Command::Alignment(request)) = t.poll_command() else {
panic!("the chunk asks for alignment");
};
(t, request)
}
#[test]
fn a_pool_completion_answers_only_its_own_command() {
use crate::{
core::{Event, RefusedCompletion, UnitAlignment, default_oov_policy},
types::TranscriberError,
};
let foreign = |outcome: Result<(), RefusedCompletion>| match outcome {
Err(refused) => {
match refused.error() {
TranscriberError::ForeignAlignment(foreign) => {
assert_eq!(foreign.chunk_id(), ChunkId::from_raw(0));
assert!(foreign.another_transcriber());
}
other => panic!("another transcriber's completion is refused by name; got {other:?}"),
}
refused.into_completion()
}
Ok(()) => panic!("another transcriber's completion must be refused"),
};
let run_job = |set: &AlignmentSet, fallback: AlignmentFallback, request: AlignmentRequest| {
let job = AlignWorkItem::new(request, Arc::new(AtomicBool::new(false)));
let resolution = set
.detect_oov(&job)
.expect("detect_oov")
.decide(default_oov_policy);
answer_job(job, |job, units| {
let resolutions = resolution.into_units_for(job, set.id())?;
resolutions
.iter()
.zip(units)
.map(|(resolution, unit)| {
resolve_not_inspected(resolution, fallback, job.language())
.map(|cause| unit.answer(UnitAlignment::Unaligned(cause)))
})
.collect()
})
};
for runs in [
Vec::new(),
vec![korean_run("hello", 0), korean_run(" world", 1)],
] {
let skip = AlignmentFallback::SkipChunk;
let registry = empty_registry(skip);
let (mut a, from_a) = transcriber_awaiting_alignment("hello world", runs.clone());
let (mut z, from_z) = transcriber_awaiting_alignment("hello world", runs.clone());
let (done_a, done_z) = (
run_job(®istry, skip, from_a),
run_job(®istry, skip, from_z),
);
assert!(done_a.report().is_some() && done_z.report().is_some());
let back_to_z = foreign(a.complete(done_z));
let back_to_a = foreign(z.complete(done_a));
assert_eq!(
(a.in_flight_chunk_count(), z.in_flight_chunk_count()),
(1, 1),
"the swap resolves nothing"
);
a.complete(back_to_a)
.expect("handed back, the transcriber that issued its command takes it");
z.complete(back_to_z)
.expect("handed back, the transcriber that issued its command takes it");
for transcriber in [&mut a, &mut z] {
match transcriber.poll_event() {
Some(Event::Transcript(t)) => {
assert_eq!(t.alignment().units().len(), runs.len().max(1));
assert!(
t.alignment().units().all(|(_, alignment)| matches!(
alignment,
UnitAlignment::Unaligned(UnalignedCause::Skipped)
)),
"{:?}",
t.alignment()
);
}
other => panic!("expected the transcript; got {other:?}"),
}
}
let error = AlignmentFallback::Error;
let registry = empty_registry(error);
let (mut a, from_a) = transcriber_awaiting_alignment("hello world", runs.clone());
let (mut z, from_z) = transcriber_awaiting_alignment("hello world", runs.clone());
let failed_a = run_job(®istry, error, from_a);
assert!(matches!(
failed_a.failure(),
Some(WorkFailure::LanguageUnsupported(_))
));
let back_to_a = foreign(z.complete(failed_a));
assert_eq!(
z.in_flight_chunk_count(),
1,
"a refused failure resolves nothing"
);
z.complete(run_job(®istry, error, from_z))
.expect("z's own failure resolves its chunk");
assert!(matches!(z.poll_event(), Some(Event::Error { .. })));
a.complete(back_to_a)
.expect("a's failure answers a's command");
assert!(matches!(a.poll_event(), Some(Event::Error { .. })));
let (mut a, from_a) = transcriber_awaiting_alignment("hello world", runs.clone());
let job = AlignWorkItem::new(from_a, Arc::new(AtomicBool::new(false)));
let panicked = answer_job(job, |_, _| panic!("an aligner fault"));
match panicked.failure() {
Some(WorkFailure::Alignment(AlignmentError::ModelInference(failure))) => assert!(
failure.message().contains("an aligner fault"),
"{}",
failure.message()
),
other => panic!("a panicking job answers as a failure; got {other:?}"),
}
a.complete(panicked)
.expect("the panicked job's failure answers its command");
assert!(matches!(a.poll_event(), Some(Event::Error { .. })));
}
}
#[test]
fn a_run_outcome_is_the_same_on_the_pool_and_direct_roads() {
use crate::core::{AlignedWords, AlignmentReport, UnitAlignment};
let tb = mediatime::Timebase::new(1, core::num::NonZeroI32::new(16_000).expect("16000 != 0"));
let aligned = || {
UnitAlignment::Aligned(
AlignedWords::new(vec![crate::types::Word::new(
SmolStr::new("hello"),
TimeRange::new(0, 10, tb),
0.9,
)])
.expect("a word"),
)
};
let runs = || vec![korean_run("hello", 0), korean_run(" world", 1)];
let words = |report: Option<&AlignmentReport>| -> Vec<crate::types::Word> {
match report {
Some(AlignmentReport::Runs(units)) => units
.iter()
.flat_map(|unit| unit.words().to_vec())
.collect(),
other => panic!("expected each run's outcome; got {other:?}"),
}
};
let (_pool_transcriber, request) = transcriber_awaiting_alignment("hello world", runs());
let job = AlignWorkItem::new(request, Arc::new(AtomicBool::new(false)));
let pooled = answer_job(job, |_, units| {
Ok(
units
.into_iter()
.map(|unit| unit.answer(aligned()))
.collect(),
)
});
let (_direct_transcriber, mut request) = transcriber_awaiting_alignment("hello world", runs());
let outcomes = request
.take_units()
.into_iter()
.map(|unit| unit.answer(aligned()))
.collect();
let direct = request.aligned(outcomes).expect("its own units, in order");
let (pooled, direct) = (words(pooled.report()), words(direct.report()));
assert_eq!(format!("{pooled:?}"), format!("{direct:?}"));
assert!(pooled.iter().all(|word| word.language() == Some(&Lang::Ko)));
}
#[test]
fn a_detection_failure_answers_its_chunk() {
use crate::core::Event;
for runs in [
Vec::new(),
vec![korean_run("hello", 0), korean_run(" world", 1)],
] {
let (mut t, request) = transcriber_awaiting_alignment("hello world", runs);
let job = AlignWorkItem::new(request, Arc::new(AtomicBool::new(false)));
let failure = WorkFailure::Alignment(AlignmentError::Normalization(AlignmentFailure::new(
SmolStr::new_static("the normaliser's rule failed"),
Lang::Ko,
)));
let completion = job.failed(failure);
assert!(matches!(
completion.failure(),
Some(WorkFailure::Alignment(AlignmentError::Normalization(_)))
));
t.complete(completion)
.expect("the failure answers the job's own command");
assert!(matches!(t.poll_event(), Some(Event::Error { .. })));
assert_eq!(t.in_flight_chunk_count(), 0);
}
}
#[test]
fn clip_sub_segments_rejects_non_16000_timebase() {
use core::num::NonZeroI32;
let tb_48k = mediatime::Timebase::new(1, NonZeroI32::new(48_000).unwrap());
let subs = vec![TimeRange::new(2_000, 3_000, tb_48k)];
let result = clip_sub_segments(&subs, 1_600, 4_800, &Lang::En);
match result {
Err(WorkFailure::Alignment(AlignmentError::ModelInference(payload))) => {
let message = payload.message();
assert!(
message.contains("1/16000") && message.contains("48000"),
"expected diagnostic citing both timebases; got {message}",
message = message,
);
}
other => panic!("expected ModelInferenceFailed, got {other:?}"),
}
}