#[cfg(test)]
mod tests;
use crate::error::WhisperResult;
#[derive(Debug, Clone)]
pub struct InterpolationConfig {
pub method: InterpolationMethod,
pub smoothing_window: usize,
pub char_weight: f32,
pub uniform_weight: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationMethod {
Linear,
CharacterProportional,
Weighted,
AttentionGuided,
}
impl Default for InterpolationConfig {
fn default() -> Self {
Self {
method: InterpolationMethod::Weighted,
smoothing_window: 3,
char_weight: 0.7,
uniform_weight: 0.3,
}
}
}
impl InterpolationConfig {
#[must_use]
pub fn linear() -> Self {
Self {
method: InterpolationMethod::Linear,
smoothing_window: 0,
char_weight: 0.0,
uniform_weight: 1.0,
}
}
#[must_use]
pub fn character_proportional() -> Self {
Self {
method: InterpolationMethod::CharacterProportional,
smoothing_window: 0,
char_weight: 1.0,
uniform_weight: 0.0,
}
}
#[must_use]
pub fn with_smoothing(mut self, window: usize) -> Self {
self.smoothing_window = window;
self
}
#[must_use]
pub fn with_method(mut self, method: InterpolationMethod) -> Self {
self.method = method;
self
}
}
#[derive(Debug, Clone)]
pub struct TokenTimestamp {
pub index: usize,
pub text: String,
pub start: f32,
pub end: f32,
pub interpolated: bool,
pub confidence: f32,
}
impl TokenTimestamp {
#[must_use]
pub fn new(index: usize, text: String, start: f32, end: f32) -> Self {
Self {
index,
text,
start,
end,
interpolated: false,
confidence: 1.0,
}
}
#[must_use]
pub fn duration(&self) -> f32 {
self.end - self.start
}
pub fn mark_interpolated(&mut self, confidence: f32) {
self.interpolated = true;
self.confidence = confidence;
}
#[must_use]
pub fn interpolated(index: usize, text: String, start: f32, end: f32, confidence: f32) -> Self {
Self {
index,
text,
start,
end,
interpolated: true,
confidence,
}
}
}
#[derive(Debug, Clone)]
pub struct TimestampInterpolator {
config: InterpolationConfig,
}
impl TimestampInterpolator {
#[must_use]
pub fn new(config: InterpolationConfig) -> Self {
Self { config }
}
pub fn interpolate_word_tokens(
&self,
word_start: f32,
word_end: f32,
tokens: &[String],
start_index: usize,
) -> WhisperResult<Vec<TokenTimestamp>> {
if tokens.len() <= 1 {
return Ok(tokens
.iter()
.enumerate()
.map(|(i, t)| TokenTimestamp::new(start_index + i, t.clone(), word_start, word_end))
.collect());
}
match self.config.method {
InterpolationMethod::Linear => {
self.interpolate_linear(word_start, word_end, tokens, start_index)
}
InterpolationMethod::CharacterProportional => {
self.interpolate_char_proportional(word_start, word_end, tokens, start_index)
}
InterpolationMethod::Weighted => {
self.interpolate_weighted(word_start, word_end, tokens, start_index)
}
InterpolationMethod::AttentionGuided => {
self.interpolate_weighted(word_start, word_end, tokens, start_index)
}
}
}
pub fn interpolate_with_attention(
&self,
word_start: f32,
word_end: f32,
tokens: &[String],
attention_weights: &[Vec<f32>],
start_index: usize,
frame_rate: f32,
) -> WhisperResult<Vec<TokenTimestamp>> {
if tokens.is_empty() {
return Ok(Vec::new());
}
if attention_weights.len() != tokens.len() {
return self.interpolate_word_tokens(word_start, word_end, tokens, start_index);
}
let duration = word_end - word_start;
let mut timestamps = Vec::with_capacity(tokens.len());
let mut current_time = word_start;
for (i, (text, attention)) in tokens.iter().zip(attention_weights.iter()).enumerate() {
let (peak_frame, _) = attention
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, &0.0));
let peak_time = peak_frame as f32 / frame_rate;
let token_center = peak_time.clamp(word_start, word_end);
let token_duration = if i + 1 < tokens.len() {
let next_peak = attention_weights[i + 1]
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map_or(0, |(idx, _)| idx);
let next_time = (next_peak as f32 / frame_rate).clamp(word_start, word_end);
(next_time - token_center).max(duration / tokens.len() as f32 * 0.5)
} else {
word_end - token_center
};
let token_start = current_time;
let token_end = (token_start + token_duration).min(word_end);
timestamps.push(TokenTimestamp::interpolated(
start_index + i,
text.clone(),
token_start,
token_end,
0.8, ));
current_time = token_end;
}
Ok(timestamps)
}
#[allow(clippy::unnecessary_wraps)]
fn interpolate_linear(
&self,
word_start: f32,
word_end: f32,
tokens: &[String],
start_index: usize,
) -> WhisperResult<Vec<TokenTimestamp>> {
let _ = self; let duration = word_end - word_start;
let token_duration = duration / tokens.len() as f32;
let mut timestamps = Vec::with_capacity(tokens.len());
let mut current_time = word_start;
for (i, text) in tokens.iter().enumerate() {
let token_end = current_time + token_duration;
timestamps.push(TokenTimestamp::interpolated(
start_index + i,
text.clone(),
current_time,
token_end,
0.5, ));
current_time = token_end;
}
Ok(timestamps)
}
#[allow(clippy::unnecessary_wraps)]
fn interpolate_char_proportional(
&self,
word_start: f32,
word_end: f32,
tokens: &[String],
start_index: usize,
) -> WhisperResult<Vec<TokenTimestamp>> {
let _ = self; let duration = word_end - word_start;
let total_chars: usize = tokens.iter().map(|t| t.chars().count().max(1)).sum();
let mut timestamps = Vec::with_capacity(tokens.len());
let mut current_time = word_start;
for (i, text) in tokens.iter().enumerate() {
let char_count = text.chars().count().max(1);
let token_duration = (char_count as f32 / total_chars as f32) * duration;
let token_end = current_time + token_duration;
timestamps.push(TokenTimestamp::interpolated(
start_index + i,
text.clone(),
current_time,
token_end,
0.6, ));
current_time = token_end;
}
Ok(timestamps)
}
#[allow(clippy::unnecessary_wraps)]
fn interpolate_weighted(
&self,
word_start: f32,
word_end: f32,
tokens: &[String],
start_index: usize,
) -> WhisperResult<Vec<TokenTimestamp>> {
let duration = word_end - word_start;
let total_chars: usize = tokens.iter().map(|t| t.chars().count().max(1)).sum();
let uniform_duration = duration / tokens.len() as f32;
let mut timestamps = Vec::with_capacity(tokens.len());
let mut current_time = word_start;
for (i, text) in tokens.iter().enumerate() {
let char_count = text.chars().count().max(1);
let char_duration = (char_count as f32 / total_chars as f32) * duration;
let weighted_duration = self
.config
.char_weight
.mul_add(char_duration, self.config.uniform_weight * uniform_duration);
let token_end = (current_time + weighted_duration).min(word_end);
timestamps.push(TokenTimestamp::interpolated(
start_index + i,
text.clone(),
current_time,
token_end,
0.65, ));
current_time = token_end;
}
if let Some(last) = timestamps.last_mut() {
last.end = word_end;
}
Ok(timestamps)
}
pub fn smooth_timestamps(&self, timestamps: &mut [TokenTimestamp]) {
if self.config.smoothing_window == 0 || timestamps.len() < 3 {
return;
}
let window = self.config.smoothing_window;
let mut smoothed_starts = Vec::with_capacity(timestamps.len());
let mut smoothed_ends = Vec::with_capacity(timestamps.len());
for i in 0..timestamps.len() {
let start = i.saturating_sub(window / 2);
let end = (i + window / 2 + 1).min(timestamps.len());
let avg_start: f32 =
timestamps[start..end].iter().map(|t| t.start).sum::<f32>() / (end - start) as f32;
let avg_end: f32 =
timestamps[start..end].iter().map(|t| t.end).sum::<f32>() / (end - start) as f32;
smoothed_starts.push(avg_start);
smoothed_ends.push(avg_end);
}
for (i, ts) in timestamps.iter_mut().enumerate() {
if ts.interpolated {
ts.start = smoothed_starts[i];
ts.end = smoothed_ends[i];
}
}
for i in 1..timestamps.len() {
if timestamps[i].start < timestamps[i - 1].end {
let mid = (timestamps[i].start + timestamps[i - 1].end) / 2.0;
timestamps[i - 1].end = mid;
timestamps[i].start = mid;
}
}
}
}
impl Default for TimestampInterpolator {
fn default() -> Self {
Self::new(InterpolationConfig::default())
}
}