use crate::error::{Error, Result};
use crate::vad::options::{SpeechSegment, VadSegmentOptions};
pub fn segments_from_probabilities(
probs: &[f32],
num_samples: usize,
sample_rate: usize,
window_size: usize,
opts: &VadSegmentOptions,
) -> Result<Vec<SpeechSegment>> {
if sample_rate == 0 {
return Err(Error::InvalidArgument {
arg: "sample_rate",
reason: "must be non-zero".to_string(),
});
}
if window_size == 0 {
return Err(Error::InvalidArgument {
arg: "window_size",
reason: "must be non-zero".to_string(),
});
}
opts.validate()?;
let expected = num_samples.div_ceil(window_size);
if probs.len() != expected {
return Err(Error::InvalidArgument {
arg: "probs",
reason: format!(
"{} probabilities for {num_samples} samples at a window of \
{window_size}: expected {expected} \
(ceil({num_samples} / {window_size}))",
probs.len()
),
});
}
let rate = sample_rate as f64;
let threshold = opts.threshold;
let neg_threshold = opts
.neg_threshold
.unwrap_or_else(|| (threshold - 0.15).max(0.01));
let min_speech_samples = rate * opts.min_speech_duration_ms as f64 / 1000.0;
let speech_pad_samples = (rate * opts.speech_pad_ms as f64 / 1000.0) as usize;
let max_speech_samples = rate * opts.max_speech_duration_s as f64
- window_size as f64
- 2.0 * speech_pad_samples as f64;
let min_silence_samples = (rate * opts.min_silence_duration_ms as f64 / 1000.0) as usize;
let min_silence_samples_at_max_speech =
(rate * opts.min_silence_at_max_speech_ms as f64 / 1000.0) as usize;
let mut triggered = false;
let mut speeches: Vec<SpeechSegment> = Vec::new();
let mut current: Option<SpeechSegment> = None;
let mut temp_end: usize = 0;
let mut prev_end: usize = 0;
let mut next_start: usize = 0;
let mut possible_ends: Vec<(usize, usize)> = Vec::new();
for (i, &speech_prob) in probs.iter().enumerate() {
let cur_sample = window_size * i;
if speech_prob >= threshold && temp_end != 0 {
let sil_dur = cur_sample - temp_end;
if sil_dur > min_silence_samples_at_max_speech {
possible_ends.push((temp_end, sil_dur));
}
temp_end = 0;
if next_start < prev_end {
next_start = cur_sample;
}
}
if speech_prob >= threshold && !triggered {
triggered = true;
current = Some(SpeechSegment {
start: cur_sample,
end: 0,
});
continue;
}
if triggered
&& let Some(seg) = current
&& (cur_sample as f64 - seg.start as f64) > max_speech_samples
{
let longest_silence = possible_ends.iter().copied().reduce(|best, candidate| {
if candidate.1 > best.1 {
candidate
} else {
best
}
});
if opts.use_max_possible_silence_at_max_speech
&& let Some((split_at, dur)) = longest_silence
{
speeches.push(SpeechSegment {
start: seg.start,
end: split_at,
});
next_start = split_at + dur;
if next_start < split_at + cur_sample {
current = Some(SpeechSegment {
start: next_start,
end: 0,
});
} else {
current = None;
triggered = false;
}
prev_end = 0;
next_start = 0;
temp_end = 0;
possible_ends.clear();
} else if prev_end != 0 {
speeches.push(SpeechSegment {
start: seg.start,
end: prev_end,
});
if next_start < prev_end {
current = None;
triggered = false;
} else {
current = Some(SpeechSegment {
start: next_start,
end: 0,
});
}
prev_end = 0;
next_start = 0;
temp_end = 0;
possible_ends.clear();
} else {
speeches.push(SpeechSegment {
start: seg.start,
end: cur_sample,
});
prev_end = 0;
next_start = 0;
temp_end = 0;
triggered = false;
current = None;
possible_ends.clear();
continue;
}
}
if speech_prob < neg_threshold
&& triggered
&& let Some(seg) = current
{
if temp_end == 0 {
temp_end = cur_sample;
}
let sil_dur_now = cur_sample - temp_end;
if !opts.use_max_possible_silence_at_max_speech
&& sil_dur_now > min_silence_samples_at_max_speech
{
prev_end = temp_end;
}
if sil_dur_now < min_silence_samples {
continue;
}
if (temp_end as f64 - seg.start as f64) > min_speech_samples {
speeches.push(SpeechSegment {
start: seg.start,
end: temp_end,
});
}
current = None;
prev_end = 0;
next_start = 0;
temp_end = 0;
triggered = false;
possible_ends.clear();
continue;
}
}
if let Some(seg) = current
&& (num_samples as f64 - seg.start as f64) > min_speech_samples
{
speeches.push(SpeechSegment {
start: seg.start,
end: num_samples,
});
}
pad_segments(&mut speeches, num_samples, speech_pad_samples);
Ok(speeches)
}
fn pad_segments(speeches: &mut [SpeechSegment], num_samples: usize, pad: usize) {
let n = speeches.len();
for i in 0..n {
if i == 0 {
speeches[i].start = speeches[i].start.saturating_sub(pad);
}
if i + 1 != n {
let silence = speeches[i + 1].start as i64 - speeches[i].end as i64;
if silence < 2 * pad as i64 {
let half = silence.div_euclid(2);
speeches[i].end = (speeches[i].end as i64 + half).max(0) as usize;
speeches[i + 1].start = (speeches[i + 1].start as i64 - half).max(0) as usize;
} else {
speeches[i].end = (speeches[i].end + pad).min(num_samples);
speeches[i + 1].start = speeches[i + 1].start.saturating_sub(pad);
}
} else {
speeches[i].end = (speeches[i].end + pad).min(num_samples);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const RATE: usize = 16000;
const WINDOW: usize = 512;
fn probs(len: usize, speech: &[(usize, usize)]) -> Vec<f32> {
let mut out = vec![0.0f32; len];
for &(from, to) in speech {
for p in &mut out[from..to] {
*p = 0.9;
}
}
out
}
fn segment(probs: &[f32], opts: &VadSegmentOptions) -> Vec<SpeechSegment> {
segments_from_probabilities(probs, probs.len() * WINDOW, RATE, WINDOW, opts)
.expect("valid segmentation inputs")
}
fn pairs(segments: &[SpeechSegment]) -> Vec<(usize, usize)> {
segments.iter().map(|s| (s.start, s.end)).collect()
}
#[test]
fn one_speech_run_becomes_one_padded_segment() {
let p = probs(60, &[(10, 30)]);
let got = segment(&p, &VadSegmentOptions::default());
assert_eq!(pairs(&got), vec![(4640, 15840)]);
}
#[test]
fn a_silence_longer_than_min_silence_splits_the_run() {
let p = probs(70, &[(10, 30), (40, 60)]);
let got = segment(&p, &VadSegmentOptions::default());
assert_eq!(pairs(&got), vec![(4640, 15840), (20000, 31200)]);
}
#[test]
fn a_silence_shorter_than_min_silence_does_not_split() {
let p = probs(60, &[(10, 30), (32, 50)]);
let got = segment(&p, &VadSegmentOptions::default());
assert_eq!(pairs(&got), vec![(4640, 26080)]);
}
#[test]
fn a_run_shorter_than_min_speech_is_dropped() {
let p = probs(40, &[(10, 15)]);
let got = segment(&p, &VadSegmentOptions::default());
assert!(got.is_empty(), "expected no segments, got {got:?}");
}
#[test]
fn speech_running_to_the_end_of_the_signal_is_closed_at_the_last_sample() {
let p = probs(30, &[(10, 30)]);
let got = segment(&p, &VadSegmentOptions::default());
assert_eq!(pairs(&got), vec![(4640, 15360)]);
}
#[test]
fn all_silence_yields_no_segments() {
let p = probs(50, &[]);
let got = segment(&p, &VadSegmentOptions::default());
assert!(got.is_empty(), "expected no segments, got {got:?}");
}
#[test]
fn a_gap_under_two_pads_is_split_between_the_neighbours() {
let opts = VadSegmentOptions {
speech_pad_ms: 100,
..VadSegmentOptions::default()
};
let p = probs(65, &[(10, 30), (35, 55)]);
let got = segment(&p, &opts);
assert_eq!(pairs(&got), vec![(3520, 16640), (16640, 29760)]);
assert_eq!(got[0].end, got[1].start, "the padded segments must meet");
}
#[test]
fn a_probability_count_that_does_not_match_the_signal_is_rejected() {
let opts = VadSegmentOptions::default();
let err = segments_from_probabilities(&[0.9], 1000, RATE, WINDOW, &opts)
.expect_err("one probability cannot cover 1000 samples");
let message = err.to_string();
assert!(message.contains("1000"), "{message}");
assert!(message.contains("512"), "{message}");
}
#[test]
fn degenerate_geometry_is_rejected() {
let opts = VadSegmentOptions::default();
assert!(segments_from_probabilities(&[], 0, 0, WINDOW, &opts).is_err());
assert!(segments_from_probabilities(&[], 0, RATE, 0, &opts).is_err());
let bad = VadSegmentOptions {
threshold: f32::NAN,
..VadSegmentOptions::default()
};
assert!(segments_from_probabilities(&[], 0, RATE, WINDOW, &bad).is_err());
}
#[test]
fn the_max_speech_cut_keeps_the_first_of_two_equal_silences() {
let mut probs = vec![0.9f32; 30];
probs[5..10].fill(0.1);
probs[15..20].fill(0.1);
let num_samples = 30 * WINDOW;
let opts = VadSegmentOptions {
max_speech_duration_s: 0.8,
min_silence_duration_ms: 10_000,
min_speech_duration_ms: 0,
speech_pad_ms: 0,
..VadSegmentOptions::default()
};
let got =
segments_from_probabilities(&probs, num_samples, RATE, WINDOW, &opts).expect("segment");
assert_eq!(
pairs(&got),
vec![(0, 2560), (5120, 15360)],
"tie-break picked the later silence"
);
}
}