examples.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. """
  2. Example usage and testing utilities for the wakeword detection system.
  3. This module provides comprehensive examples, testing utilities, and
  4. demonstration code for the Trixy wakeword detection system.
  5. """
  6. import numpy as np
  7. import torch
  8. import time
  9. import json
  10. from pathlib import Path
  11. from typing import Optional, List, Dict, Any
  12. import logging
  13. import threading
  14. from .config import (
  15. WakewordConfig, create_default_config, create_lightweight_config,
  16. create_high_performance_config
  17. )
  18. from .detector import WakewordDetector, create_wakeword_detector
  19. from .detection_engine import DetectionResult, WakewordType
  20. from .audio_buffer import AudioChunk
  21. from .optimization import PerformanceMonitor, ModelOptimizer
  22. class MockEventHandler:
  23. """Mock event handler for testing purposes."""
  24. def __init__(self):
  25. self.triggered_events = []
  26. self.event_callbacks = {}
  27. def trigger(self, event_name: str, event_data: Any):
  28. """Trigger an event."""
  29. self.triggered_events.append({
  30. 'event_name': event_name,
  31. 'event_data': event_data,
  32. 'timestamp': time.time()
  33. })
  34. # Call callbacks if registered
  35. if event_name in self.event_callbacks:
  36. for callback in self.event_callbacks[event_name]:
  37. try:
  38. callback(event_name, event_data)
  39. except Exception as e:
  40. print(f"Error in event callback: {e}")
  41. def register_callback(self, event_name: str, callback):
  42. """Register event callback."""
  43. if event_name not in self.event_callbacks:
  44. self.event_callbacks[event_name] = []
  45. self.event_callbacks[event_name].append(callback)
  46. def get_triggered_events(self, event_name: Optional[str] = None) -> List[Dict]:
  47. """Get triggered events."""
  48. if event_name:
  49. return [e for e in self.triggered_events if e['event_name'] == event_name]
  50. return self.triggered_events
  51. def clear_events(self):
  52. """Clear event history."""
  53. self.triggered_events.clear()
  54. class AudioSimulator:
  55. """Simulate audio input for testing wakeword detection."""
  56. def __init__(self, sample_rate: int = 16000):
  57. self.sample_rate = sample_rate
  58. def generate_silence(self, duration_seconds: float) -> np.ndarray:
  59. """Generate silence audio."""
  60. samples = int(duration_seconds * self.sample_rate)
  61. return np.zeros(samples, dtype=np.float32)
  62. def generate_white_noise(self, duration_seconds: float, amplitude: float = 0.1) -> np.ndarray:
  63. """Generate white noise audio."""
  64. samples = int(duration_seconds * self.sample_rate)
  65. return np.random.normal(0, amplitude, samples).astype(np.float32)
  66. def generate_sine_wave(self, duration_seconds: float, frequency: float = 440.0,
  67. amplitude: float = 0.5) -> np.ndarray:
  68. """Generate sine wave audio."""
  69. samples = int(duration_seconds * self.sample_rate)
  70. t = np.linspace(0, duration_seconds, samples, False)
  71. return (amplitude * np.sin(2 * np.pi * frequency * t)).astype(np.float32)
  72. def generate_synthetic_wakeword(self, duration_seconds: float = 1.0) -> np.ndarray:
  73. """Generate synthetic wakeword-like audio."""
  74. # Combine multiple frequency components to simulate speech
  75. samples = int(duration_seconds * self.sample_rate)
  76. t = np.linspace(0, duration_seconds, samples, False)
  77. # Fundamental frequency (voice pitch)
  78. f0 = 150 # Hz
  79. audio = 0.3 * np.sin(2 * np.pi * f0 * t)
  80. # Add harmonics
  81. for harmonic in [2, 3, 4, 5]:
  82. amplitude = 0.1 / harmonic
  83. audio += amplitude * np.sin(2 * np.pi * f0 * harmonic * t)
  84. # Add formants (speech resonances)
  85. formants = [800, 1200, 2600] # Hz
  86. for formant in formants:
  87. amplitude = 0.05 * np.exp(-0.5 * ((t - duration_seconds/2) / (duration_seconds/6))**2)
  88. audio += amplitude * np.sin(2 * np.pi * formant * t)
  89. # Apply envelope (attack, sustain, decay)
  90. envelope = np.ones_like(t)
  91. attack_samples = int(0.1 * samples)
  92. decay_samples = int(0.2 * samples)
  93. # Attack
  94. envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
  95. # Decay
  96. envelope[-decay_samples:] = np.linspace(1, 0, decay_samples)
  97. audio *= envelope
  98. # Add slight noise
  99. audio += 0.02 * np.random.normal(0, 1, samples)
  100. return audio.astype(np.float32)
  101. def load_test_audio(self, file_path: str) -> Optional[np.ndarray]:
  102. """Load test audio file."""
  103. try:
  104. import librosa
  105. audio, sr = librosa.load(file_path, sr=self.sample_rate, mono=True)
  106. return audio.astype(np.float32)
  107. except ImportError:
  108. print("librosa not available, cannot load audio file")
  109. return None
  110. except Exception as e:
  111. print(f"Error loading audio file: {e}")
  112. return None
  113. def create_test_sequence(self) -> List[np.ndarray]:
  114. """Create a sequence of test audio chunks."""
  115. return [
  116. self.generate_silence(0.5),
  117. self.generate_white_noise(0.3, 0.05),
  118. self.generate_synthetic_wakeword(1.0),
  119. self.generate_silence(0.5),
  120. self.generate_sine_wave(0.2, 1000, 0.1),
  121. self.generate_synthetic_wakeword(1.2),
  122. self.generate_silence(1.0)
  123. ]
  124. class WakewordTester:
  125. """Comprehensive testing utility for wakeword detection."""
  126. def __init__(self, detector: WakewordDetector, audio_simulator: AudioSimulator):
  127. self.detector = detector
  128. self.audio_simulator = audio_simulator
  129. self.test_results = []
  130. def run_basic_test(self) -> Dict[str, Any]:
  131. """Run basic functionality test."""
  132. print("Running basic wakeword detection test...")
  133. # Start detector
  134. self.detector.start()
  135. # Generate test audio
  136. test_audio = self.audio_simulator.generate_synthetic_wakeword(1.0)
  137. # Process audio
  138. start_time = time.time()
  139. results = self.detector.process_audio(test_audio)
  140. processing_time = time.time() - start_time
  141. # Stop detector
  142. self.detector.stop()
  143. test_result = {
  144. 'test_name': 'basic_test',
  145. 'audio_duration': len(test_audio) / self.audio_simulator.sample_rate,
  146. 'processing_time': processing_time,
  147. 'results_count': len(results),
  148. 'success': True
  149. }
  150. self.test_results.append(test_result)
  151. print(f"Basic test completed: {len(results)} results in {processing_time:.3f}s")
  152. return test_result
  153. def run_performance_test(self, duration_seconds: float = 10.0) -> Dict[str, Any]:
  154. """Run performance stress test."""
  155. print(f"Running performance test for {duration_seconds} seconds...")
  156. # Set up performance monitoring
  157. perf_monitor = PerformanceMonitor()
  158. perf_monitor.start_monitoring()
  159. # Start detector
  160. self.detector.start()
  161. start_time = time.time()
  162. total_chunks = 0
  163. total_detections = 0
  164. try:
  165. while time.time() - start_time < duration_seconds:
  166. # Generate random audio chunk
  167. chunk_type = np.random.choice(['silence', 'noise', 'wakeword'], p=[0.6, 0.3, 0.1])
  168. if chunk_type == 'silence':
  169. audio = self.audio_simulator.generate_silence(0.5)
  170. elif chunk_type == 'noise':
  171. audio = self.audio_simulator.generate_white_noise(0.5, 0.1)
  172. else: # wakeword
  173. audio = self.audio_simulator.generate_synthetic_wakeword(1.0)
  174. # Process audio
  175. results = self.detector.process_audio(audio)
  176. total_chunks += 1
  177. # Count detections
  178. for result in results:
  179. if hasattr(result, 'is_wakeword_detected') and result.is_wakeword_detected:
  180. total_detections += 1
  181. # Small delay to avoid overwhelming the system
  182. time.sleep(0.01)
  183. except KeyboardInterrupt:
  184. print("Performance test interrupted")
  185. # Stop systems
  186. self.detector.stop()
  187. perf_monitor.stop_monitoring()
  188. # Get performance stats
  189. detector_stats = self.detector.get_performance_stats()
  190. perf_summary = perf_monitor.get_performance_summary()
  191. test_result = {
  192. 'test_name': 'performance_test',
  193. 'duration': time.time() - start_time,
  194. 'total_chunks': total_chunks,
  195. 'total_detections': total_detections,
  196. 'chunks_per_second': total_chunks / (time.time() - start_time),
  197. 'detector_stats': detector_stats,
  198. 'performance_summary': perf_summary
  199. }
  200. self.test_results.append(test_result)
  201. print(f"Performance test completed: {total_chunks} chunks, {total_detections} detections")
  202. return test_result
  203. def run_accuracy_test(self, test_cases: List[Dict[str, Any]]) -> Dict[str, Any]:
  204. """Run accuracy test with labeled test cases."""
  205. print(f"Running accuracy test with {len(test_cases)} test cases...")
  206. self.detector.start()
  207. correct_predictions = 0
  208. total_predictions = 0
  209. confusion_matrix = {
  210. 'true_positives': 0,
  211. 'false_positives': 0,
  212. 'true_negatives': 0,
  213. 'false_negatives': 0
  214. }
  215. for i, test_case in enumerate(test_cases):
  216. audio_type = test_case['type'] # 'wakeword', 'negative'
  217. expected_detection = test_case['should_detect']
  218. # Generate or load audio
  219. if 'audio_data' in test_case:
  220. audio = test_case['audio_data']
  221. elif audio_type == 'wakeword':
  222. audio = self.audio_simulator.generate_synthetic_wakeword()
  223. else:
  224. audio = self.audio_simulator.generate_white_noise(1.0, 0.1)
  225. # Process audio
  226. results = self.detector.process_audio(audio)
  227. # Check prediction
  228. detected = any(hasattr(r, 'is_wakeword_detected') and r.is_wakeword_detected for r in results)
  229. if detected == expected_detection:
  230. correct_predictions += 1
  231. # Update confusion matrix
  232. if expected_detection and detected:
  233. confusion_matrix['true_positives'] += 1
  234. elif expected_detection and not detected:
  235. confusion_matrix['false_negatives'] += 1
  236. elif not expected_detection and detected:
  237. confusion_matrix['false_positives'] += 1
  238. else:
  239. confusion_matrix['true_negatives'] += 1
  240. total_predictions += 1
  241. if i % 10 == 0:
  242. print(f"Processed {i+1}/{len(test_cases)} test cases")
  243. self.detector.stop()
  244. # Calculate metrics
  245. accuracy = correct_predictions / total_predictions if total_predictions > 0 else 0
  246. tp = confusion_matrix['true_positives']
  247. fp = confusion_matrix['false_positives']
  248. fn = confusion_matrix['false_negatives']
  249. precision = tp / (tp + fp) if (tp + fp) > 0 else 0
  250. recall = tp / (tp + fn) if (tp + fn) > 0 else 0
  251. f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
  252. test_result = {
  253. 'test_name': 'accuracy_test',
  254. 'total_cases': total_predictions,
  255. 'correct_predictions': correct_predictions,
  256. 'accuracy': accuracy,
  257. 'precision': precision,
  258. 'recall': recall,
  259. 'f1_score': f1_score,
  260. 'confusion_matrix': confusion_matrix
  261. }
  262. self.test_results.append(test_result)
  263. print(f"Accuracy test completed: {accuracy:.3f} accuracy, {f1_score:.3f} F1-score")
  264. return test_result
  265. def run_latency_test(self, num_iterations: int = 100) -> Dict[str, Any]:
  266. """Run latency measurement test."""
  267. print(f"Running latency test with {num_iterations} iterations...")
  268. self.detector.start()
  269. latencies = []
  270. for i in range(num_iterations):
  271. # Generate test audio
  272. audio = self.audio_simulator.generate_synthetic_wakeword(1.0)
  273. # Measure processing latency
  274. start_time = time.time()
  275. results = self.detector.process_audio(audio)
  276. end_time = time.time()
  277. latency_ms = (end_time - start_time) * 1000
  278. latencies.append(latency_ms)
  279. if i % 20 == 0:
  280. print(f"Completed {i+1}/{num_iterations} latency measurements")
  281. self.detector.stop()
  282. test_result = {
  283. 'test_name': 'latency_test',
  284. 'iterations': num_iterations,
  285. 'mean_latency_ms': np.mean(latencies),
  286. 'std_latency_ms': np.std(latencies),
  287. 'min_latency_ms': min(latencies),
  288. 'max_latency_ms': max(latencies),
  289. 'p95_latency_ms': np.percentile(latencies, 95),
  290. 'p99_latency_ms': np.percentile(latencies, 99)
  291. }
  292. self.test_results.append(test_result)
  293. print(f"Latency test completed: {test_result['mean_latency_ms']:.1f}ms average")
  294. return test_result
  295. def get_test_report(self) -> Dict[str, Any]:
  296. """Get comprehensive test report."""
  297. return {
  298. 'test_results': self.test_results,
  299. 'summary': {
  300. 'total_tests': len(self.test_results),
  301. 'successful_tests': sum(1 for t in self.test_results if t.get('success', True)),
  302. 'test_names': [t['test_name'] for t in self.test_results]
  303. }
  304. }
  305. def save_test_report(self, file_path: str):
  306. """Save test report to file."""
  307. report = self.get_test_report()
  308. with open(file_path, 'w') as f:
  309. json.dump(report, f, indent=2, default=str)
  310. print(f"Test report saved to: {file_path}")
  311. def example_basic_usage():
  312. """Basic usage example of the wakeword detection system."""
  313. print("=== Basic Usage Example ===")
  314. # Create configuration
  315. config = create_default_config()
  316. # Note: In real usage, you would set these to actual values
  317. config.model_config.model_path = "path/to/your/model.pth"
  318. config.model_config.model_password = "your_password"
  319. config.satellite_id = "example_satellite"
  320. # Create mock event handler for this example
  321. event_handler = MockEventHandler()
  322. # Register event callback
  323. def on_wakeword_detected(event_name, event_data):
  324. print(f"Wakeword detected: {event_data.wakeword_type} "
  325. f"(confidence: {event_data.confidence:.3f})")
  326. event_handler.register_callback("wakeword_received", on_wakeword_detected)
  327. try:
  328. # Create detector (this would fail without a real model file)
  329. # detector = WakewordDetector(config, event_handler)
  330. print("Note: This example requires a real model file to run")
  331. print("Configuration created successfully:")
  332. print(f"- Model path: {config.model_config.model_path}")
  333. print(f"- Device: {config.model_config.device}")
  334. print(f"- Sample rate: {config.audio_config.sample_rate} Hz")
  335. print(f"- Custom threshold: {config.detection_config.custom_threshold}")
  336. print(f"- System command threshold: {config.detection_config.system_command_threshold}")
  337. except Exception as e:
  338. print(f"Cannot create detector without valid model: {e}")
  339. def example_performance_optimization():
  340. """Example of performance optimization techniques."""
  341. print("=== Performance Optimization Example ===")
  342. # Create lightweight configuration for resource-constrained devices
  343. lightweight_config = create_lightweight_config()
  344. print("Lightweight configuration:")
  345. print(f"- Half precision: {lightweight_config.model_config.use_half_precision}")
  346. print(f"- Memory limit: {lightweight_config.performance_config.max_memory_usage_mb} MB")
  347. print(f"- Worker threads: {lightweight_config.performance_config.max_worker_threads}")
  348. # Create high-performance configuration
  349. high_perf_config = create_high_performance_config()
  350. print("\nHigh-performance configuration:")
  351. print(f"- Model compilation: {high_perf_config.model_config.enable_model_compilation}")
  352. print(f"- Batch inference: {high_perf_config.detection_config.batch_inference}")
  353. print(f"- Memory limit: {high_perf_config.performance_config.max_memory_usage_mb} MB")
  354. # Example of auto-tuning
  355. from .config import auto_tune_config
  356. tuned_config = auto_tune_config(
  357. lightweight_config,
  358. target_latency_ms=50.0,
  359. available_memory_mb=256.0
  360. )
  361. print(f"\nAuto-tuned configuration for 50ms latency and 256MB memory:")
  362. print(f"- Chunk duration: {tuned_config.audio_config.chunk_duration}s")
  363. print(f"- Temporal smoothing: {tuned_config.detection_config.use_temporal_smoothing}")
  364. def example_testing_framework():
  365. """Example of using the testing framework."""
  366. print("=== Testing Framework Example ===")
  367. # Create audio simulator
  368. audio_sim = AudioSimulator(sample_rate=16000)
  369. # Generate test audio
  370. print("Generating test audio samples...")
  371. silence = audio_sim.generate_silence(1.0)
  372. noise = audio_sim.generate_white_noise(1.0, 0.1)
  373. synthetic_wakeword = audio_sim.generate_synthetic_wakeword(1.0)
  374. print(f"Generated silence: {len(silence)} samples")
  375. print(f"Generated noise: {len(noise)} samples")
  376. print(f"Generated synthetic wakeword: {len(synthetic_wakeword)} samples")
  377. # Create test sequence
  378. test_sequence = audio_sim.create_test_sequence()
  379. total_duration = sum(len(chunk) for chunk in test_sequence) / audio_sim.sample_rate
  380. print(f"Created test sequence: {len(test_sequence)} chunks, {total_duration:.1f}s total")
  381. # Mock event handler
  382. event_handler = MockEventHandler()
  383. # Note: Would create detector with real model for actual testing
  384. print("\nNote: Testing requires a real model file to run")
  385. print("Test cases would include:")
  386. print("- Basic functionality test")
  387. print("- Performance stress test")
  388. print("- Accuracy evaluation")
  389. print("- Latency measurement")
  390. def example_streaming_audio():
  391. """Example of streaming audio processing."""
  392. print("=== Streaming Audio Example ===")
  393. def simulate_audio_stream(detector, duration_seconds=5.0):
  394. """Simulate real-time audio streaming."""
  395. audio_sim = AudioSimulator()
  396. chunk_duration = 0.1 # 100ms chunks
  397. sample_rate = detector.config.audio_config.sample_rate
  398. chunk_samples = int(chunk_duration * sample_rate)
  399. print(f"Simulating {duration_seconds}s of audio streaming...")
  400. start_time = time.time()
  401. chunk_count = 0
  402. while time.time() - start_time < duration_seconds:
  403. # Generate audio chunk (mix of silence and occasional synthetic speech)
  404. if np.random.random() < 0.1: # 10% chance of "wakeword"
  405. audio_chunk = audio_sim.generate_synthetic_wakeword(chunk_duration)
  406. elif np.random.random() < 0.3: # 30% chance of noise
  407. audio_chunk = audio_sim.generate_white_noise(chunk_duration, 0.05)
  408. else: # 60% silence
  409. audio_chunk = audio_sim.generate_silence(chunk_duration)
  410. # Process chunk
  411. results = detector.process_audio(audio_chunk)
  412. chunk_count += 1
  413. # Print detection results
  414. for result in results:
  415. if hasattr(result, 'is_wakeword_detected') and result.is_wakeword_detected:
  416. print(f"Detection: {result.wakeword_type.value} "
  417. f"(confidence: {result.confidence:.3f})")
  418. # Simulate real-time processing
  419. time.sleep(chunk_duration * 0.8) # Slightly faster than real-time
  420. print(f"Processed {chunk_count} audio chunks")
  421. print("Note: Streaming example requires a real detector instance")
  422. print("Would simulate real-time audio processing with periodic detections")
  423. def run_all_examples():
  424. """Run all examples."""
  425. print("Running all Trixy Wakeword Detection examples...\n")
  426. try:
  427. example_basic_usage()
  428. print("\n" + "="*60 + "\n")
  429. example_performance_optimization()
  430. print("\n" + "="*60 + "\n")
  431. example_testing_framework()
  432. print("\n" + "="*60 + "\n")
  433. example_streaming_audio()
  434. print("\n" + "="*60 + "\n")
  435. print("All examples completed successfully!")
  436. except Exception as e:
  437. print(f"Error running examples: {e}")
  438. import traceback
  439. traceback.print_exc()
  440. if __name__ == "__main__":
  441. # Set up logging
  442. logging.basicConfig(
  443. level=logging.INFO,
  444. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  445. )
  446. run_all_examples()