|
|
@@ -8,12 +8,20 @@ import numpy as np
|
|
|
import webrtcvad
|
|
|
|
|
|
|
|
|
+END_OF_TURN = "end_of_turn"
|
|
|
+PAUSE = "pause"
|
|
|
+HARD_CAP = "hard_cap"
|
|
|
+FLUSH = "flush"
|
|
|
+
|
|
|
+
|
|
|
@dataclass
|
|
|
class Utterance:
|
|
|
"""A complete utterance ready for transcription."""
|
|
|
audio: np.ndarray # float32, mono, normalized to [-1, 1], 16 kHz
|
|
|
duration_s: float
|
|
|
started_at: float # time.time() when confirmed
|
|
|
+ rms: float = 0.0 # root-mean-square level of the emitted audio
|
|
|
+ reason: str = "unknown" # why the utterance ended (END_OF_TURN/PAUSE/HARD_CAP/FLUSH)
|
|
|
|
|
|
|
|
|
def make_vad(aggressiveness: int) -> webrtcvad.Vad:
|
|
|
@@ -137,18 +145,18 @@ class FrameSegmenter:
|
|
|
|
|
|
if self.silence_run >= self.silence_frames:
|
|
|
# End of turn: the speaker stopped.
|
|
|
- return self._finish(keep_tail=True)
|
|
|
+ return self._finish(keep_tail=True, reason=END_OF_TURN)
|
|
|
|
|
|
if len(self.segment) >= self.max_frames:
|
|
|
# Hard cap (speaker never paused) - must cut somewhere.
|
|
|
- return self._finish(keep_tail=False)
|
|
|
+ return self._finish(keep_tail=False, reason=HARD_CAP)
|
|
|
|
|
|
if (
|
|
|
len(self.segment) >= self.soft_max_frames
|
|
|
and self.silence_run >= self.chunk_pause_frames
|
|
|
):
|
|
|
# Long speech: end a chunk at a natural pause instead of shipping a huge blob.
|
|
|
- return self._finish(keep_tail=True)
|
|
|
+ return self._finish(keep_tail=True, reason=PAUSE)
|
|
|
|
|
|
return None
|
|
|
|
|
|
@@ -165,10 +173,10 @@ class FrameSegmenter:
|
|
|
def flush(self) -> Utterance | None:
|
|
|
"""Finalize any in-progress utterance (called on shutdown)."""
|
|
|
if self.speaking:
|
|
|
- return self._finish(keep_tail=False)
|
|
|
+ return self._finish(keep_tail=False, reason=FLUSH)
|
|
|
return None
|
|
|
|
|
|
- def _finish(self, keep_tail: bool) -> Utterance | None:
|
|
|
+ def _finish(self, keep_tail: bool, reason: str) -> Utterance | None:
|
|
|
"""Finalize and emit an utterance, or discard it if it is a blip or too quiet."""
|
|
|
frames = self.segment
|
|
|
trailing_silence = self.silence_run
|
|
|
@@ -215,4 +223,6 @@ class FrameSegmenter:
|
|
|
audio=audio,
|
|
|
duration_s=len(audio) / self.sample_rate,
|
|
|
started_at=self.started_at,
|
|
|
+ rms=rms,
|
|
|
+ reason=reason,
|
|
|
)
|