vad.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. # -*- coding: utf-8 -*-
  2. """
  3. Voice Activity Detection (VAD) für Silence-Erkennung.
  4. """
  5. from dataclasses import dataclass, field
  6. from datetime import datetime, timedelta
  7. from enum import Enum
  8. from typing import Callable
  9. import numpy as np
  10. class VADState(Enum):
  11. """VAD-Zustand."""
  12. WAITING = "waiting" # Warten auf Sprache
  13. SPEECH = "speech" # Sprache erkannt
  14. SILENCE = "silence" # Stille nach Sprache
  15. TIMEOUT = "timeout" # Timeout erreicht
  16. NO_SPEECH = "no_speech" # Keine Sprache innerhalb Timeout
  17. @dataclass
  18. class VADConfig:
  19. """Konfiguration für Voice Activity Detection."""
  20. # Schwellenwerte
  21. speech_threshold: float = 0.02 # RMS-Threshold für Sprache
  22. silence_threshold: float = 0.01 # RMS-Threshold für Stille
  23. # Zeitkonfiguration
  24. min_speech_ms: int = 200 # Mindestdauer für Sprache
  25. silence_duration_ms: int = 3000 # Stille-Dauer für Ende (3 Sekunden)
  26. max_duration_ms: int = 60000 # Maximale Aufnahmedauer (60 Sekunden)
  27. # Audio-Format
  28. sample_rate: int = 16000
  29. frame_length_ms: int = 80
  30. # No-Speech Timeout
  31. no_speech_timeout_ms: int = 5000 # 5 Sekunden ohne Sprache → Abbruch
  32. # Glättung
  33. smoothing_frames: int = 3 # Frames für Glättung
  34. @property
  35. def frame_length_samples(self) -> int:
  36. """Samples pro Frame."""
  37. return int(self.sample_rate * self.frame_length_ms / 1000)
  38. @dataclass
  39. class SpeechSegment:
  40. """Ein Sprach-Segment."""
  41. start_time: datetime
  42. end_time: datetime | None = None
  43. peak_level: float = 0.0
  44. avg_level: float = 0.0
  45. frame_count: int = 0
  46. @property
  47. def duration_ms(self) -> float:
  48. """Dauer in Millisekunden."""
  49. if self.end_time is None:
  50. return (datetime.now() - self.start_time).total_seconds() * 1000
  51. return (self.end_time - self.start_time).total_seconds() * 1000
  52. @property
  53. def is_complete(self) -> bool:
  54. """Prüft ob Segment abgeschlossen ist."""
  55. return self.end_time is not None
  56. class VoiceActivityDetector:
  57. """
  58. Voice Activity Detector für Sprach-/Stille-Erkennung.
  59. Erkennt:
  60. - Beginn von Sprache (User hat angefangen zu sprechen)
  61. - Ende von Sprache (Stille nach Sprache)
  62. - Timeout (maximale Aufnahmedauer erreicht)
  63. """
  64. def __init__(self, config: VADConfig | None = None):
  65. """
  66. Initialisiert den VAD.
  67. Args:
  68. config: VAD-Konfiguration
  69. """
  70. self._config = config or VADConfig()
  71. # State
  72. self._state = VADState.WAITING
  73. self._start_time: datetime | None = None
  74. self._speech_detected = False
  75. self._speech_start_time: datetime | None = None
  76. self._last_speech_time: datetime | None = None
  77. # Level-Tracking
  78. self._level_history: list[float] = []
  79. self._peak_level = 0.0
  80. self._total_level = 0.0
  81. self._frame_count = 0
  82. # Segmente
  83. self._current_segment: SpeechSegment | None = None
  84. self._segments: list[SpeechSegment] = []
  85. # Callbacks
  86. self._on_speech_start: list[Callable[[], None]] = []
  87. self._on_speech_end: list[Callable[[float], None]] = [] # Dauer in ms
  88. self._on_silence: list[Callable[[], None]] = []
  89. self._on_timeout: list[Callable[[], None]] = []
  90. self._on_no_speech: list[Callable[[], None]] = []
  91. @property
  92. def config(self) -> VADConfig:
  93. """Gibt Konfiguration zurück."""
  94. return self._config
  95. @property
  96. def state(self) -> VADState:
  97. """Aktueller Zustand."""
  98. return self._state
  99. @property
  100. def is_active(self) -> bool:
  101. """Prüft ob VAD aktiv ist."""
  102. return self._start_time is not None
  103. @property
  104. def has_speech(self) -> bool:
  105. """Prüft ob Sprache erkannt wurde."""
  106. return self._speech_detected
  107. @property
  108. def duration_ms(self) -> float:
  109. """Aktuelle Dauer in Millisekunden."""
  110. if self._start_time is None:
  111. return 0.0
  112. return (datetime.now() - self._start_time).total_seconds() * 1000
  113. @property
  114. def silence_duration_ms(self) -> float:
  115. """Aktuelle Stille-Dauer in Millisekunden."""
  116. if self._last_speech_time is None:
  117. return 0.0
  118. return (datetime.now() - self._last_speech_time).total_seconds() * 1000
  119. @property
  120. def segments(self) -> list[SpeechSegment]:
  121. """Alle erkannten Sprach-Segmente."""
  122. return list(self._segments)
  123. def start(self) -> None:
  124. """Startet die VAD-Analyse."""
  125. self._start_time = datetime.now()
  126. self._state = VADState.WAITING
  127. self._speech_detected = False
  128. self._speech_start_time = None
  129. self._last_speech_time = None
  130. self._level_history.clear()
  131. self._peak_level = 0.0
  132. self._total_level = 0.0
  133. self._frame_count = 0
  134. self._current_segment = None
  135. self._segments.clear()
  136. def stop(self) -> None:
  137. """Stoppt die VAD-Analyse."""
  138. # Schließe aktuelles Segment ab
  139. if self._current_segment and not self._current_segment.is_complete:
  140. self._current_segment.end_time = datetime.now()
  141. self._segments.append(self._current_segment)
  142. self._current_segment = None
  143. self._start_time = None
  144. def process_frame(self, audio_frame: bytes | np.ndarray) -> VADState:
  145. """
  146. Verarbeitet einen Audio-Frame.
  147. Args:
  148. audio_frame: Audio-Daten (16-bit PCM oder numpy array)
  149. Returns:
  150. Aktueller VAD-Zustand
  151. """
  152. if self._start_time is None:
  153. self.start()
  154. # Konvertiere zu numpy
  155. if isinstance(audio_frame, bytes):
  156. audio = np.frombuffer(audio_frame, dtype=np.int16)
  157. else:
  158. audio = audio_frame
  159. # Berechne RMS-Level
  160. level = self._calculate_rms(audio)
  161. self._update_level_tracking(level)
  162. # Prüfe Timeout
  163. if self.duration_ms >= self._config.max_duration_ms:
  164. self._state = VADState.TIMEOUT
  165. self._trigger_timeout()
  166. return self._state
  167. # Geglättetes Level für Entscheidungen
  168. smoothed_level = self._get_smoothed_level()
  169. # State-Machine
  170. if self._state == VADState.WAITING:
  171. self._handle_waiting_state(smoothed_level)
  172. elif self._state == VADState.SPEECH:
  173. self._handle_speech_state(smoothed_level)
  174. elif self._state == VADState.SILENCE:
  175. self._handle_silence_state(smoothed_level)
  176. return self._state
  177. def _calculate_rms(self, audio: np.ndarray) -> float:
  178. """Berechnet RMS-Level."""
  179. if len(audio) == 0:
  180. return 0.0
  181. audio_float = audio.astype(np.float32) / 32768.0
  182. rms = np.sqrt(np.mean(audio_float ** 2))
  183. return float(rms)
  184. def _update_level_tracking(self, level: float) -> None:
  185. """Aktualisiert Level-Tracking."""
  186. self._level_history.append(level)
  187. # Begrenze History
  188. max_history = self._config.smoothing_frames * 3
  189. while len(self._level_history) > max_history:
  190. self._level_history.pop(0)
  191. # Statistiken
  192. self._peak_level = max(self._peak_level, level)
  193. self._total_level += level
  194. self._frame_count += 1
  195. def _get_smoothed_level(self) -> float:
  196. """Gibt geglättetes Level zurück."""
  197. if len(self._level_history) < self._config.smoothing_frames:
  198. return sum(self._level_history) / len(self._level_history) if self._level_history else 0.0
  199. # Durchschnitt der letzten N Frames
  200. recent = self._level_history[-self._config.smoothing_frames:]
  201. return sum(recent) / len(recent)
  202. def _handle_waiting_state(self, level: float) -> None:
  203. """Behandelt WAITING-Zustand."""
  204. if level >= self._config.speech_threshold:
  205. # Sprache beginnt
  206. self._speech_start_time = datetime.now()
  207. self._last_speech_time = datetime.now()
  208. self._state = VADState.SPEECH
  209. # Starte neues Segment
  210. self._current_segment = SpeechSegment(
  211. start_time=datetime.now(),
  212. peak_level=level,
  213. avg_level=level,
  214. frame_count=1,
  215. )
  216. else:
  217. # No-Speech Timeout prüfen
  218. if (self._config.no_speech_timeout_ms > 0
  219. and self._start_time
  220. and self.duration_ms >= self._config.no_speech_timeout_ms):
  221. self._state = VADState.NO_SPEECH
  222. self._trigger_no_speech()
  223. def _handle_speech_state(self, level: float) -> None:
  224. """Behandelt SPEECH-Zustand."""
  225. if level >= self._config.silence_threshold:
  226. # Immer noch Sprache
  227. self._last_speech_time = datetime.now()
  228. # Update Segment
  229. if self._current_segment:
  230. self._current_segment.peak_level = max(self._current_segment.peak_level, level)
  231. self._current_segment.frame_count += 1
  232. self._current_segment.avg_level = (
  233. (self._current_segment.avg_level * (self._current_segment.frame_count - 1) + level)
  234. / self._current_segment.frame_count
  235. )
  236. # Prüfe ob Mindestdauer erreicht
  237. if self._speech_start_time:
  238. speech_duration = (datetime.now() - self._speech_start_time).total_seconds() * 1000
  239. if speech_duration >= self._config.min_speech_ms and not self._speech_detected:
  240. self._speech_detected = True
  241. self._trigger_speech_start()
  242. else:
  243. # Beginn von Stille
  244. self._state = VADState.SILENCE
  245. def _handle_silence_state(self, level: float) -> None:
  246. """Behandelt SILENCE-Zustand."""
  247. if level >= self._config.speech_threshold:
  248. # Sprache wieder da
  249. self._last_speech_time = datetime.now()
  250. self._state = VADState.SPEECH
  251. # Update Segment
  252. if self._current_segment:
  253. self._current_segment.peak_level = max(self._current_segment.peak_level, level)
  254. self._current_segment.frame_count += 1
  255. else:
  256. # Prüfe Stille-Dauer (nur wenn vorher Sprache war)
  257. if self._speech_detected and self._last_speech_time:
  258. silence_ms = (datetime.now() - self._last_speech_time).total_seconds() * 1000
  259. if silence_ms >= self._config.silence_duration_ms:
  260. # Stille lange genug - Ende
  261. self._complete_segment()
  262. self._trigger_silence()
  263. def _complete_segment(self) -> None:
  264. """Schließt aktuelles Segment ab."""
  265. if self._current_segment:
  266. self._current_segment.end_time = datetime.now()
  267. self._segments.append(self._current_segment)
  268. self._current_segment = None
  269. def _trigger_speech_start(self) -> None:
  270. """Triggert Speech-Start Callbacks."""
  271. for callback in self._on_speech_start:
  272. try:
  273. callback()
  274. except Exception:
  275. pass
  276. def _trigger_silence(self) -> None:
  277. """Triggert Silence Callbacks."""
  278. speech_duration = 0.0
  279. if self._speech_start_time and self._last_speech_time:
  280. speech_duration = (self._last_speech_time - self._speech_start_time).total_seconds() * 1000
  281. for callback in self._on_speech_end:
  282. try:
  283. callback(speech_duration)
  284. except Exception:
  285. pass
  286. for callback in self._on_silence:
  287. try:
  288. callback()
  289. except Exception:
  290. pass
  291. def _trigger_no_speech(self) -> None:
  292. """Triggert No-Speech Callbacks."""
  293. for callback in self._on_no_speech:
  294. try:
  295. callback()
  296. except Exception:
  297. pass
  298. def _trigger_timeout(self) -> None:
  299. """Triggert Timeout Callbacks."""
  300. self._complete_segment()
  301. for callback in self._on_timeout:
  302. try:
  303. callback()
  304. except Exception:
  305. pass
  306. def on_speech_start(self, callback: Callable[[], None]) -> None:
  307. """Registriert Callback für Sprach-Beginn."""
  308. self._on_speech_start.append(callback)
  309. def on_speech_end(self, callback: Callable[[float], None]) -> None:
  310. """Registriert Callback für Sprach-Ende (mit Dauer in ms)."""
  311. self._on_speech_end.append(callback)
  312. def on_silence(self, callback: Callable[[], None]) -> None:
  313. """Registriert Callback für Stille-Erkennung."""
  314. self._on_silence.append(callback)
  315. def on_timeout(self, callback: Callable[[], None]) -> None:
  316. """Registriert Callback für Timeout."""
  317. self._on_timeout.append(callback)
  318. def on_no_speech(self, callback: Callable[[], None]) -> None:
  319. """Registriert Callback für No-Speech-Timeout."""
  320. self._on_no_speech.append(callback)
  321. def get_stats(self) -> dict:
  322. """Gibt Statistiken zurück."""
  323. avg_level = self._total_level / self._frame_count if self._frame_count > 0 else 0.0
  324. return {
  325. "state": self._state.value,
  326. "is_active": self.is_active,
  327. "has_speech": self._speech_detected,
  328. "duration_ms": self.duration_ms,
  329. "silence_duration_ms": self.silence_duration_ms,
  330. "peak_level": self._peak_level,
  331. "avg_level": avg_level,
  332. "frame_count": self._frame_count,
  333. "segment_count": len(self._segments),
  334. }
  335. def reset(self) -> None:
  336. """Setzt VAD zurück."""
  337. self._state = VADState.WAITING
  338. self._start_time = None
  339. self._speech_detected = False
  340. self._speech_start_time = None
  341. self._last_speech_time = None
  342. self._level_history.clear()
  343. self._peak_level = 0.0
  344. self._total_level = 0.0
  345. self._frame_count = 0
  346. self._current_segment = None
  347. self._segments.clear()