use std::ops::Range;
use wavo::{Token, Transcript};
const FRAME: usize = 160;
const WINDOW: usize = 30;
const MIN_TAIL: usize = 16000;
pub fn segments(pcm: &[f32], max: usize) -> Vec<Range<usize>> {
let fits = pcm.len() <= max;
let frames = pcm.chunks(FRAME).take(if fits { 0 } else { usize::MAX });
let energy: Vec<f32> = frames.map(|f| f.iter().map(|x| x * x).sum()).collect();
let (mut out, mut start) = (Vec::new(), 0);
while pcm.len() - start > max {
let first = (start + max / 2) / FRAME;
let end = (start + max).min(pcm.len() - MIN_TAIL) / FRAME;
let sums: Vec<f32> =
(first..=end - WINDOW).map(|f| energy[f..f + WINDOW].iter().sum()).collect();
let least = sums.iter().copied().fold(f32::INFINITY, f32::min);
let window = first + sums.iter().rposition(|&s| s <= 2.0 * least).unwrap();
let frames = (window..window + WINDOW).rev();
let cut = frames.min_by(|&a, &b| energy[a].total_cmp(&energy[b])).unwrap() * FRAME;
out.push(start..cut);
start = cut;
}
out.push(start..pcm.len());
out
}
pub fn join(parts: impl IntoIterator<Item = (usize, Transcript)>) -> (Transcript, Vec<usize>) {
let (mut out, mut starts) = (Transcript::default(), Vec::new());
for (start, part) in parts {
starts.push(out.tokens.len());
if !part.text.is_empty() {
out.text += if out.text.is_empty() { "" } else { " " };
out.text += &part.text;
}
let offset = (start / 16) as u32; let shift = |mut t: Token| {
t.start_ms += offset;
t
};
out.tokens.extend(part.tokens.into_iter().map(shift));
}
(out, starts)
}
#[cfg(test)]
mod tests {
use std::f32::consts::TAU;
use super::*;
const SEC: usize = 16000;
fn audio(ms: usize, pauses: &[(usize, usize)], floor: f32) -> Vec<f32> {
let mut x = 1u32;
let mut noise = move || {
x = x.wrapping_mul(1664525).wrapping_add(1013904223);
floor * ((x >> 8) as f32 / (1 << 23) as f32 - 1.0)
};
(0..ms * 16)
.map(|i| {
let t = i / 16;
if pauses.iter().any(|&(s, n)| (s..s + n).contains(&t)) {
noise()
} else {
let level = 0.3 + 0.2 * (TAU * 4.0 * i as f32 / 16000.0).sin();
level * (TAU * 180.0 * i as f32 / 16000.0).sin() + noise()
}
})
.collect()
}
fn check_ranges(ranges: &[Range<usize>], len: usize, max: usize) {
assert_eq!(ranges.first().unwrap().start, 0);
assert_eq!(ranges.last().unwrap().end, len);
for pair in ranges.windows(2) {
assert_eq!(pair[0].end, pair[1].start, "contiguous");
assert_eq!(pair[0].end % FRAME, 0, "cut on a frame start");
assert!(pair[0].len() >= max / 2 - FRAME, "{:?}", pair[0]);
}
assert!(ranges.iter().all(|r| r.len() <= max), "{ranges:?}");
assert!(ranges.last().unwrap().len() >= MIN_TAIL, "{ranges:?}");
}
#[test]
fn short_audio_is_one_range() {
let one = |pcm: &[f32], max| match &segments(pcm, max)[..] {
[range] => range.clone(),
ranges => panic!("{ranges:?}"),
};
assert_eq!(one(&audio(25_000, &[(10_000, 500)], 0.0), 25 * SEC), 0..25 * SEC);
assert_eq!(one(&audio(60_000, &[], 0.0), usize::MAX), 0..60 * SEC);
assert_eq!(one(&[], 25 * SEC), 0..0);
}
#[test]
fn cuts_in_the_pause_nearest_the_limit() {
let pauses = [(8_000, 600), (14_000, 400), (21_000, 500), (33_000, 300), (45_000, 400)];
for floor in [0.0, 0.01] {
let pcm = audio(47_400, &pauses, floor);
let ranges = segments(&pcm, 25 * SEC);
check_ranges(&ranges, pcm.len(), 25 * SEC);
let cuts: Vec<usize> = ranges[1..].iter().map(|r| r.start / 16).collect();
assert_eq!(cuts.len(), 2, "{cuts:?}");
assert!((21_000..21_500).contains(&cuts[0]), "{cuts:?}");
assert!((45_000..45_400).contains(&cuts[1]), "{cuts:?}");
let quiet = segments(&pcm.iter().map(|x| x * 0.01).collect::<Vec<_>>(), 25 * SEC);
assert_eq!(quiet, ranges, "the level doesn't matter");
}
}
#[test]
fn without_pauses_cuts_stay_in_range() {
let pcm = audio(130_000, &[], 0.001);
let ranges = segments(&pcm, 25 * SEC);
check_ranges(&ranges, pcm.len(), 25 * SEC);
assert!(ranges.len() >= 6, "{ranges:?}");
let pcm = audio(60_000, &[], 0.0); let ranges = segments(&pcm, 5 * SEC);
check_ranges(&ranges, pcm.len(), 5 * SEC);
}
#[test]
fn join_shifts_tokens_and_skips_empty_texts() {
let token = |id, start_ms| {
let mut t = Token::default();
(t.id, t.piece, t.start_ms) = (id, format!("▁{id}"), start_ms);
t
};
let part = |text: &str, tokens| {
let mut t = Transcript::default();
(t.text, t.tokens) = (text.into(), tokens);
t
};
let (t, starts) = join([
(0, part("a b", vec![token(1, 40), token(2, 80)])),
(16 * 20_000, part("", vec![])),
(16 * 30_010, part("c", vec![token(3, 0)])),
]);
assert_eq!(t.text, "a b c");
let times: Vec<(u32, u32)> = t.tokens.iter().map(|t| (t.id, t.start_ms)).collect();
assert_eq!(times, [(1, 40), (2, 80), (3, 30_010)]);
assert_eq!(starts, [0, 2, 2]);
let one = part("x", vec![token(7, 120)]);
assert_eq!(join([(0, one.clone())]).0, one, "a single segment from 0 is unchanged");
}
}