use core::{fmt, ops::Range, time::Duration};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpeechSegment {
start_sample: u64,
end_sample: u64,
}
impl fmt::Display for SpeechSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SpeechSegment {{ start: {:.3}s, end: {:.3}s, duration: {:.3}s }}",
self.start().as_secs_f64(),
self.end().as_secs_f64(),
self.duration().as_secs_f64(),
)
}
}
impl SpeechSegment {
pub const SAMPLE_RATE_HZ: u32 = 16_000;
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(start_sample: u64, end_sample: u64) -> Self {
Self {
start_sample,
end_sample,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn start_sample(&self) -> u64 {
self.start_sample
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn end_sample(&self) -> u64 {
self.end_sample
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn sample_count(&self) -> u64 {
self.end_sample.saturating_sub(self.start_sample)
}
pub fn start(&self) -> Duration {
Duration::from_secs_f64(self.start_sample as f64 / Self::SAMPLE_RATE_HZ as f64)
}
pub fn end(&self) -> Duration {
Duration::from_secs_f64(self.end_sample as f64 / Self::SAMPLE_RATE_HZ as f64)
}
pub fn duration(&self) -> Duration {
Duration::from_secs_f64(self.sample_count() as f64 / Self::SAMPLE_RATE_HZ as f64)
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn range(&self) -> Range<u64> {
self.start_sample..self.end_sample
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn range_usize(&self) -> Range<usize> {
let to_usize = |v: u64| -> usize {
if v > usize::MAX as u64 {
usize::MAX
} else {
v as usize
}
};
to_usize(self.start_sample)..to_usize(self.end_sample)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FrameResult {
frame_index: u64,
raw_prob: f32,
smoothed_prob: f32,
is_speech: bool,
is_speech_start: bool,
is_speech_end: bool,
speech_start_frame: Option<u64>,
speech_end_frame: Option<u64>,
}
impl FrameResult {
pub const FRAME_SHIFT_SAMPLES: u32 = 160;
pub const SAMPLE_RATE_HZ: u32 = 16_000;
#[cfg_attr(not(tarpaulin), inline(always))]
#[allow(clippy::too_many_arguments)]
pub const fn new(
frame_index: u64,
raw_prob: f32,
smoothed_prob: f32,
is_speech: bool,
is_speech_start: bool,
is_speech_end: bool,
speech_start_frame: Option<u64>,
speech_end_frame: Option<u64>,
) -> Self {
Self {
frame_index,
raw_prob,
smoothed_prob,
is_speech,
is_speech_start,
is_speech_end,
speech_start_frame,
speech_end_frame,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn frame_index(&self) -> u64 {
self.frame_index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn raw_prob(&self) -> f32 {
self.raw_prob
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn smoothed_prob(&self) -> f32 {
self.smoothed_prob
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_speech(&self) -> bool {
self.is_speech
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_speech_start(&self) -> bool {
self.is_speech_start
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_speech_end(&self) -> bool {
self.is_speech_end
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn speech_start_frame(&self) -> Option<u64> {
self.speech_start_frame
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn speech_end_frame(&self) -> Option<u64> {
self.speech_end_frame
}
pub fn timestamp(&self) -> Duration {
let samples = self.frame_index * Self::FRAME_SHIFT_SAMPLES as u64;
Duration::from_secs_f64(samples as f64 / Self::SAMPLE_RATE_HZ as f64)
}
pub fn closed_segment(&self) -> Option<SpeechSegment> {
if !self.is_speech_end {
return None;
}
let start = self.speech_start_frame? * Self::FRAME_SHIFT_SAMPLES as u64;
let end = self.speech_end_frame? * Self::FRAME_SHIFT_SAMPLES as u64;
Some(SpeechSegment::new(start, end))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_count_is_end_minus_start() {
let s = SpeechSegment::new(160, 1600);
assert_eq!(s.sample_count(), 1440);
}
#[test]
fn timestamps_round_trip_through_sample_rate() {
let s = SpeechSegment::new(16_000, 32_000);
assert_eq!(s.start(), Duration::from_secs(1));
assert_eq!(s.end(), Duration::from_secs(2));
assert_eq!(s.duration(), Duration::from_secs(1));
}
#[test]
fn range_usize_slices_pcm_directly() {
let pcm = [0.0f32; 2_000];
let s = SpeechSegment::new(160, 320);
let slice = &pcm[s.range_usize()];
assert_eq!(slice.len(), 160);
}
#[test]
fn empty_segment_has_zero_sample_count() {
let s = SpeechSegment::new(100, 100);
assert_eq!(s.sample_count(), 0);
assert!(s.range().is_empty());
}
#[test]
fn frame_result_closed_segment_is_some_only_when_is_speech_end() {
let result = FrameResult::new(20, 0.9, 0.85, true, false, true, Some(2), Some(20));
let segment = result.closed_segment().expect("segment closes");
assert_eq!(segment.start_sample(), 2 * 160);
assert_eq!(segment.end_sample(), 20 * 160);
let mid = FrameResult::new(15, 0.8, 0.75, true, false, false, Some(2), None);
assert!(mid.closed_segment().is_none());
}
#[test]
fn frame_result_timestamp_uses_frame_shift_samples() {
let result = FrameResult::new(100, 0.0, 0.0, false, false, false, None, None);
assert_eq!(result.timestamp(), Duration::from_millis(1_000));
}
}