pub fn dynamic_time_warping(
similarity: &[f32],
rows: usize,
cols: usize,
) -> (Vec<usize>, Vec<usize>) {
if rows == 0 || cols == 0 {
return (Vec::new(), Vec::new());
}
debug_assert_eq!(similarity.len(), rows * cols);
let cost_cols = cols + 1;
let mut cost = vec![f64::INFINITY; (rows + 1) * cost_cols];
let mut trace = vec![2u8; (rows + 1) * cost_cols];
cost[0] = 0.0;
for j in 1..=cols {
for i in 1..=rows {
let c0 = cost[(i - 1) * cost_cols + (j - 1)];
let c1 = cost[(i - 1) * cost_cols + j];
let c2 = cost[i * cost_cols + (j - 1)];
let (c, t) = if c0 < c1 && c0 < c2 {
(c0, 0u8)
} else if c1 < c0 && c1 < c2 {
(c1, 1u8)
} else {
(c2, 2u8)
};
let similarity_val = similarity[(i - 1) * cols + (j - 1)] as f64;
cost[i * cost_cols + j] = -similarity_val + c;
trace[i * cost_cols + j] = t;
}
}
trace[..cost_cols].fill(2);
trace.iter_mut().step_by(cost_cols).for_each(|t| *t = 1);
let mut text_indices = Vec::new();
let mut time_indices = Vec::new();
let mut i = rows;
let mut j = cols;
while i > 0 || j > 0 {
text_indices.push(i - 1);
time_indices.push(j - 1);
match trace[i * cost_cols + j] {
0 => {
i -= 1;
j -= 1;
}
1 => i -= 1,
_ => j -= 1,
}
}
text_indices.reverse();
time_indices.reverse();
(text_indices, time_indices)
}
pub fn token_timestamps(
alignment: &[f32],
rows: usize,
cols: usize,
time_precision: f32,
num_input_ids: usize,
) -> Vec<f32> {
if rows == 0 {
return Vec::new();
}
let (text_indices, time_indices) = dynamic_time_warping(alignment, rows, cols);
let mut jump_times: Vec<f32> = Vec::with_capacity(rows);
let mut prev_text: Option<usize> = None;
for (step, &text_idx) in text_indices.iter().enumerate() {
let is_jump = prev_text != Some(text_idx);
if is_jump {
jump_times.push(time_indices[step] as f32 * time_precision);
}
prev_text = Some(text_idx);
}
let mut out = Vec::with_capacity(num_input_ids + rows + 1);
out.resize(num_input_ids, 0.0);
out.extend_from_slice(&jump_times);
if let Some(&last) = jump_times.last() {
out.push(last);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[rustfmt::skip]
const SIM_3X5: [f32; 15] = [
0.10, 0.20, 0.90, 0.10, 0.05,
0.05, 0.10, 0.30, 0.80, 0.10,
0.05, 0.05, 0.10, 0.20, 0.90,
];
#[test]
fn matches_hf_reference_path() {
let (text_indices, time_indices) = dynamic_time_warping(&SIM_3X5, 3, 5);
assert_eq!(text_indices, vec![0, 0, 0, 1, 1, 2, 2]);
assert_eq!(time_indices, vec![0, 1, 2, 2, 3, 3, 4]);
}
#[test]
fn jump_times_match_hf_reference() {
let ts = token_timestamps(&SIM_3X5, 3, 5, 0.02, 0);
let expected = [0.0f32, 0.04, 0.06, 0.06];
assert_eq!(ts.len(), expected.len());
for (got, want) in ts.iter().zip(expected.iter()) {
assert!((got - want).abs() < 1e-6, "got {got}, want {want}");
}
}
#[test]
fn token_timestamps_prepends_zeros_for_prefix() {
let ts = token_timestamps(&SIM_3X5, 3, 5, 0.02, 4);
assert_eq!(&ts[..4], &[0.0, 0.0, 0.0, 0.0]);
assert_eq!(ts.len(), 4 + 3 + 1);
let expected_tail = [0.0f32, 0.04, 0.06, 0.06];
for (got, want) in ts[4..].iter().zip(expected_tail.iter()) {
assert!((got - want).abs() < 1e-6, "got {got}, want {want}");
}
}
#[test]
fn ties_prefer_left_matching_hf() {
let flat = [0.0f32; 6];
let (text_indices, time_indices) = dynamic_time_warping(&flat, 2, 3);
assert_eq!(text_indices, vec![0, 1, 1, 1]);
assert_eq!(time_indices, vec![0, 0, 1, 2]);
}
#[test]
fn empty_matrix_yields_empty_path() {
let (text_indices, time_indices) = dynamic_time_warping(&[], 0, 5);
assert!(text_indices.is_empty());
assert!(time_indices.is_empty());
assert!(token_timestamps(&[], 0, 5, 0.02, 4).is_empty());
}
}