"""End-of-Utterance Detection.""" import time from typing import Optional class EouDetector: """ End-of-utterance detector. Fires EOU only after continuous silence for silence_threshold_ms. Short pauses between sentences do not trigger EOU because speech resets the silence timer (one EOU per turn). """ def __init__(self, silence_threshold_ms: int = 1000, min_speech_duration_ms: int = 250): """ Initialize EOU detector. Args: silence_threshold_ms: How long silence must last to trigger EOU (default 1000ms) min_speech_duration_ms: Minimum speech duration to consider valid (default 250ms) """ self.threshold = silence_threshold_ms / 1000.0 self.min_speech = min_speech_duration_ms / 1000.0 self._silence_threshold_ms = silence_threshold_ms self._min_speech_duration_ms = min_speech_duration_ms # State self.is_speaking = False self.speech_start_time = 0.0 self.silence_start_time: Optional[float] = None self.triggered = False def process(self, vad_status: str) -> bool: """ Process VAD status and detect end of utterance. Input: "Speech" or "Silence" (from VAD). Output: True if EOU detected, False otherwise. Short breaks between phrases reset the silence clock when speech resumes, so only one EOU is emitted after the user truly stops. """ now = time.time() if vad_status == "Speech": if not self.is_speaking: self.is_speaking = True self.speech_start_time = now self.triggered = False # Any speech resets silence timer — short pause + more speech = one utterance self.silence_start_time = None return False if vad_status == "Silence": if not self.is_speaking: return False if self.silence_start_time is None: self.silence_start_time = now speech_duration = self.silence_start_time - self.speech_start_time if speech_duration < self.min_speech: self.is_speaking = False self.silence_start_time = None return False silence_duration = now - self.silence_start_time if silence_duration >= self.threshold and not self.triggered: self.triggered = True self.is_speaking = False self.silence_start_time = None return True return False def reset(self) -> None: """Reset EOU detector state.""" self.is_speaking = False self.speech_start_time = 0.0 self.silence_start_time = None self.triggered = False