#[cfg(test)]
mod tests;
use crate::error::WhisperResult;
use super::alignment::TokenAlignment;
#[derive(Debug, Clone)]
pub struct BoundaryConfig {
pub min_silence_duration: f32,
pub silence_threshold: f32,
pub min_word_duration: f32,
pub max_word_duration: f32,
pub use_audio_energy: bool,
}
impl Default for BoundaryConfig {
fn default() -> Self {
Self {
min_silence_duration: 0.05,
silence_threshold: 0.01,
min_word_duration: 0.05,
max_word_duration: 5.0,
use_audio_energy: true,
}
}
}
impl BoundaryConfig {
#[must_use]
pub fn precise() -> Self {
Self {
min_silence_duration: 0.03,
silence_threshold: 0.005,
min_word_duration: 0.03,
max_word_duration: 3.0,
use_audio_energy: true,
}
}
#[must_use]
pub fn fast() -> Self {
Self {
min_silence_duration: 0.1,
silence_threshold: 0.02,
min_word_duration: 0.1,
max_word_duration: 10.0,
use_audio_energy: false,
}
}
#[must_use]
pub fn with_min_silence(mut self, duration: f32) -> Self {
self.min_silence_duration = duration;
self
}
#[must_use]
pub fn with_min_word_duration(mut self, duration: f32) -> Self {
self.min_word_duration = duration;
self
}
}
#[derive(Debug, Clone)]
pub struct WordBoundary {
pub start: f32,
pub end: f32,
pub start_confidence: f32,
pub end_confidence: f32,
pub audio_refined: bool,
pub token_indices: Vec<usize>,
}
impl WordBoundary {
#[must_use]
pub fn new(start: f32, end: f32) -> Self {
Self {
start,
end,
start_confidence: 0.5,
end_confidence: 0.5,
audio_refined: false,
token_indices: Vec::new(),
}
}
#[must_use]
pub fn duration(&self) -> f32 {
self.end - self.start
}
#[must_use]
pub fn confidence(&self) -> f32 {
(self.start_confidence + self.end_confidence) / 2.0
}
#[must_use]
pub fn is_high_confidence(&self) -> bool {
self.confidence() >= 0.7
}
#[must_use]
pub fn with_confidence(mut self, start: f32, end: f32) -> Self {
self.start_confidence = start;
self.end_confidence = end;
self
}
#[must_use]
pub fn with_tokens(mut self, indices: Vec<usize>) -> Self {
self.token_indices = indices;
self
}
#[must_use]
pub fn with_audio_refined(mut self, refined: bool) -> Self {
self.audio_refined = refined;
self
}
}
#[derive(Debug, Clone)]
pub struct BoundaryDetector {
config: BoundaryConfig,
}
impl BoundaryDetector {
#[must_use]
pub fn new(config: BoundaryConfig) -> Self {
Self { config }
}
pub fn detect_boundaries(
&self,
alignments: &[TokenAlignment],
word_starts: &[usize],
) -> WhisperResult<Vec<WordBoundary>> {
if alignments.is_empty() || word_starts.is_empty() {
return Ok(Vec::new());
}
let mut boundaries = Vec::with_capacity(word_starts.len());
for (i, &start_idx) in word_starts.iter().enumerate() {
let end_idx = if i + 1 < word_starts.len() {
word_starts[i + 1] - 1
} else {
alignments.len() - 1
};
if start_idx >= alignments.len() {
continue;
}
let start_alignment = &alignments[start_idx];
let end_alignment = &alignments[end_idx.min(alignments.len() - 1)];
let mut boundary =
WordBoundary::new(start_alignment.start_time, end_alignment.end_time)
.with_confidence(start_alignment.confidence, end_alignment.confidence)
.with_tokens((start_idx..=end_idx.min(alignments.len() - 1)).collect());
boundary = self.validate_boundary(boundary);
boundaries.push(boundary);
}
Ok(boundaries)
}
pub fn refine_with_audio(
&self,
boundaries: &[WordBoundary],
audio_energy: &[f32],
frame_rate: f32,
) -> WhisperResult<Vec<WordBoundary>> {
if !self.config.use_audio_energy || audio_energy.is_empty() {
return Ok(boundaries.to_vec());
}
let mut refined = Vec::with_capacity(boundaries.len());
for boundary in boundaries {
let refined_boundary = self.refine_single_boundary(boundary, audio_energy, frame_rate);
refined.push(refined_boundary);
}
Ok(refined)
}
fn refine_single_boundary(
&self,
boundary: &WordBoundary,
audio_energy: &[f32],
frame_rate: f32,
) -> WordBoundary {
let mut result = boundary.clone();
let start_frame = (boundary.start * frame_rate) as usize;
let end_frame = (boundary.end * frame_rate) as usize;
if let Some(refined_start) = self.find_speech_onset(audio_energy, start_frame, frame_rate) {
result.start = refined_start;
result.start_confidence = 0.9;
}
if let Some(refined_end) = self.find_speech_offset(audio_energy, end_frame, frame_rate) {
result.end = refined_end;
result.end_confidence = 0.9;
}
result.audio_refined = true;
result
}
fn search_range(
&self,
energy_len: usize,
approx_frame: usize,
frame_rate: f32,
) -> Option<(usize, usize)> {
let search_window = (self.config.min_word_duration * frame_rate) as usize;
let start = approx_frame.saturating_sub(search_window);
let end = (approx_frame + search_window).min(energy_len);
(start < end && end <= energy_len).then_some((start, end))
}
#[allow(clippy::needless_range_loop)]
fn find_speech_onset(
&self,
energy: &[f32],
approx_frame: usize,
frame_rate: f32,
) -> Option<f32> {
let (start, end) = self.search_range(energy.len(), approx_frame, frame_rate)?;
for i in start..end {
if energy[i] > self.config.silence_threshold {
return Some(i as f32 / frame_rate);
}
}
None
}
fn find_speech_offset(
&self,
energy: &[f32],
approx_frame: usize,
frame_rate: f32,
) -> Option<f32> {
let (start, end) = self.search_range(energy.len(), approx_frame, frame_rate)?;
for i in (start..end).rev() {
if energy[i] > self.config.silence_threshold {
return Some((i + 1) as f32 / frame_rate);
}
}
None
}
fn validate_boundary(&self, mut boundary: WordBoundary) -> WordBoundary {
if boundary.duration() < self.config.min_word_duration {
boundary.end = boundary.start + self.config.min_word_duration;
}
if boundary.duration() > self.config.max_word_duration {
boundary.end = boundary.start + self.config.max_word_duration;
boundary.end_confidence *= 0.5; }
if boundary.end <= boundary.start {
boundary.end = boundary.start + self.config.min_word_duration;
}
boundary
}
pub fn compute_boundary_confidence(&self, alignments: &[TokenAlignment]) -> f32 {
if alignments.is_empty() {
return 0.0;
}
let avg_confidence: f32 =
alignments.iter().map(|a| a.confidence).sum::<f32>() / alignments.len() as f32;
let mut monotonic_score = 1.0f32;
for i in 1..alignments.len() {
if alignments[i].frame_position < alignments[i - 1].frame_position {
monotonic_score *= 0.9;
}
}
avg_confidence * monotonic_score
}
pub fn detect_silence_gaps(&self, boundaries: &[WordBoundary]) -> Vec<(f32, f32)> {
let mut gaps = Vec::new();
for i in 1..boundaries.len() {
let gap_start = boundaries[i - 1].end;
let gap_end = boundaries[i].start;
let gap_duration = gap_end - gap_start;
if gap_duration >= self.config.min_silence_duration {
gaps.push((gap_start, gap_end));
}
}
gaps
}
}
impl Default for BoundaryDetector {
fn default() -> Self {
Self::new(BoundaryConfig::default())
}
}