vad.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. """
  2. Voice Activity Detection (VAD) for Voice Recognition system.
  3. This module provides comprehensive VAD capabilities including WebRTC VAD,
  4. energy-based detection, and hybrid approaches optimized for speaker recognition.
  5. """
  6. import logging
  7. import warnings
  8. from typing import Optional, List, Tuple, Union, Dict, Any
  9. from dataclasses import dataclass
  10. import numpy as np
  11. import torch
  12. import torch.nn as nn
  13. import torch.nn.functional as F
  14. from abc import ABC, abstractmethod
  15. # Configure logging
  16. logger = logging.getLogger(__name__)
  17. # Try to import WebRTC VAD
  18. try:
  19. import webrtcvad
  20. WEBRTC_AVAILABLE = True
  21. except ImportError:
  22. WEBRTC_AVAILABLE = False
  23. logger.warning("WebRTC VAD not available. Install webrtcvad package for WebRTC support.")
  24. @dataclass
  25. class VADResult:
  26. """Result of voice activity detection."""
  27. is_speech: bool
  28. confidence: float
  29. start_time: float
  30. end_time: float
  31. frame_results: Optional[List[bool]] = None
  32. energy_levels: Optional[List[float]] = None
  33. def to_dict(self) -> Dict[str, Any]:
  34. """Convert to dictionary representation."""
  35. return {
  36. 'is_speech': self.is_speech,
  37. 'confidence': self.confidence,
  38. 'start_time': self.start_time,
  39. 'end_time': self.end_time,
  40. 'frame_results': self.frame_results,
  41. 'energy_levels': self.energy_levels
  42. }
  43. @dataclass
  44. class VADConfig:
  45. """Configuration for Voice Activity Detection."""
  46. # General settings
  47. sample_rate: int = 16000
  48. frame_duration_ms: int = 30 # Frame duration in milliseconds
  49. # WebRTC VAD settings
  50. webrtc_aggressiveness: int = 2 # 0-3, higher = more aggressive
  51. # Energy-based VAD settings
  52. energy_threshold: float = 0.01
  53. energy_min_length_ms: int = 100 # Minimum speech length
  54. energy_hangover_ms: int = 50 # Hangover time after speech
  55. energy_prethreshold: float = 0.005 # Pre-trigger threshold
  56. # Spectral VAD settings
  57. spectral_threshold: float = 0.5
  58. spectral_rolloff_threshold: float = 0.85
  59. spectral_centroid_threshold: float = 2000.0
  60. # Temporal smoothing
  61. use_temporal_smoothing: bool = True
  62. smoothing_window_size: int = 5
  63. # Combination settings (for hybrid VAD)
  64. combination_mode: str = "weighted" # "majority", "weighted", "conservative", "aggressive"
  65. webrtc_weight: float = 0.4
  66. energy_weight: float = 0.3
  67. spectral_weight: float = 0.3
  68. def __post_init__(self):
  69. """Validate configuration parameters."""
  70. if self.frame_duration_ms not in [10, 20, 30]:
  71. raise ValueError("WebRTC VAD frame duration must be 10, 20, or 30 ms")
  72. if not 0 <= self.webrtc_aggressiveness <= 3:
  73. raise ValueError("WebRTC aggressiveness must be 0-3")
  74. # Validate combination weights
  75. total_weight = self.webrtc_weight + self.energy_weight + self.spectral_weight
  76. if abs(total_weight - 1.0) > 1e-6:
  77. logger.warning(f"Combination weights sum to {total_weight}, not 1.0")
  78. class VoiceActivityDetector(ABC):
  79. """Abstract base class for Voice Activity Detectors."""
  80. def __init__(self, config: VADConfig):
  81. self.config = config
  82. self.sample_rate = config.sample_rate
  83. self.frame_size = int(config.frame_duration_ms * config.sample_rate / 1000)
  84. @abstractmethod
  85. def detect(self, audio: Union[np.ndarray, torch.Tensor]) -> VADResult:
  86. """
  87. Detect voice activity in audio.
  88. Args:
  89. audio: Audio signal
  90. Returns:
  91. VAD result
  92. """
  93. pass
  94. def _ensure_numpy(self, audio: Union[np.ndarray, torch.Tensor]) -> np.ndarray:
  95. """Convert audio to numpy array if needed."""
  96. if isinstance(audio, torch.Tensor):
  97. return audio.detach().cpu().numpy()
  98. return audio
  99. def _ensure_torch(self, audio: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
  100. """Convert audio to torch tensor if needed."""
  101. if isinstance(audio, np.ndarray):
  102. return torch.from_numpy(audio).float()
  103. return audio.float()
  104. class WebRTCVAD(VoiceActivityDetector):
  105. """WebRTC-based Voice Activity Detector."""
  106. def __init__(self, config: VADConfig):
  107. super().__init__(config)
  108. if not WEBRTC_AVAILABLE:
  109. raise ImportError("WebRTC VAD not available. Install webrtcvad package.")
  110. self.vad = webrtcvad.Vad(config.webrtc_aggressiveness)
  111. # WebRTC VAD only supports 8kHz, 16kHz, 32kHz, 48kHz
  112. if config.sample_rate not in [8000, 16000, 32000, 48000]:
  113. logger.warning(f"WebRTC VAD may not work optimally with {config.sample_rate}Hz")
  114. def detect(self, audio: Union[np.ndarray, torch.Tensor]) -> VADResult:
  115. """Detect voice activity using WebRTC VAD."""
  116. audio_np = self._ensure_numpy(audio)
  117. # Ensure audio is in the right format (int16)
  118. if audio_np.dtype != np.int16:
  119. audio_np = (audio_np * 32767).astype(np.int16)
  120. # Process audio in frames
  121. frame_results = []
  122. num_frames = len(audio_np) // self.frame_size
  123. for i in range(num_frames):
  124. start_idx = i * self.frame_size
  125. end_idx = start_idx + self.frame_size
  126. frame = audio_np[start_idx:end_idx]
  127. # WebRTC VAD requires exact frame size
  128. if len(frame) == self.frame_size:
  129. try:
  130. is_speech = self.vad.is_speech(frame.tobytes(), self.sample_rate)
  131. frame_results.append(is_speech)
  132. except Exception as e:
  133. logger.warning(f"WebRTC VAD error: {e}")
  134. frame_results.append(False)
  135. else:
  136. frame_results.append(False)
  137. # Calculate overall result
  138. if not frame_results:
  139. return VADResult(False, 0.0, 0.0, len(audio_np) / self.sample_rate, frame_results)
  140. speech_frames = sum(frame_results)
  141. total_frames = len(frame_results)
  142. confidence = speech_frames / total_frames if total_frames > 0 else 0.0
  143. # Apply temporal smoothing if enabled
  144. if self.config.use_temporal_smoothing:
  145. frame_results = self._apply_temporal_smoothing(frame_results)
  146. is_speech = any(frame_results)
  147. else:
  148. is_speech = confidence > 0.3 # Threshold for overall speech detection
  149. duration = len(audio_np) / self.sample_rate
  150. return VADResult(is_speech, confidence, 0.0, duration, frame_results)
  151. def _apply_temporal_smoothing(self, frame_results: List[bool]) -> List[bool]:
  152. """Apply temporal smoothing to frame results."""
  153. if len(frame_results) < self.config.smoothing_window_size:
  154. return frame_results
  155. smoothed = []
  156. window_size = self.config.smoothing_window_size
  157. half_window = window_size // 2
  158. for i in range(len(frame_results)):
  159. start = max(0, i - half_window)
  160. end = min(len(frame_results), i + half_window + 1)
  161. window = frame_results[start:end]
  162. # Majority vote in window
  163. speech_count = sum(window)
  164. smoothed.append(speech_count > len(window) // 2)
  165. return smoothed
  166. class EnergyBasedVAD(VoiceActivityDetector):
  167. """Energy-based Voice Activity Detector."""
  168. def __init__(self, config: VADConfig):
  169. super().__init__(config)
  170. self.energy_history = []
  171. self.background_energy = None
  172. self.adaptation_rate = 0.01
  173. def detect(self, audio: Union[np.ndarray, torch.Tensor]) -> VADResult:
  174. """Detect voice activity using energy-based method."""
  175. audio_tensor = self._ensure_torch(audio)
  176. # Calculate frame-wise energy
  177. frame_energies = []
  178. frame_results = []
  179. num_frames = len(audio_tensor) // self.frame_size
  180. for i in range(num_frames):
  181. start_idx = i * self.frame_size
  182. end_idx = start_idx + self.frame_size
  183. frame = audio_tensor[start_idx:end_idx]
  184. # Calculate RMS energy
  185. energy = torch.sqrt(torch.mean(frame ** 2)).item()
  186. frame_energies.append(energy)
  187. # Update background energy estimate
  188. self._update_background_energy(energy)
  189. # Determine if frame contains speech
  190. is_speech = self._is_speech_frame(energy)
  191. frame_results.append(is_speech)
  192. # Post-process results
  193. frame_results = self._post_process_frames(frame_results)
  194. # Calculate overall result
  195. if not frame_results:
  196. duration = len(audio_tensor) / self.sample_rate
  197. return VADResult(False, 0.0, 0.0, duration, frame_results, frame_energies)
  198. speech_frames = sum(frame_results)
  199. total_frames = len(frame_results)
  200. confidence = speech_frames / total_frames if total_frames > 0 else 0.0
  201. is_speech = confidence > 0.2
  202. duration = len(audio_tensor) / self.sample_rate
  203. return VADResult(is_speech, confidence, 0.0, duration, frame_results, frame_energies)
  204. def _update_background_energy(self, energy: float):
  205. """Update background energy estimate using adaptive method."""
  206. if self.background_energy is None:
  207. self.background_energy = energy
  208. else:
  209. # Only update if energy is low (likely background)
  210. if energy < self.background_energy * 2:
  211. self.background_energy = (1 - self.adaptation_rate) * self.background_energy + \
  212. self.adaptation_rate * energy
  213. def _is_speech_frame(self, energy: float) -> bool:
  214. """Determine if a frame contains speech based on energy."""
  215. if self.background_energy is None:
  216. return energy > self.config.energy_threshold
  217. # Adaptive threshold based on background energy
  218. adaptive_threshold = max(
  219. self.config.energy_threshold,
  220. self.background_energy * 3 # 3x background energy
  221. )
  222. return energy > adaptive_threshold
  223. def _post_process_frames(self, frame_results: List[bool]) -> List[bool]:
  224. """Post-process frame results to remove short segments and add hangover."""
  225. if not frame_results:
  226. return frame_results
  227. # Convert to frame indices
  228. min_length_frames = int(self.config.energy_min_length_ms / self.config.frame_duration_ms)
  229. hangover_frames = int(self.config.energy_hangover_ms / self.config.frame_duration_ms)
  230. # Remove short speech segments
  231. processed = self._remove_short_segments(frame_results, min_length_frames)
  232. # Add hangover
  233. processed = self._add_hangover(processed, hangover_frames)
  234. return processed
  235. def _remove_short_segments(self, results: List[bool], min_length: int) -> List[bool]:
  236. """Remove speech segments shorter than minimum length."""
  237. if min_length <= 1:
  238. return results
  239. processed = results.copy()
  240. i = 0
  241. while i < len(processed):
  242. if processed[i]: # Start of speech segment
  243. segment_start = i
  244. while i < len(processed) and processed[i]:
  245. i += 1
  246. segment_length = i - segment_start
  247. # Remove short segments
  248. if segment_length < min_length:
  249. for j in range(segment_start, i):
  250. processed[j] = False
  251. else:
  252. i += 1
  253. return processed
  254. def _add_hangover(self, results: List[bool], hangover_frames: int) -> List[bool]:
  255. """Add hangover time after speech segments."""
  256. if hangover_frames <= 0:
  257. return results
  258. processed = results.copy()
  259. for i in range(len(results) - 1):
  260. if results[i] and not results[i + 1]: # End of speech segment
  261. # Add hangover
  262. end_hangover = min(len(processed), i + 1 + hangover_frames)
  263. for j in range(i + 1, end_hangover):
  264. processed[j] = True
  265. return processed
  266. class SpectralVAD(VoiceActivityDetector):
  267. """Spectral-based Voice Activity Detector."""
  268. def __init__(self, config: VADConfig):
  269. super().__init__(config)
  270. self.n_fft = 512
  271. self.hop_length = self.frame_size
  272. def detect(self, audio: Union[np.ndarray, torch.Tensor]) -> VADResult:
  273. """Detect voice activity using spectral features."""
  274. audio_tensor = self._ensure_torch(audio)
  275. # Compute STFT
  276. stft = torch.stft(
  277. audio_tensor,
  278. n_fft=self.n_fft,
  279. hop_length=self.hop_length,
  280. return_complex=True
  281. )
  282. magnitude = torch.abs(stft)
  283. power = magnitude ** 2
  284. # Calculate spectral features frame by frame
  285. frame_results = []
  286. num_frames = power.size(-1)
  287. for i in range(num_frames):
  288. frame_power = power[:, i]
  289. # Spectral rolloff
  290. rolloff = self._spectral_rolloff(frame_power)
  291. # Spectral centroid
  292. centroid = self._spectral_centroid(frame_power)
  293. # Zero crossing rate (approximate from phase)
  294. zcr = self._zero_crossing_rate(audio_tensor, i)
  295. # Combine features for decision
  296. is_speech = self._classify_frame(rolloff, centroid, zcr)
  297. frame_results.append(is_speech)
  298. # Calculate overall result
  299. if not frame_results:
  300. duration = len(audio_tensor) / self.sample_rate
  301. return VADResult(False, 0.0, 0.0, duration, frame_results)
  302. speech_frames = sum(frame_results)
  303. total_frames = len(frame_results)
  304. confidence = speech_frames / total_frames if total_frames > 0 else 0.0
  305. is_speech = confidence > 0.3
  306. duration = len(audio_tensor) / self.sample_rate
  307. return VADResult(is_speech, confidence, 0.0, duration, frame_results)
  308. def _spectral_rolloff(self, power_spectrum: torch.Tensor, rolloff_percent: float = 0.85) -> float:
  309. """Calculate spectral rolloff frequency."""
  310. total_energy = torch.sum(power_spectrum)
  311. if total_energy == 0:
  312. return 0.0
  313. cumulative_energy = torch.cumsum(power_spectrum, dim=0)
  314. rolloff_threshold = rolloff_percent * total_energy
  315. rolloff_bin = torch.where(cumulative_energy >= rolloff_threshold)[0]
  316. if len(rolloff_bin) > 0:
  317. return float(rolloff_bin[0]) / len(power_spectrum)
  318. return 1.0
  319. def _spectral_centroid(self, power_spectrum: torch.Tensor) -> float:
  320. """Calculate spectral centroid."""
  321. if torch.sum(power_spectrum) == 0:
  322. return 0.0
  323. frequencies = torch.arange(len(power_spectrum), dtype=torch.float32)
  324. centroid = torch.sum(frequencies * power_spectrum) / torch.sum(power_spectrum)
  325. # Convert to Hz
  326. return float(centroid * self.sample_rate / self.n_fft)
  327. def _zero_crossing_rate(self, audio: torch.Tensor, frame_idx: int) -> float:
  328. """Calculate zero crossing rate for a frame."""
  329. start_idx = frame_idx * self.hop_length
  330. end_idx = min(start_idx + self.frame_size, len(audio))
  331. if end_idx <= start_idx:
  332. return 0.0
  333. frame = audio[start_idx:end_idx]
  334. zero_crossings = torch.sum(torch.diff(torch.sign(frame)) != 0)
  335. return float(zero_crossings) / len(frame)
  336. def _classify_frame(self, rolloff: float, centroid: float, zcr: float) -> bool:
  337. """Classify frame as speech or non-speech based on spectral features."""
  338. # Speech typically has:
  339. # - Higher spectral rolloff (more high-frequency content)
  340. # - Moderate spectral centroid
  341. # - Moderate zero crossing rate
  342. rolloff_score = rolloff > self.config.spectral_rolloff_threshold
  343. centroid_score = centroid > self.config.spectral_centroid_threshold
  344. zcr_score = 0.01 < zcr < 0.5 # Typical range for speech
  345. # Simple voting
  346. votes = sum([rolloff_score, centroid_score, zcr_score])
  347. return votes >= 2
  348. class HybridVAD(VoiceActivityDetector):
  349. """Hybrid Voice Activity Detector combining multiple methods."""
  350. def __init__(self, config: VADConfig):
  351. super().__init__(config)
  352. # Initialize component VADs
  353. self.vads = {}
  354. if WEBRTC_AVAILABLE:
  355. try:
  356. self.vads['webrtc'] = WebRTCVAD(config)
  357. except Exception as e:
  358. logger.warning(f"Failed to initialize WebRTC VAD: {e}")
  359. self.vads['energy'] = EnergyBasedVAD(config)
  360. self.vads['spectral'] = SpectralVAD(config)
  361. def detect(self, audio: Union[np.ndarray, torch.Tensor]) -> VADResult:
  362. """Detect voice activity using hybrid approach."""
  363. # Get results from all available VADs
  364. results = {}
  365. for name, vad in self.vads.items():
  366. try:
  367. results[name] = vad.detect(audio)
  368. except Exception as e:
  369. logger.warning(f"VAD {name} failed: {e}")
  370. continue
  371. if not results:
  372. duration = len(audio) / self.sample_rate
  373. return VADResult(False, 0.0, 0.0, duration)
  374. # Combine results
  375. return self._combine_results(results, len(audio) / self.sample_rate)
  376. def _combine_results(self, results: Dict[str, VADResult], duration: float) -> VADResult:
  377. """Combine results from multiple VADs."""
  378. if self.config.combination_mode == "majority":
  379. return self._majority_vote(results, duration)
  380. elif self.config.combination_mode == "weighted":
  381. return self._weighted_combination(results, duration)
  382. elif self.config.combination_mode == "conservative":
  383. return self._conservative_combination(results, duration)
  384. elif self.config.combination_mode == "aggressive":
  385. return self._aggressive_combination(results, duration)
  386. else:
  387. # Default to weighted
  388. return self._weighted_combination(results, duration)
  389. def _majority_vote(self, results: Dict[str, VADResult], duration: float) -> VADResult:
  390. """Combine using majority vote."""
  391. speech_votes = sum(1 for result in results.values() if result.is_speech)
  392. total_votes = len(results)
  393. is_speech = speech_votes > total_votes // 2
  394. confidence = sum(result.confidence for result in results.values()) / total_votes
  395. return VADResult(is_speech, confidence, 0.0, duration)
  396. def _weighted_combination(self, results: Dict[str, VADResult], duration: float) -> VADResult:
  397. """Combine using weighted average."""
  398. weights = {
  399. 'webrtc': self.config.webrtc_weight,
  400. 'energy': self.config.energy_weight,
  401. 'spectral': self.config.spectral_weight
  402. }
  403. total_weight = 0.0
  404. weighted_confidence = 0.0
  405. weighted_speech = 0.0
  406. for name, result in results.items():
  407. weight = weights.get(name, 1.0 / len(results))
  408. total_weight += weight
  409. weighted_confidence += weight * result.confidence
  410. weighted_speech += weight * (1.0 if result.is_speech else 0.0)
  411. if total_weight > 0:
  412. final_confidence = weighted_confidence / total_weight
  413. final_speech = weighted_speech / total_weight
  414. else:
  415. final_confidence = 0.0
  416. final_speech = 0.0
  417. is_speech = final_speech > 0.5
  418. return VADResult(is_speech, final_confidence, 0.0, duration)
  419. def _conservative_combination(self, results: Dict[str, VADResult], duration: float) -> VADResult:
  420. """Conservative combination - all VADs must agree for speech."""
  421. is_speech = all(result.is_speech for result in results.values())
  422. confidence = min(result.confidence for result in results.values()) if results else 0.0
  423. return VADResult(is_speech, confidence, 0.0, duration)
  424. def _aggressive_combination(self, results: Dict[str, VADResult], duration: float) -> VADResult:
  425. """Aggressive combination - any VAD detecting speech triggers speech."""
  426. is_speech = any(result.is_speech for result in results.values())
  427. confidence = max(result.confidence for result in results.values()) if results else 0.0
  428. return VADResult(is_speech, confidence, 0.0, duration)
  429. def create_vad_detector(vad_type: str, config: Optional[VADConfig] = None) -> VoiceActivityDetector:
  430. """
  431. Factory function to create VAD detector.
  432. Args:
  433. vad_type: Type of VAD ("webrtc", "energy", "spectral", "hybrid")
  434. config: VAD configuration
  435. Returns:
  436. Configured VAD detector
  437. """
  438. if config is None:
  439. config = VADConfig()
  440. if vad_type.lower() == "webrtc":
  441. return WebRTCVAD(config)
  442. elif vad_type.lower() == "energy":
  443. return EnergyBasedVAD(config)
  444. elif vad_type.lower() == "spectral":
  445. return SpectralVAD(config)
  446. elif vad_type.lower() == "hybrid":
  447. return HybridVAD(config)
  448. else:
  449. raise ValueError(f"Unsupported VAD type: {vad_type}")
  450. def combine_vad_results(results: List[VADResult], method: str = "weighted") -> VADResult:
  451. """
  452. Combine multiple VAD results.
  453. Args:
  454. results: List of VAD results to combine
  455. method: Combination method ("majority", "weighted", "conservative", "aggressive")
  456. Returns:
  457. Combined VAD result
  458. """
  459. if not results:
  460. return VADResult(False, 0.0, 0.0, 0.0)
  461. if len(results) == 1:
  462. return results[0]
  463. # Calculate combined metrics
  464. confidences = [r.confidence for r in results]
  465. speech_flags = [r.is_speech for r in results]
  466. if method == "majority":
  467. is_speech = sum(speech_flags) > len(speech_flags) // 2
  468. confidence = sum(confidences) / len(confidences)
  469. elif method == "weighted":
  470. # Weight by confidence
  471. weights = [r.confidence for r in results]
  472. total_weight = sum(weights) if sum(weights) > 0 else len(weights)
  473. weighted_speech = sum(w * s for w, s in zip(weights, speech_flags)) / total_weight
  474. is_speech = weighted_speech > 0.5
  475. confidence = sum(confidences) / len(confidences)
  476. elif method == "conservative":
  477. is_speech = all(speech_flags)
  478. confidence = min(confidences)
  479. elif method == "aggressive":
  480. is_speech = any(speech_flags)
  481. confidence = max(confidences)
  482. else:
  483. raise ValueError(f"Unsupported combination method: {method}")
  484. # Use timing from first result
  485. start_time = results[0].start_time
  486. end_time = results[0].end_time
  487. return VADResult(is_speech, confidence, start_time, end_time)
  488. # Example usage and testing
  489. def test_vad_systems():
  490. """Test different VAD systems with sample audio."""
  491. # Generate test audio (sine wave + noise)
  492. sample_rate = 16000
  493. duration = 2.0
  494. t = np.linspace(0, duration, int(sample_rate * duration))
  495. # Create audio with speech-like characteristics
  496. audio = np.sin(2 * np.pi * 200 * t) * np.sin(2 * np.pi * 3 * t) # AM modulation
  497. audio += 0.1 * np.random.randn(len(audio)) # Add noise
  498. # Test different VAD systems
  499. config = VADConfig()
  500. # Test energy-based VAD
  501. energy_vad = create_vad_detector("energy", config)
  502. energy_result = energy_vad.detect(audio)
  503. print(f"Energy VAD: speech={energy_result.is_speech}, confidence={energy_result.confidence:.3f}")
  504. # Test spectral VAD
  505. spectral_vad = create_vad_detector("spectral", config)
  506. spectral_result = spectral_vad.detect(audio)
  507. print(f"Spectral VAD: speech={spectral_result.is_speech}, confidence={spectral_result.confidence:.3f}")
  508. # Test WebRTC VAD if available
  509. if WEBRTC_AVAILABLE:
  510. try:
  511. webrtc_vad = create_vad_detector("webrtc", config)
  512. webrtc_result = webrtc_vad.detect(audio)
  513. print(f"WebRTC VAD: speech={webrtc_result.is_speech}, confidence={webrtc_result.confidence:.3f}")
  514. except Exception as e:
  515. print(f"WebRTC VAD failed: {e}")
  516. # Test hybrid VAD
  517. hybrid_vad = create_vad_detector("hybrid", config)
  518. hybrid_result = hybrid_vad.detect(audio)
  519. print(f"Hybrid VAD: speech={hybrid_result.is_speech}, confidence={hybrid_result.confidence:.3f}")
  520. if __name__ == "__main__":
  521. test_vad_systems()