#[cfg(test)]
mod tests;
use crate::error::WhisperResult;
#[derive(Debug, Clone)]
pub struct SegmentationConfig {
pub min_segment_duration: f32,
pub energy_threshold: f32,
pub zcr_threshold: f32,
pub frame_size: usize,
pub frame_hop: usize,
pub smoothing_window: usize,
}
impl Default for SegmentationConfig {
fn default() -> Self {
Self {
min_segment_duration: 0.3,
energy_threshold: 0.01,
zcr_threshold: 0.1,
frame_size: 400, frame_hop: 160, smoothing_window: 5,
}
}
}
impl SegmentationConfig {
#[must_use]
pub fn for_realtime() -> Self {
Self {
min_segment_duration: 0.2,
smoothing_window: 3,
..Default::default()
}
}
#[must_use]
pub fn for_accuracy() -> Self {
Self {
min_segment_duration: 0.5,
smoothing_window: 7,
energy_threshold: 0.005,
..Default::default()
}
}
#[must_use]
pub fn with_min_segment_duration(mut self, duration: f32) -> Self {
self.min_segment_duration = duration;
self
}
#[must_use]
pub fn with_energy_threshold(mut self, threshold: f32) -> Self {
self.energy_threshold = threshold;
self
}
}
#[derive(Debug, Clone)]
pub struct SpeakerSegment {
speaker_id: usize,
start: f32,
end: f32,
confidence: f32,
}
impl SpeakerSegment {
#[must_use]
pub fn new(speaker_id: usize, start: f32, end: f32, confidence: f32) -> Self {
Self {
speaker_id,
start,
end,
confidence,
}
}
#[must_use]
pub fn unknown(start: f32, end: f32) -> Self {
Self {
speaker_id: usize::MAX,
start,
end,
confidence: 0.0,
}
}
#[must_use]
pub fn speaker_id(&self) -> usize {
self.speaker_id
}
#[must_use]
pub fn start(&self) -> f32 {
self.start
}
#[must_use]
pub fn end(&self) -> f32 {
self.end
}
#[must_use]
pub fn duration(&self) -> f32 {
self.end - self.start
}
#[must_use]
pub fn confidence(&self) -> f32 {
self.confidence
}
#[must_use]
pub fn with_speaker_id(&self, speaker_id: usize) -> Self {
Self {
speaker_id,
start: self.start,
end: self.end,
confidence: self.confidence,
}
}
#[must_use]
pub fn extend_to(&self, new_end: f32) -> Self {
Self {
speaker_id: self.speaker_id,
start: self.start,
end: new_end,
confidence: self.confidence,
}
}
#[must_use]
pub fn overlaps(&self, start: f32, end: f32) -> bool {
self.start < end && self.end > start
}
#[must_use]
pub fn overlap_duration(&self, other: &Self) -> f32 {
let overlap_start = self.start.max(other.start);
let overlap_end = self.end.min(other.end);
(overlap_end - overlap_start).max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct SpeakerTurn {
from_speaker: usize,
to_speaker: usize,
time: f32,
}
impl SpeakerTurn {
#[must_use]
pub fn new(from_speaker: usize, to_speaker: usize, time: f32) -> Self {
Self {
from_speaker,
to_speaker,
time,
}
}
#[must_use]
pub fn from_speaker(&self) -> usize {
self.from_speaker
}
#[must_use]
pub fn to_speaker(&self) -> usize {
self.to_speaker
}
#[must_use]
pub fn time(&self) -> f32 {
self.time
}
}
#[derive(Debug)]
pub struct TurnDetector {
config: SegmentationConfig,
}
impl TurnDetector {
#[must_use]
pub fn new(config: SegmentationConfig) -> Self {
Self { config }
}
pub fn detect_segments(
&self,
audio: &[f32],
sample_rate: u32,
) -> WhisperResult<Vec<SpeakerSegment>> {
if audio.is_empty() {
return Ok(Vec::new());
}
let (energy, zcr) = self.compute_energy_and_zcr(audio);
let vad = self.detect_voice_activity(&energy, &zcr);
let smoothed_vad = self.smooth_vad(&vad);
let segments = self.vad_to_segments(&smoothed_vad, sample_rate);
let filtered = segments
.into_iter()
.filter(|s| s.duration() >= self.config.min_segment_duration)
.collect();
Ok(filtered)
}
fn compute_energy_and_zcr(&self, audio: &[f32]) -> (Vec<f32>, Vec<f32>) {
let num_frames =
(audio.len().saturating_sub(self.config.frame_size)) / self.config.frame_hop + 1;
if num_frames == 0 {
return (Vec::new(), Vec::new());
}
let mut energy = Vec::with_capacity(num_frames);
let mut zcr = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * self.config.frame_hop;
let end = (start + self.config.frame_size).min(audio.len());
let frame_energy: f32 = audio[start..end].iter().map(|&s| s * s).sum();
energy.push((frame_energy / (end - start) as f32).sqrt());
let frame = &audio[start..end];
let crossings: f32 = frame
.windows(2)
.filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
.count() as f32;
zcr.push(crossings / (end - start - 1).max(1) as f32);
}
(energy, zcr)
}
#[cfg(test)]
fn compute_zcr(&self, audio: &[f32]) -> Vec<f32> {
self.compute_energy_and_zcr(audio).1
}
fn compute_energy(&self, audio: &[f32]) -> Vec<f32> {
self.compute_energy_and_zcr(audio).0
}
fn detect_voice_activity(&self, energy: &[f32], zcr: &[f32]) -> Vec<bool> {
if energy.is_empty() {
return Vec::new();
}
let sorted_energy: Vec<f32> = {
let mut e = energy.to_vec();
e.sort_by(|a, b| a.total_cmp(b));
e
};
let noise_floor = sorted_energy[sorted_energy.len() / 4]; let adaptive_threshold = noise_floor + self.config.energy_threshold;
energy
.iter()
.zip(zcr.iter())
.map(|(&e, &z)| e > adaptive_threshold && z < self.config.zcr_threshold)
.collect()
}
fn smooth_vad(&self, vad: &[bool]) -> Vec<bool> {
if vad.len() <= self.config.smoothing_window {
return vad.to_vec();
}
let half_window = self.config.smoothing_window / 2;
let mut smoothed = Vec::with_capacity(vad.len());
for i in 0..vad.len() {
let start = i.saturating_sub(half_window);
let end = (i + half_window + 1).min(vad.len());
let active_count = vad[start..end].iter().filter(|&&v| v).count();
let threshold = (end - start) / 2;
smoothed.push(active_count > threshold);
}
smoothed
}
fn vad_to_segments(&self, vad: &[bool], sample_rate: u32) -> Vec<SpeakerSegment> {
let frame_duration = self.config.frame_hop as f32 / sample_rate as f32;
let mut segments = Vec::new();
let mut in_speech = false;
let mut segment_start = 0.0f32;
for (i, &is_speech) in vad.iter().enumerate() {
let time = i as f32 * frame_duration;
if is_speech && !in_speech {
in_speech = true;
segment_start = time;
} else if !is_speech && in_speech {
in_speech = false;
segments.push(SpeakerSegment::unknown(segment_start, time));
}
}
if in_speech {
let end_time = vad.len() as f32 * frame_duration;
segments.push(SpeakerSegment::unknown(segment_start, end_time));
}
segments
}
pub fn detect_change_points(&self, audio: &[f32], sample_rate: u32) -> WhisperResult<Vec<f32>> {
let energy = self.compute_energy(audio);
if energy.len() < 10 {
return Ok(Vec::new());
}
let frame_duration = self.config.frame_hop as f32 / sample_rate as f32;
let mut change_points = Vec::new();
let window = 5;
for i in window..energy.len() - window {
let left_mean: f32 = energy[i - window..i].iter().sum::<f32>() / window as f32;
let right_mean: f32 = energy[i..i + window].iter().sum::<f32>() / window as f32;
let diff = (right_mean - left_mean).abs();
let threshold = (left_mean + right_mean) / 2.0 * 0.5;
if diff > threshold && diff > self.config.energy_threshold {
let time = i as f32 * frame_duration;
change_points.push(time);
}
}
let merged = self.merge_nearby_points(&change_points, 0.3);
Ok(merged)
}
fn merge_nearby_points(&self, points: &[f32], min_gap: f32) -> Vec<f32> {
let _ = self; if points.is_empty() {
return Vec::new();
}
let mut merged = vec![points[0]];
for &point in points.iter().skip(1) {
if let Some(&last) = merged.last() {
if point - last >= min_gap {
merged.push(point);
}
}
}
merged
}
#[must_use]
pub fn config(&self) -> &SegmentationConfig {
&self.config
}
}