use core::num::NonZeroUsize;
use mediatime::{TimeRange, Timebase};
use smol_str::format_smolstr;
use crate::{
runner::aligner::{
algorithm::{
compose::DEFAULT_MIN_SPEECH_COVERAGE,
encode::{LogProbsTV, log_softmax_with_finite_guard},
errors::{EmissionsError, EmissionsFailure},
trellis_beam::SEAM_PATH_FRAME_BUDGET,
},
core::coerce_speech_coverage,
},
time::{ANALYSIS_TIMEBASE, SAMPLE_RATE_HZ},
};
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
pub struct SpeechCoverage(f32);
impl SpeechCoverage {
pub const DEFAULT: Self = Self(DEFAULT_MIN_SPEECH_COVERAGE);
#[must_use]
pub fn new(value: f32) -> Option<Self> {
if value.is_finite() && (0.0..=1.0).contains(&value) {
Some(Self(value))
} else {
None
}
}
#[must_use]
pub const fn clamped(value: f32) -> Self {
Self(coerce_speech_coverage(value))
}
#[must_use]
pub const fn get(self) -> f32 {
self.0
}
}
impl Default for SpeechCoverage {
fn default() -> Self {
Self::DEFAULT
}
}
impl Eq for SpeechCoverage {}
#[allow(
clippy::derive_ord_xor_partial_ord,
reason = "PartialOrd is derived and Ord delegates to it; they cannot \
disagree. A manual Ord is required because f32 has no Ord, and it is \
SOUND here only because the constructor excludes NaN — which is the \
invariant this whole type exists to establish."
)]
impl Ord for SpeechCoverage {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self
.0
.partial_cmp(&other.0)
.expect("SpeechCoverage excludes NaN by construction")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SpanError {
#[error(
"expected spans in the chunk-local 1/{expected} timebase, got {num}/{den}; \
samples will not match audio if we proceed"
)]
Timebase {
expected: u32,
num: u32,
den: u32,
},
#[error("span start {start} is after its end {end}")]
StartAfterEnd {
start: u64,
end: u64,
},
#[error("sample index {value} exceeds the representable maximum {max}")]
OutOfRange {
value: u64,
max: u64,
},
#[error("timebase numerator must be non-zero (got 0/{den})")]
ZeroNumeratorTimebase {
den: u32,
},
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct SampleSpan {
start: u64,
end: u64,
}
impl SampleSpan {
pub const MAX_SAMPLE: u64 = i64::MAX as u64;
pub const fn new(start: u64, end: u64) -> Result<Self, SpanError> {
if start > Self::MAX_SAMPLE {
return Err(SpanError::OutOfRange {
value: start,
max: Self::MAX_SAMPLE,
});
}
if end > Self::MAX_SAMPLE {
return Err(SpanError::OutOfRange {
value: end,
max: Self::MAX_SAMPLE,
});
}
if start > end {
return Err(SpanError::StartAfterEnd { start, end });
}
Ok(Self { start, end })
}
pub fn from_time_range(range: TimeRange) -> Result<Self, SpanError> {
let tb = range.timebase();
if tb.num() != 1 || tb.den().get() != SAMPLE_RATE_HZ {
return Err(SpanError::Timebase {
expected: SAMPLE_RATE_HZ,
num: tb.num(),
den: tb.den().get(),
});
}
Self::from_analysis_pts(range.start_pts(), range.end_pts())
}
pub fn from_time_range_rescaled(range: TimeRange) -> Result<Self, SpanError> {
let tb = range.timebase();
if tb.num() == 0 {
return Err(SpanError::ZeroNumeratorTimebase {
den: tb.den().get(),
});
}
let start = Timebase::rescale_pts(range.start_pts(), tb, ANALYSIS_TIMEBASE);
let end = Timebase::rescale_pts(range.end_pts(), tb, ANALYSIS_TIMEBASE);
Self::from_analysis_pts(start, end)
}
fn from_analysis_pts(start_pts: i64, end_pts: i64) -> Result<Self, SpanError> {
let start = start_pts.max(0) as u64;
let end = end_pts.max(0) as u64;
Self::new(start, end)
}
#[must_use]
pub const fn start(self) -> u64 {
self.start
}
#[must_use]
pub const fn end(self) -> u64 {
self.end
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.start == self.end
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct SpeechSpans {
spans: Vec<SampleSpan>,
}
impl SpeechSpans {
#[must_use]
pub fn all_speech() -> Self {
Self {
spans: vec![SampleSpan {
start: 0,
end: SampleSpan::MAX_SAMPLE,
}],
}
}
#[must_use]
pub fn new(spans: impl IntoIterator<Item = SampleSpan>) -> Self {
let mut spans: Vec<SampleSpan> = spans.into_iter().filter(|s| !s.is_empty()).collect();
spans.sort_unstable();
let mut coalesced: Vec<SampleSpan> = Vec::with_capacity(spans.len());
for span in spans {
match coalesced.last_mut() {
Some(last) if span.start <= last.end => {
if span.end > last.end {
last.end = span.end;
}
}
_ => coalesced.push(span),
}
}
Self { spans: coalesced }
}
pub fn from_time_ranges(ranges: &[TimeRange]) -> Result<Self, SpanError> {
let spans = ranges
.iter()
.copied()
.map(SampleSpan::from_time_range)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self::new(spans))
}
pub fn from_time_ranges_rescaled(ranges: &[TimeRange]) -> Result<Self, SpanError> {
let spans = ranges
.iter()
.copied()
.map(SampleSpan::from_time_range_rescaled)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self::new(spans))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.spans.is_empty()
}
pub(crate) fn as_slice(&self) -> &[SampleSpan] {
&self.spans
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OutputClock {
chunk_first_sample_in_stream: u64,
timebase: Timebase,
base_pts: i64,
}
impl OutputClock {
pub const fn new(
chunk_first_sample_in_stream: u64,
timebase: Timebase,
base_pts: i64,
) -> Result<Self, SpanError> {
if timebase.num() == 0 {
return Err(SpanError::ZeroNumeratorTimebase {
den: timebase.den().get(),
});
}
Ok(Self {
chunk_first_sample_in_stream,
timebase,
base_pts,
})
}
#[must_use]
pub const fn chunk_first_sample_in_stream(&self) -> u64 {
self.chunk_first_sample_in_stream
}
pub(crate) fn range(&self, start_sample: u64, end_sample: u64) -> TimeRange {
let to_pts = |sample: u64| -> i64 {
let clamped = i64::try_from(sample).unwrap_or(i64::MAX);
self.base_pts.saturating_add(Timebase::rescale_pts(
clamped,
ANALYSIS_TIMEBASE,
self.timebase,
))
};
let start = to_pts(start_sample);
let end = to_pts(end_sample);
TimeRange::new(start, end.max(start), self.timebase)
}
}
pub struct Emissions {
inner: LogProbsTV,
vocab: NonZeroUsize,
}
impl Emissions {
pub const FRAME_BUDGET: usize = SEAM_PATH_FRAME_BUDGET;
pub fn from_log_probs(t: usize, v: NonZeroUsize, data: Vec<f32>) -> Result<Self, EmissionsError> {
Self::check_budget(t)?;
let inner = LogProbsTV::new(t, v.get(), data)?;
Ok(Self { inner, vocab: v })
}
pub fn from_logits(t: usize, v: NonZeroUsize, raw: Vec<f32>) -> Result<Self, EmissionsError> {
Self::from_logits_slice(t, v, &raw)
}
pub fn from_logits_slice(t: usize, v: NonZeroUsize, raw: &[f32]) -> Result<Self, EmissionsError> {
Self::check_budget(t)?;
let data = log_softmax_with_finite_guard(raw, t, v.get())?;
let inner = LogProbsTV::from_parts_unchecked(t, v.get(), data);
Ok(Self { inner, vocab: v })
}
#[must_use]
pub const fn frames(&self) -> usize {
self.inner.t()
}
#[must_use]
pub const fn vocab(&self) -> NonZeroUsize {
self.vocab
}
pub(crate) const fn inner(&self) -> &LogProbsTV {
&self.inner
}
fn check_budget(t: usize) -> Result<(), EmissionsError> {
if t > Self::FRAME_BUDGET {
return Err(EmissionsError::PathBudget(EmissionsFailure::new(
format_smolstr!(
"emissions frame count T={t} exceeds the budget of {} frames; the CTC path holds \
one point per frame, so aligning at this T would reserve hundreds of megabytes up \
front. Supply emissions with a realistic frame count (frames ≈ audio_samples / \
encoder_hop).",
Self::FRAME_BUDGET
),
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests;