use crate::error::{Error, Result};
use crate::eval::normalize;
use crate::vad::{SpeechSegment, VadSegmentOptions, speech_timestamps};
use boostr::model::audio::vad::SileroVad;
use numr::dtype::DType;
use numr::ops::{ConvOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pace {
pub speech_s: f64,
pub leading_silence_s: f64,
pub trailing_silence_s: f64,
pub speech_ratio: f64,
pub words: usize,
pub words_per_speech_s: Option<f64>,
pub segments: usize,
}
pub fn pace_from_segments(
segments: &[SpeechSegment],
total_samples: usize,
sample_rate: u32,
prompt: &str,
) -> Pace {
let rate = f64::from(sample_rate.max(1));
let total_s = total_samples as f64 / rate;
let speech_s: f64 = segments.iter().map(|s| s.len() as f64 / rate).sum();
let (leading_silence_s, trailing_silence_s) = match (segments.first(), segments.last()) {
(Some(first), Some(last)) => (
first.start as f64 / rate,
total_samples.saturating_sub(last.end) as f64 / rate,
),
_ => (total_s, 0.0),
};
let words = normalize(prompt).len();
Pace {
speech_s,
leading_silence_s,
trailing_silence_s,
speech_ratio: if total_s > 0.0 {
speech_s / total_s
} else {
0.0
},
words,
words_per_speech_s: (speech_s > 0.0).then(|| words as f64 / speech_s),
segments: segments.len(),
}
}
pub fn pace<R, C>(
vad: &SileroVad<R>,
client: &C,
samples_16k: &[f32],
prompt: &str,
opts: &VadSegmentOptions,
) -> Result<Pace>
where
R: Runtime<DType = DType>,
C: RuntimeClient<R> + TensorOps<R> + ConvOps<R>,
{
let segments = speech_timestamps(vad, client, samples_16k, opts)?;
let rate = u32::try_from(vad.config().sample_rate).map_err(|_| Error::InvalidArgument {
arg: "vad",
reason: format!(
"VAD sample rate {} does not fit in u32",
vad.config().sample_rate
),
})?;
Ok(pace_from_segments(
&segments,
samples_16k.len(),
rate,
prompt,
))
}
#[cfg(test)]
mod tests {
use super::*;
const RATE: u32 = 16_000;
fn seg(start_s: f64, end_s: f64) -> SpeechSegment {
SpeechSegment {
start: (start_s * RATE as f64) as usize,
end: (end_s * RATE as f64) as usize,
}
}
#[test]
fn silences_and_ratio_come_from_the_outer_segments() {
let segments = [seg(0.5, 1.5), seg(2.0, 3.0)];
let p = pace_from_segments(&segments, 4 * RATE as usize, RATE, "one two three four");
assert!((p.speech_s - 2.0).abs() < 1e-6);
assert!((p.leading_silence_s - 0.5).abs() < 1e-6);
assert!((p.trailing_silence_s - 1.0).abs() < 1e-6);
assert!((p.speech_ratio - 0.5).abs() < 1e-6);
assert_eq!(p.words, 4);
assert!((p.words_per_speech_s.expect("speech") - 2.0).abs() < 1e-6);
assert_eq!(p.segments, 2);
}
#[test]
fn no_speech_is_all_leading_silence() {
let p = pace_from_segments(&[], 2 * RATE as usize, RATE, "hello");
assert_eq!(p.speech_s, 0.0);
assert!((p.leading_silence_s - 2.0).abs() < 1e-6);
assert_eq!(p.trailing_silence_s, 0.0);
assert_eq!(p.speech_ratio, 0.0);
assert_eq!(p.words_per_speech_s, None);
}
#[test]
fn words_are_counted_from_the_normalised_prompt() {
let p = pace_from_segments(&[seg(0.0, 1.0)], RATE as usize, RATE, "Ter-delete, okay?");
assert_eq!(p.words, 3);
}
}