use std::{
sync::{Arc, atomic::AtomicBool},
vec::Vec,
};
use mediatime::TimeRange;
use smol_str::{SmolStr, format_smolstr};
use core::sync::atomic::Ordering;
use std::{sync::Mutex, time::Instant};
use ort::session::RunOptions;
use crate::{
align::Run,
core::{AlignmentResult, ResolvedOov},
runner::aligner::{Aligner, AlignmentFallback, AlignmentLookup, AlignmentSet},
types::{
AlignmentError, AlignmentFailure, ChunkId, Lang, LanguageUnsupportedForAlignment, Word,
WorkFailure, WorkerHangTimeout, WorkerKind,
},
};
pub struct AlignWorkItem {
chunk_id: ChunkId,
samples: Arc<[f32]>,
sub_segments: Vec<TimeRange>,
text: SmolStr,
language: Lang,
runs: Vec<Run>,
abort_flag: Arc<AtomicBool>,
chunk_first_sample_in_stream: u64,
samples_to_output_range: Arc<dyn Fn(u64, u64) -> TimeRange + Send + Sync>,
oov_decisions: Vec<Vec<ResolvedOov>>,
}
impl AlignWorkItem {
#[allow(
clippy::too_many_arguments,
reason = "mirrors `Command::Alignment` fields + caller-owned abort_flag; \
destructured-pattern callers naturally line them up positionally"
)]
pub fn from_run_alignment(
transcriber: &crate::core::Transcriber,
chunk_id: ChunkId,
samples: Arc<[f32]>,
text: SmolStr,
language: Lang,
runs: Vec<Run>,
abort_flag: Arc<AtomicBool>,
oov_decisions: Vec<Vec<ResolvedOov>>,
) -> Option<Self> {
use core::num::NonZeroU32;
let chunk_first = transcriber.chunk_first_sample(chunk_id)?;
let raw_subs = transcriber.chunk_sub_segments_samples(chunk_id)?;
let bridge = transcriber.chunk_samples_to_output_range_fn(chunk_id)?;
let tb_16k = mediatime::Timebase::new(1, NonZeroU32::new(16_000).unwrap());
let aligner_subs: Vec<TimeRange> = raw_subs
.iter()
.map(|(s, e)| {
TimeRange::new(
(*s as i64) - (chunk_first as i64),
(*e as i64) - (chunk_first as i64),
tb_16k,
)
})
.collect();
Some(Self {
chunk_id,
samples,
sub_segments: aligner_subs,
text,
language,
runs,
abort_flag,
chunk_first_sample_in_stream: chunk_first,
samples_to_output_range: bridge,
oov_decisions,
})
}
#[must_use]
pub const fn chunk_id(&self) -> ChunkId {
self.chunk_id
}
#[must_use]
pub fn samples(&self) -> &Arc<[f32]> {
&self.samples
}
#[must_use]
pub fn sub_segments(&self) -> &[TimeRange] {
&self.sub_segments
}
#[must_use]
pub fn text(&self) -> &SmolStr {
&self.text
}
#[must_use]
pub const fn language(&self) -> &Lang {
&self.language
}
#[must_use]
pub fn runs(&self) -> &[Run] {
&self.runs
}
#[must_use]
pub fn abort_flag(&self) -> &Arc<AtomicBool> {
&self.abort_flag
}
#[must_use]
pub const fn chunk_first_sample_in_stream(&self) -> u64 {
self.chunk_first_sample_in_stream
}
#[must_use]
pub fn samples_to_output_range(&self) -> &Arc<dyn Fn(u64, u64) -> TimeRange + Send + Sync> {
&self.samples_to_output_range
}
#[must_use]
pub fn oov_decisions(&self) -> &[Vec<ResolvedOov>] {
&self.oov_decisions
}
}
pub fn run_one_alignment(
set: &AlignmentSet,
job: &AlignWorkItem,
run_options: &RunOptions,
) -> Result<AlignmentResult, WorkFailure> {
let started_at = Instant::now();
if job.abort_flag.load(Ordering::Relaxed) {
return Err(WorkFailure::WorkerHang(WorkerHangTimeout::new(
WorkerKind::Alignment,
started_at.elapsed(),
)));
}
let outer = job.oov_decisions.len();
let expected = if job.runs.is_empty() {
1
} else {
job.runs.len()
};
if outer != 0 && outer != expected {
return Err(WorkFailure::Alignment(AlignmentError::Tokenization(
AlignmentFailure::new(
format_smolstr!(
"AlignWorkItem::oov_decisions outer shape mismatch: \
expected 0 (no OOV) or {expected} ({}), got {outer}. \
This typically means stale per-run decisions are being \
applied to a whole-chunk job (or vice versa). Recompute \
decisions for this chunk's text via \
`AlignmentSet::detect_oov` / `detect_oov_per_run`.",
if job.runs.is_empty() {
"exactly one whole-chunk vec"
} else {
"one inner vec per run"
},
),
job.language.clone(),
),
)));
}
validate_oov_decision_languages(&job.runs, &job.language, &job.oov_decisions)?;
let outcome = if job.runs.is_empty() {
match set.lookup(&job.language) {
AlignmentLookup::Hit { aligner, .. } => {
run_under_lock(aligner, job, run_options, &job.abort_flag)
}
AlignmentLookup::AnyFallback { aligner } => {
run_under_lock(aligner, job, run_options, &job.abort_flag)
}
AlignmentLookup::Miss { fallback } => match fallback {
AlignmentFallback::SkipChunk => Ok(AlignmentResult::new(Vec::new())),
AlignmentFallback::Error => Err(WorkFailure::LanguageUnsupported(
LanguageUnsupportedForAlignment::new(job.language.clone()),
)),
},
}
} else {
dispatch_runs(set, job, run_options)
};
match outcome {
Ok(_) => outcome,
Err(ref f) if alignment_failure_is_recoverable(f) => {
if let WorkFailure::Alignment(err) = f {
let language = match err {
AlignmentError::ModelInference(p)
| AlignmentError::Tokenization(p)
| AlignmentError::Normalization(p)
| AlignmentError::NoAlignmentPath(p)
| AlignmentError::EmptyText(p)
| AlignmentError::SemanticOutOfVocab(p)
| AlignmentError::Aborted(p) => p.language(),
};
eprintln!(
"asry alignment recovered chunk={:?} kind={err:?} language={language:?}",
job.chunk_id,
);
}
Ok(AlignmentResult::new(Vec::new()))
}
Err(WorkFailure::WorkerHang(timeout)) => Err(WorkFailure::WorkerHang(WorkerHangTimeout::new(
timeout.kind(),
started_at.elapsed(),
))),
Err(_) => outcome,
}
}
fn validate_oov_decision_languages(
runs: &[Run],
job_language: &Lang,
oov_decisions: &[Vec<ResolvedOov>],
) -> Result<(), WorkFailure> {
if runs.is_empty() {
if let Some(chunk_decisions) = oov_decisions.first() {
for (i, resolved) in chunk_decisions.iter().enumerate() {
if resolved.event().language() != job_language {
return Err(WorkFailure::Alignment(AlignmentError::Tokenization(
AlignmentFailure::new(
format_smolstr!(
"AlignWorkItem::oov_decisions[0][{i}].event.language = {:?} but \
job.language = {:?}. This typically means stale decisions from a \
previous chunk's run leaked into a whole-chunk job; the caller's \
language-conditional policy would run against the wrong key. \
Recompute via `AlignmentSet::detect_oov` for THIS chunk.",
resolved.event().language(),
job_language,
),
job_language.clone(),
),
)));
}
}
}
return Ok(());
}
for (run_idx, run) in runs.iter().enumerate() {
let Some(run_decisions) = oov_decisions.get(run_idx) else {
continue;
};
let expected_lang = run.language();
for (i, resolved) in run_decisions.iter().enumerate() {
if resolved.event().language() != expected_lang {
return Err(WorkFailure::Alignment(AlignmentError::Tokenization(
AlignmentFailure::new(
format_smolstr!(
"AlignWorkItem::oov_decisions[{run_idx}][{i}].event.language = {} \
but runs[{run_idx}].language() = {}. This typically means stale \
decisions from a previous chunk leaked into a per-run dispatch; \
the caller's language-conditional policy would run against the \
wrong key. Recompute via `AlignmentSet::detect_oov_per_run` for \
THIS chunk's runs.",
resolved.event().language(),
expected_lang,
),
expected_lang.clone(),
),
)));
}
}
}
Ok(())
}
fn alignment_failure_is_recoverable(failure: &WorkFailure) -> bool {
matches!(
failure,
WorkFailure::Alignment(
AlignmentError::NoAlignmentPath(_)
| AlignmentError::EmptyText(_)
| AlignmentError::SemanticOutOfVocab(_)
)
)
}
fn run_under_lock(
aligner: &Mutex<Aligner>,
job: &AlignWorkItem,
run_options: &RunOptions,
abort_flag: &AtomicBool,
) -> Result<AlignmentResult, WorkFailure> {
let mut guard = match aligner.lock() {
Ok(g) => g,
Err(poisoned) => {
poisoned.into_inner()
}
};
let bound = job.samples_to_output_range.clone();
let chunk_decisions = job
.oov_decisions
.first()
.map(|v| v.as_slice())
.unwrap_or(&[]);
guard.align(
&job.samples,
&job.sub_segments,
job.text.as_str(),
job.chunk_first_sample_in_stream,
move |a, b| (bound)(a, b),
abort_flag,
run_options,
chunk_decisions,
&job.language,
)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) struct BoundsSourceCounters {
runs_total: usize,
runs_dtw: usize,
runs_segment: usize,
runs_wholeclip: usize,
runs_unaligned: usize,
}
impl BoundsSourceCounters {
pub(super) fn observe_bounds(&mut self, source: crate::align::BoundsSource) {
self.runs_total += 1;
match source {
crate::align::BoundsSource::Dtw => self.runs_dtw += 1,
crate::align::BoundsSource::Segment => self.runs_segment += 1,
crate::align::BoundsSource::Wholeclip => self.runs_wholeclip += 1,
}
}
pub(super) const fn observe_unaligned(&mut self) {
self.runs_unaligned += 1;
}
pub(super) const fn runs_total(&self) -> usize {
self.runs_total
}
pub(super) const fn runs_dtw(&self) -> usize {
self.runs_dtw
}
pub(super) const fn runs_segment(&self) -> usize {
self.runs_segment
}
pub(super) const fn runs_wholeclip(&self) -> usize {
self.runs_wholeclip
}
pub(super) const fn runs_unaligned(&self) -> usize {
self.runs_unaligned
}
}
fn check_abort_between_runs(
abort_flag: &AtomicBool,
dispatch_started_at: Instant,
) -> Result<(), WorkFailure> {
if abort_flag.load(Ordering::Relaxed) {
return Err(WorkFailure::WorkerHang(WorkerHangTimeout::new(
WorkerKind::Alignment,
dispatch_started_at.elapsed(),
)));
}
Ok(())
}
fn dispatch_runs(
set: &AlignmentSet,
job: &AlignWorkItem,
run_options: &RunOptions,
) -> Result<AlignmentResult, WorkFailure> {
let mut counters = BoundsSourceCounters::default();
let mut all_words: Vec<Word> = Vec::new();
let dispatch_started_at = Instant::now();
for (run_idx, run) in job.runs.iter().enumerate() {
if let Err(failure) = check_abort_between_runs(&job.abort_flag, dispatch_started_at) {
emit_telemetry(job.chunk_id, &counters);
return Err(failure);
}
counters.observe_bounds(run.bounds_source());
let (slice_lo, slice_hi) =
run_audio_slice(run, job.samples.len(), job.chunk_first_sample_in_stream);
let lookup = set.lookup(run.language());
let aligner_lock = match lookup {
AlignmentLookup::Hit { aligner, .. } => Some(aligner),
AlignmentLookup::AnyFallback { aligner } => Some(aligner),
AlignmentLookup::Miss { fallback } => match fallback {
AlignmentFallback::SkipChunk => {
counters.observe_unaligned();
None
}
AlignmentFallback::Error => {
emit_telemetry(job.chunk_id, &counters);
return Err(WorkFailure::LanguageUnsupported(
LanguageUnsupportedForAlignment::new(run.language().clone()),
));
}
},
};
let Some(aligner) = aligner_lock else {
continue;
};
let run_subs = clip_sub_segments(&job.sub_segments, slice_lo, slice_hi, run.language())
.inspect_err(|_| {
emit_telemetry(job.chunk_id, &counters);
})?;
let run_samples = &job.samples[slice_lo..slice_hi];
let run_first_sample_in_stream = job
.chunk_first_sample_in_stream
.saturating_add(slice_lo as u64);
let run_oov_decisions = job
.oov_decisions
.get(run_idx)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let outcome = run_one_per_run(
aligner,
run,
run_samples,
&run_subs,
run_first_sample_in_stream,
job.samples_to_output_range.clone(),
&job.abort_flag,
run_options,
run_oov_decisions,
);
match outcome {
Ok(result) => {
let run_lang = run.language().clone();
for word in result.into_words() {
all_words.push(word.with_language(Some(run_lang.clone())));
}
}
Err(failure) => {
if alignment_failure_is_recoverable(&failure) {
if let WorkFailure::Alignment(err) = &failure {
let language = match err {
AlignmentError::ModelInference(p)
| AlignmentError::Tokenization(p)
| AlignmentError::Normalization(p)
| AlignmentError::NoAlignmentPath(p)
| AlignmentError::EmptyText(p)
| AlignmentError::SemanticOutOfVocab(p)
| AlignmentError::Aborted(p) => p.language(),
};
let run_chars = run.text().chars().count();
eprintln!(
"asry alignment recovered chunk={:?} run_language={:?} run_bounds={:?} \
run_chars={run_chars} kind={err:?} dropped_failure_language={language:?}",
job.chunk_id,
run.language(),
run.bounds_source(),
);
}
counters.observe_unaligned();
continue;
}
emit_telemetry(job.chunk_id, &counters);
return Err(failure);
}
}
}
sort_words_by_pts(&mut all_words);
emit_telemetry(job.chunk_id, &counters);
Ok(AlignmentResult::new(all_words))
}
fn sort_words_by_pts(words: &mut [Word]) {
words.sort_by_key(|w| {
let r = w.range();
(r.start_pts(), r.end_pts())
});
}
fn run_audio_slice(
run: &Run,
samples_len: usize,
_chunk_first_sample_in_stream: u64,
) -> (usize, usize) {
use crate::align::BoundsSource;
if matches!(run.bounds_source(), BoundsSource::Wholeclip) {
return (0, samples_len);
}
let t0 = run.audio_t0_ms();
let t1 = run.audio_t1_ms();
if t0 < 0 || t1 <= t0 {
return (0, 0);
}
let lo_u64 = (t0 as u64).saturating_mul(16);
let hi_u64 = (t1 as u64).saturating_mul(16);
if lo_u64 >= samples_len as u64 {
eprintln!(
"asry alignment Run bounds appear out-of-chunk: \
audio_t0_ms={t0} audio_t1_ms={t1} chunk_samples_len={samples_len}; \
check your AsrSource — Run::audio_t*_ms must be chunk-local ms, not stream-absolute"
);
return (samples_len, samples_len);
}
let lo = lo_u64.min(samples_len as u64) as usize;
let hi = hi_u64.min(samples_len as u64) as usize;
if hi <= lo {
return (lo, lo);
}
(lo, hi)
}
fn clip_sub_segments(
subs: &[TimeRange],
slice_lo: usize,
slice_hi: usize,
language: &Lang,
) -> Result<Vec<TimeRange>, WorkFailure> {
use core::num::NonZeroU32;
let tb = mediatime::Timebase::new(1, NonZeroU32::new(16_000).unwrap());
let mut out = Vec::with_capacity(subs.len());
let lo_i = slice_lo as i64;
let hi_i = slice_hi as i64;
for sub in subs {
let actual_tb = sub.timebase();
if actual_tb.num() != 1 || actual_tb.den().get() != 16_000 {
return Err(WorkFailure::Alignment(AlignmentError::ModelInference(
AlignmentFailure::new(
format_smolstr!(
"sub_segments must be in 1/16000 (chunk-local sample-index) timebase; got \
{}/{}. Convert via `Transcriber::chunk_first_sample` + a 1/16000 timebase \
before passing to the aligner.",
actual_tb.num(),
actual_tb.den().get(),
),
language.clone(),
),
)));
}
let s = sub.start_pts().max(lo_i);
let e = sub.end_pts().min(hi_i);
if e > s {
out.push(TimeRange::new(s - lo_i, e - lo_i, tb));
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn run_one_per_run(
aligner: &Mutex<Aligner>,
run: &Run,
run_samples: &[f32],
run_sub_segments: &[TimeRange],
run_first_sample_in_stream: u64,
samples_to_output_range: Arc<dyn Fn(u64, u64) -> TimeRange + Send + Sync>,
abort_flag: &AtomicBool,
run_options: &RunOptions,
oov_decisions: &[ResolvedOov],
) -> Result<AlignmentResult, WorkFailure> {
let mut guard = match aligner.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
let bound = samples_to_output_range.clone();
guard.align(
run_samples,
run_sub_segments,
run.text(),
run_first_sample_in_stream,
move |a, b| (bound)(a, b),
abort_flag,
run_options,
oov_decisions,
run.language(),
)
}
fn emit_telemetry(chunk_id: ChunkId, c: &BoundsSourceCounters) {
std::eprintln!(
"script_dispatch chunk={} runs={} dtw={} segment={} wholeclip={} unaligned={}",
chunk_id.as_u64(),
c.runs_total(),
c.runs_dtw(),
c.runs_segment(),
c.runs_wholeclip(),
c.runs_unaligned(),
);
}
#[cfg(test)]
mod tests;