ml_manager.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  1. """
  2. ML Manager for Client-side ML Integration in Trixy Voice Assistant.
  3. This module coordinates wakeword detection and voice recognition systems,
  4. providing a unified interface for client-side AI processing. It manages
  5. the complete workflow from audio input to event generation.
  6. Key Features:
  7. - Unified coordination of wakeword and voice recognition
  8. - Real-time audio pipeline management
  9. - Event system integration
  10. - Performance monitoring and optimization
  11. - Error handling and fallback mechanisms
  12. - Configuration management for ML models
  13. - Network protocol integration
  14. - Application container support
  15. Usage:
  16. from trixy_core.ml import MLManager, MLConfig
  17. # Create configuration
  18. config = MLConfig(
  19. wakeword_model_path="/path/to/wakeword.pth",
  20. voice_recognition_model_path="/path/to/voice.pth",
  21. audio_sample_rate=16000
  22. )
  23. # Initialize ML manager
  24. ml_manager = MLManager(config, event_handler, app_container)
  25. # Start ML processing
  26. await ml_manager.start()
  27. # Process audio chunk
  28. await ml_manager.process_audio_chunk(audio_data)
  29. """
  30. import asyncio
  31. import logging
  32. import time
  33. import threading
  34. from typing import Optional, Dict, Any, List, Callable, Union
  35. from dataclasses import dataclass
  36. from enum import Enum
  37. import numpy as np
  38. from .wakeword import (
  39. WakewordDetector, WakewordConfig, create_wakeword_detector,
  40. WakewordDetectorError, DetectionResult, WakewordType
  41. )
  42. from .wakeword.config import (
  43. ModelConfig as WakewordModelConfig,
  44. AudioConfig as WakewordAudioConfig,
  45. PerformanceConfig as WakewordPerformanceConfig
  46. )
  47. from .wakeword.detection_engine import DetectionConfig as WakewordDetectionConfig
  48. from .wakeword.audio_features import SpectrogramConfig
  49. from .performance_monitor import PerformanceMonitor, MetricType
  50. from .voice_recognition import (
  51. VoiceRecognitionDetector, VoiceRecognitionConfig, create_voice_recognition_detector,
  52. VoiceRecognitionDetectorError, IdentificationResult
  53. )
  54. from .voice_recognition.config import (
  55. ModelConfig as VoiceModelConfig,
  56. AudioConfig as VoiceAudioConfig,
  57. RecognitionConfig,
  58. PerformanceConfig as VoicePerformanceConfig
  59. )
  60. class MLProcessingState(Enum):
  61. """ML processing state enumeration."""
  62. IDLE = "idle"
  63. LISTENING = "listening"
  64. WAKEWORD_DETECTED = "wakeword_detected"
  65. RECORDING = "recording"
  66. PROCESSING = "processing"
  67. ERROR = "error"
  68. class MLSystemMode(Enum):
  69. """ML system operating mode."""
  70. CLIENT = "client"
  71. STANDALONE = "standalone"
  72. DEBUG = "debug"
  73. @dataclass
  74. class MLConfig:
  75. """Configuration for ML Manager."""
  76. # Wakeword configuration
  77. wakeword_model_path: str = ""
  78. wakeword_model_password: str = ""
  79. wakeword_confidence_threshold: float = 0.7
  80. # Voice recognition configuration
  81. voice_recognition_model_path: str = ""
  82. voice_recognition_model_password: str = ""
  83. voice_recognition_confidence_threshold: float = 0.8
  84. # Audio configuration
  85. audio_sample_rate: int = 16000
  86. audio_chunk_size: int = 1024
  87. audio_channels: int = 1
  88. audio_format: str = "int16"
  89. # Processing configuration
  90. buffer_duration_seconds: float = 10.0
  91. silence_timeout_seconds: float = 3.0
  92. max_recording_seconds: float = 60.0
  93. # Performance configuration
  94. enable_optimization: bool = True
  95. enable_monitoring: bool = True
  96. memory_limit_mb: int = 512
  97. processing_timeout_seconds: float = 30.0
  98. # System configuration
  99. system_mode: MLSystemMode = MLSystemMode.CLIENT
  100. debug_mode: bool = False
  101. # Network configuration
  102. server_host: Optional[str] = None
  103. server_port: Optional[int] = None
  104. def validate(self) -> bool:
  105. """Validate configuration parameters."""
  106. # Wakeword model is required, voice recognition is optional
  107. if not self.wakeword_model_path:
  108. return False
  109. if self.audio_sample_rate <= 0 or self.audio_chunk_size <= 0:
  110. return False
  111. if self.buffer_duration_seconds <= 0 or self.max_recording_seconds <= 0:
  112. return False
  113. if self.wakeword_confidence_threshold < 0 or self.wakeword_confidence_threshold > 1:
  114. return False
  115. if self.voice_recognition_confidence_threshold < 0 or self.voice_recognition_confidence_threshold > 1:
  116. return False
  117. return True
  118. @dataclass
  119. class WakewordEvent:
  120. """Wakeword detection event data."""
  121. wakeword_type: WakewordType
  122. confidence: float
  123. speaker_id: Optional[str]
  124. speaker_name: Optional[str]
  125. timestamp: float
  126. audio_buffer: Optional[np.ndarray] = None
  127. volume: float = 0.0
  128. @dataclass
  129. class AudioProcessingResult:
  130. """Audio processing result data."""
  131. conversation_id: Optional[str]
  132. audio_data: np.ndarray
  133. speaker_id: Optional[str]
  134. speaker_name: Optional[str]
  135. confidence: float
  136. timestamp: float
  137. duration: float
  138. class MLManagerError(Exception):
  139. """Base exception for ML Manager errors."""
  140. pass
  141. def build_wakeword_config_from_flat_json(flat_config: Dict[str, Any]) -> WakewordConfig:
  142. """
  143. Build complete WakewordConfig from flat JSON configuration.
  144. This helper function constructs a fully-nested WakewordConfig with all
  145. required sub-configurations from a flat dictionary structure (like from
  146. client_config.json's "ml" section).
  147. Args:
  148. flat_config: Flat configuration dictionary with keys like:
  149. - wakeword_model_path: str
  150. - wakeword_model_password: str (optional)
  151. - wakeword_threshold: float (optional)
  152. - audio_sample_rate: int (optional)
  153. - enable_gpu: bool (optional)
  154. etc.
  155. Returns:
  156. WakewordConfig: Complete configuration with all sub-configs
  157. Example:
  158. >>> flat_config = {
  159. ... "wakeword_model_path": "models/wakeword/model.pth",
  160. ... "wakeword_threshold": 0.8,
  161. ... "audio_sample_rate": 16000
  162. ... }
  163. >>> config = build_wakeword_config_from_flat_json(flat_config)
  164. """
  165. from pathlib import Path
  166. # Convert relative model path to absolute if needed
  167. model_path = flat_config.get("wakeword_model_path", "")
  168. if model_path and not Path(model_path).is_absolute():
  169. # Assume relative to project root (where main.py is)
  170. model_path = str(Path.cwd() / model_path)
  171. # Build ModelConfig
  172. model_config = WakewordModelConfig(
  173. model_path=model_path,
  174. model_password=flat_config.get("wakeword_model_password", ""),
  175. device="cuda" if flat_config.get("enable_gpu", True) else "cpu",
  176. use_model_optimization=flat_config.get("enable_optimization", True),
  177. verify_model_hash=True,
  178. use_half_precision=False,
  179. enable_model_compilation=True,
  180. fallback_on_error=True
  181. )
  182. # Build SpectrogramConfig
  183. spectrogram_config = SpectrogramConfig(
  184. sample_rate=flat_config.get("audio_sample_rate", 16000),
  185. n_fft=512,
  186. hop_length=160, # 10ms at 16kHz
  187. win_length=400, # 25ms at 16kHz
  188. window_fn="hann",
  189. n_mels=40,
  190. f_min=80.0,
  191. f_max=None, # Will default to sample_rate/2
  192. power=2.0,
  193. normalized=True,
  194. center=True,
  195. pad_mode="constant",
  196. log_offset=1e-8,
  197. time_frames=151
  198. )
  199. # Build AudioConfig
  200. audio_config = WakewordAudioConfig(
  201. sample_rate=flat_config.get("audio_sample_rate", 16000),
  202. channels=flat_config.get("audio_channels", 1),
  203. bit_depth=16,
  204. buffer_duration=2.0,
  205. chunk_duration=1.0,
  206. overlap_ratio=0.5,
  207. spectrogram_config=spectrogram_config,
  208. enable_noise_reduction=flat_config.get("enable_noise_reduction", False),
  209. enable_gain_control=True,
  210. enable_voice_activity_detection=False,
  211. max_chunks_in_memory=10,
  212. use_circular_buffer=True
  213. )
  214. # Build DetectionConfig
  215. detection_config = WakewordDetectionConfig(
  216. custom_threshold=flat_config.get("wakeword_threshold", 0.7),
  217. system_command_threshold=flat_config.get("wakeword_threshold", 0.8),
  218. negative_threshold=0.3,
  219. use_temporal_smoothing=True,
  220. smoothing_window_size=5,
  221. min_detections_in_window=3,
  222. use_half_precision=False,
  223. enable_model_optimization=flat_config.get("enable_optimization", True),
  224. batch_inference=False,
  225. max_batch_size=flat_config.get("batch_size", 1),
  226. log_all_predictions=False,
  227. save_detection_features=False
  228. )
  229. # Build PerformanceConfig
  230. performance_config = WakewordPerformanceConfig(
  231. enable_threading=True,
  232. max_worker_threads=2,
  233. thread_priority="normal",
  234. max_memory_usage_mb=512.0,
  235. enable_memory_pool=True,
  236. gc_frequency=100,
  237. enable_batch_inference=False,
  238. max_batch_size=flat_config.get("batch_size", 1),
  239. inference_timeout_ms=100.0,
  240. enable_performance_monitoring=True,
  241. log_performance_stats=False,
  242. stats_update_interval=10
  243. )
  244. # Build complete WakewordConfig
  245. wakeword_config = WakewordConfig(
  246. model_config=model_config,
  247. audio_config=audio_config,
  248. detection_config=detection_config,
  249. performance_config=performance_config,
  250. enable_event_system=True,
  251. event_handler=None, # Will be set later
  252. satellite_id="",
  253. debug_mode=flat_config.get("debug_mode", False),
  254. save_debug_audio=False,
  255. debug_output_path="/tmp/trixy_wakeword_debug",
  256. enable_voice_recognition_integration=True,
  257. enable_conversation_context=True,
  258. enable_adaptive_thresholds=False
  259. )
  260. return wakeword_config
  261. def build_voice_recognition_config_from_flat_json(flat_config: Dict[str, Any]) -> VoiceRecognitionConfig:
  262. """
  263. Build complete VoiceRecognitionConfig from flat JSON configuration.
  264. This helper function constructs a fully-nested VoiceRecognitionConfig with
  265. all required sub-configurations from a flat dictionary structure.
  266. Args:
  267. flat_config: Flat configuration dictionary with keys like:
  268. - voice_recognition_model_path: str
  269. - voice_recognition_model_password: str (optional)
  270. - voice_recognition_threshold: float (optional)
  271. - audio_sample_rate: int (optional)
  272. - enable_gpu: bool (optional)
  273. etc.
  274. Returns:
  275. VoiceRecognitionConfig: Complete configuration with all sub-configs
  276. Example:
  277. >>> flat_config = {
  278. ... "voice_recognition_model_path": "models/voice/model.pth",
  279. ... "voice_recognition_threshold": 0.7,
  280. ... "audio_sample_rate": 16000
  281. ... }
  282. >>> config = build_voice_recognition_config_from_flat_json(flat_config)
  283. """
  284. from pathlib import Path
  285. # Convert relative model path to absolute if needed
  286. model_path = flat_config.get("voice_recognition_model_path", "")
  287. if model_path and not Path(model_path).is_absolute():
  288. # Assume relative to project root (where main.py is)
  289. model_path = str(Path.cwd() / model_path)
  290. # Build ModelConfig
  291. model_config = VoiceModelConfig(
  292. model_path=model_path,
  293. model_password=flat_config.get("voice_recognition_model_password", None),
  294. architecture_type="ecapa_tdnn", # Default architecture
  295. embedding_dim=192,
  296. num_speakers=4, # Default, will be updated from model metadata
  297. speaker_mapping={},
  298. ecapa_channels=512,
  299. use_attention_pooling=True,
  300. dropout_rate=0.1,
  301. use_batch_norm=True
  302. )
  303. # Build AudioConfig
  304. audio_config = VoiceAudioConfig(
  305. sample_rate=flat_config.get("audio_sample_rate", 16000),
  306. audio_length=3.0,
  307. feature_type="log_mel",
  308. n_mels=40,
  309. n_fft=512,
  310. hop_length=160, # 10ms at 16kHz
  311. win_length=400, # 25ms at 16kHz
  312. f_min=80.0,
  313. f_max=None, # Will default to sample_rate/2
  314. n_mfcc=40,
  315. dct_type=2,
  316. norm="ortho",
  317. log_mels=False,
  318. use_vad=flat_config.get("vad_enabled", True),
  319. vad_type="webrtc",
  320. vad_aggressiveness=flat_config.get("vad_aggressiveness", 2),
  321. vad_frame_duration=flat_config.get("vad_frame_duration", 30),
  322. energy_threshold=0.01,
  323. use_augmentation=False, # Don't augment during inference
  324. noise_factor=0.05,
  325. speed_factor=0.05,
  326. pitch_factor=0.02,
  327. volume_factor=0.1,
  328. reverb_factor=0.03
  329. )
  330. # Build RecognitionConfig
  331. recognition_config = RecognitionConfig(
  332. confidence_threshold=flat_config.get("voice_recognition_threshold", 0.7),
  333. verification_threshold=0.6,
  334. identification_threshold=0.5,
  335. loss_type="angular_margin", # ArcFace
  336. angular_margin=0.5,
  337. angular_scale=64.0,
  338. ge2e_init_w=10.0,
  339. ge2e_init_b=-5.0,
  340. contrastive_margin=2.0,
  341. triplet_margin=0.3,
  342. similarity_metric="cosine",
  343. use_score_normalization=True,
  344. use_temporal_smoothing=True,
  345. smoothing_window_size=5,
  346. confidence_decay_factor=0.9,
  347. use_anti_spoofing=False,
  348. spoofing_threshold=0.5
  349. )
  350. # Build PerformanceConfig
  351. performance_config = VoicePerformanceConfig(
  352. device="cuda" if flat_config.get("enable_gpu", True) else "cpu",
  353. batch_size=flat_config.get("batch_size", 1),
  354. use_half_precision=False,
  355. use_torch_compile=False,
  356. max_memory_usage_mb=512,
  357. clear_cache_interval=100,
  358. target_latency_ms=100.0,
  359. use_streaming_inference=True,
  360. chunk_size_ms=30.0,
  361. num_workers=2,
  362. pin_memory=True,
  363. use_quantization=False,
  364. quantization_backend="fbgemm",
  365. use_tensorrt=False
  366. )
  367. # Build complete VoiceRecognitionConfig
  368. voice_config = VoiceRecognitionConfig(
  369. model_config=model_config,
  370. audio_config=audio_config,
  371. recognition_config=recognition_config,
  372. performance_config=performance_config,
  373. debug_mode=flat_config.get("debug_mode", False),
  374. log_level="INFO",
  375. save_debug_audio=False,
  376. debug_audio_dir="./debug/voice_recognition"
  377. )
  378. return voice_config
  379. class MLManager:
  380. """
  381. ML Manager for coordinating wakeword detection and voice recognition.
  382. This class provides a unified interface for client-side ML processing,
  383. coordinating between wakeword detection and voice recognition systems.
  384. It handles the complete workflow from audio input to event generation.
  385. """
  386. def __init__(
  387. self,
  388. config: MLConfig,
  389. event_handler: Optional[Any] = None,
  390. app_container: Optional[Any] = None,
  391. logger: Optional[logging.Logger] = None
  392. ):
  393. """
  394. Initialize ML Manager.
  395. Args:
  396. config: ML configuration
  397. event_handler: Event handler for triggering events
  398. app_container: Application container for dependency injection
  399. logger: Logger instance
  400. """
  401. self.config = config
  402. self.event_handler = event_handler
  403. self.app_container = app_container
  404. self.logger = logger or logging.getLogger(__name__)
  405. # Validate configuration
  406. if not self.config.validate():
  407. raise MLManagerError("Invalid ML configuration")
  408. # Initialize state
  409. self._state = MLProcessingState.IDLE
  410. self._running = False
  411. self._audio_buffer = []
  412. self._processing_lock = threading.Lock()
  413. # Initialize components
  414. self._wakeword_detector: Optional[WakewordDetector] = None
  415. self._voice_recognition_detector: Optional[VoiceRecognitionDetector] = None
  416. # Performance monitoring
  417. self._start_time = 0.0
  418. self._processing_stats = {
  419. "wakeword_detections": 0,
  420. "voice_recognitions": 0,
  421. "processing_errors": 0,
  422. "average_processing_time": 0.0
  423. }
  424. self._performance_monitor = PerformanceMonitor(
  425. config.get('performance_config', {}) if hasattr(config, 'get') else {}
  426. )
  427. # Audio processing
  428. self._recording_start_time = 0.0
  429. self._silence_start_time = 0.0
  430. self._last_audio_activity = 0.0
  431. # Network integration
  432. self._network_client = None
  433. if self.config.server_host and self.config.server_port:
  434. self._initialize_network_client()
  435. async def initialize(self) -> None:
  436. """Initialize ML components."""
  437. try:
  438. self.logger.info("Initializing ML Manager...")
  439. # DEBUG: Log the actual config values
  440. self.logger.info(f"DEBUG - self.config.wakeword_model_path = {self.config.wakeword_model_path}")
  441. self.logger.info(f"DEBUG - self.config.voice_recognition_model_path = {self.config.voice_recognition_model_path}")
  442. # Build flat config dict from MLConfig for helper functions
  443. flat_config = {
  444. "wakeword_model_path": self.config.wakeword_model_path,
  445. "wakeword_model_password": self.config.wakeword_model_password,
  446. "wakeword_threshold": self.config.wakeword_confidence_threshold,
  447. "voice_recognition_model_path": self.config.voice_recognition_model_path,
  448. "voice_recognition_model_password": self.config.voice_recognition_model_password,
  449. "voice_recognition_threshold": self.config.voice_recognition_confidence_threshold,
  450. "audio_sample_rate": self.config.audio_sample_rate,
  451. "audio_channels": self.config.audio_channels,
  452. "batch_size": 1,
  453. "enable_gpu": True,
  454. "enable_optimization": self.config.enable_optimization,
  455. "debug_mode": self.config.debug_mode,
  456. "vad_enabled": True,
  457. "vad_aggressiveness": 2,
  458. "vad_frame_duration": 30,
  459. "enable_noise_reduction": False
  460. }
  461. # Build complete wakeword configuration from flat JSON
  462. wakeword_config = build_wakeword_config_from_flat_json(flat_config)
  463. wakeword_config.event_handler = self.event_handler
  464. # Check if voice recognition is enabled (has model path specified)
  465. voice_recognition_enabled = (
  466. self.config.voice_recognition_model_path and
  467. self.config.voice_recognition_model_path.strip()
  468. )
  469. # Build complete voice recognition configuration only if enabled
  470. voice_config = None
  471. if voice_recognition_enabled:
  472. voice_config = build_voice_recognition_config_from_flat_json(flat_config)
  473. self.logger.info(f"Voice recognition enabled - model path: {voice_config.model_config.model_path}")
  474. else:
  475. self.logger.info("Voice recognition disabled - no model path specified")
  476. # Debug: Log the model paths to verify they're correct
  477. self.logger.info(f"Wakeword model path: {wakeword_config.model_config.model_path}")
  478. # Check if model files exist
  479. from pathlib import Path
  480. if not Path(wakeword_config.model_config.model_path).exists():
  481. raise MLManagerError(f"Wakeword model file not found: {wakeword_config.model_config.model_path}")
  482. if voice_recognition_enabled and not Path(voice_config.model_config.model_path).exists():
  483. raise MLManagerError(f"Voice recognition model file not found: {voice_config.model_config.model_path}")
  484. # Validate configurations - validate sub-configs individually since overall
  485. # WakewordConfig.validate() has a design issue with AudioConfig validation
  486. if not wakeword_config.model_config.validate():
  487. raise MLManagerError(f"Invalid wakeword model configuration - model_path validation failed")
  488. if not wakeword_config.detection_config.validate():
  489. raise MLManagerError("Invalid wakeword detection configuration")
  490. if not wakeword_config.performance_config.validate():
  491. raise MLManagerError("Invalid wakeword performance configuration")
  492. self.logger.info(f"Wakeword config built successfully: threshold={wakeword_config.detection_config.custom_threshold}")
  493. if voice_recognition_enabled:
  494. self.logger.info(f"Voice config built successfully: threshold={voice_config.recognition_config.confidence_threshold}")
  495. # Initialize detectors
  496. # For wakeword detector, use direct constructor with config object
  497. self._wakeword_detector = WakewordDetector(
  498. config=wakeword_config,
  499. event_handler=self.event_handler,
  500. logger=self.logger
  501. )
  502. # For voice recognition detector, only initialize if enabled
  503. if voice_recognition_enabled:
  504. self._voice_recognition_detector = create_voice_recognition_detector(
  505. config=voice_config,
  506. event_handler=self.event_handler,
  507. audio_callback=None
  508. )
  509. else:
  510. self._voice_recognition_detector = None
  511. self.logger.info("Voice recognition detector not initialized (disabled)")
  512. # Apply optimizations if enabled
  513. if self.config.enable_optimization:
  514. await self._apply_optimizations()
  515. self.logger.info("ML Manager initialized successfully")
  516. except Exception as e:
  517. self.logger.error(f"Failed to initialize ML Manager: {e}")
  518. import traceback
  519. self.logger.error(f"Traceback: {traceback.format_exc()}")
  520. raise MLManagerError(f"Initialization failed: {e}")
  521. async def start(self) -> None:
  522. """Start ML processing."""
  523. if self._running:
  524. self.logger.warning("ML Manager is already running")
  525. return
  526. try:
  527. self.logger.info("Starting ML Manager...")
  528. # Initialize if not already done
  529. if not self._wakeword_detector:
  530. await self.initialize()
  531. # Start detectors
  532. self._wakeword_detector.start()
  533. if self._voice_recognition_detector:
  534. self._voice_recognition_detector.start()
  535. else:
  536. self.logger.info("Voice recognition detector is disabled, skipping start")
  537. # Set state
  538. self._running = True
  539. self._state = MLProcessingState.LISTENING
  540. self._start_time = time.time()
  541. # Start performance monitoring
  542. if self.config.enable_monitoring:
  543. await self._performance_monitor.start_monitoring()
  544. # Trigger system event
  545. if self.event_handler:
  546. await self._trigger_event("ml_system_started", {
  547. "timestamp": time.time(),
  548. "mode": self.config.system_mode.value
  549. })
  550. self.logger.info("ML Manager started successfully")
  551. except Exception as e:
  552. self.logger.error(f"Failed to start ML Manager: {e}")
  553. self._state = MLProcessingState.ERROR
  554. raise MLManagerError(f"Start failed: {e}")
  555. async def stop(self) -> None:
  556. """Stop ML processing."""
  557. if not self._running:
  558. return
  559. try:
  560. self.logger.info("Stopping ML Manager...")
  561. # Stop detectors
  562. if self._wakeword_detector:
  563. self._wakeword_detector.stop()
  564. if self._voice_recognition_detector:
  565. self._voice_recognition_detector.stop()
  566. # Stop performance monitoring
  567. if self.config.enable_monitoring:
  568. await self._performance_monitor.stop_monitoring()
  569. # Clear state
  570. self._running = False
  571. self._state = MLProcessingState.IDLE
  572. self._audio_buffer.clear()
  573. # Trigger system event
  574. if self.event_handler:
  575. uptime = time.time() - self._start_time
  576. await self._trigger_event("ml_system_stopped", {
  577. "timestamp": time.time(),
  578. "uptime": uptime,
  579. "stats": self._processing_stats.copy()
  580. })
  581. self.logger.info("ML Manager stopped successfully")
  582. except Exception as e:
  583. self.logger.error(f"Error stopping ML Manager: {e}")
  584. async def process_audio_chunk(self, audio_data: np.ndarray) -> None:
  585. """
  586. Process audio chunk through ML pipeline.
  587. Args:
  588. audio_data: Audio data as numpy array
  589. """
  590. if not self._running:
  591. return
  592. # Start performance timer
  593. timer_key = self._performance_monitor.start_timer("ml_manager", "process_audio_chunk")
  594. try:
  595. with self._processing_lock:
  596. # Process based on current state
  597. if self._state == MLProcessingState.LISTENING:
  598. await self._process_listening_state(audio_data)
  599. elif self._state == MLProcessingState.RECORDING:
  600. await self._process_recording_state(audio_data)
  601. # Record throughput metric
  602. self._performance_monitor.record_metric(
  603. "ml_manager", MetricType.THROUGHPUT,
  604. len(audio_data), "samples"
  605. )
  606. except Exception as e:
  607. self.logger.error(f"Error processing audio chunk: {e}")
  608. self._processing_stats["processing_errors"] += 1
  609. self._performance_monitor.record_metric(
  610. "ml_manager", MetricType.ERROR_RATE, 1, "errors"
  611. )
  612. finally:
  613. # End performance timer
  614. processing_time = self._performance_monitor.end_timer(timer_key)
  615. if processing_time > 0:
  616. self._performance_monitor.record_metric(
  617. "ml_manager", MetricType.LATENCY,
  618. processing_time, "milliseconds"
  619. )
  620. async def _process_listening_state(self, audio_data: np.ndarray) -> None:
  621. """Process audio in listening state (wakeword detection)."""
  622. # Feed audio to wakeword detector
  623. if self._wakeword_detector:
  624. self._wakeword_detector.process_audio_chunk(audio_data)
  625. async def _process_recording_state(self, audio_data: np.ndarray) -> None:
  626. """Process audio in recording state (after wakeword detected)."""
  627. # Add audio to buffer
  628. self._audio_buffer.append(audio_data)
  629. # Check for voice activity
  630. has_voice = self._detect_voice_activity(audio_data)
  631. current_time = time.time()
  632. if has_voice:
  633. self._last_audio_activity = current_time
  634. self._silence_start_time = 0.0
  635. else:
  636. if self._last_audio_activity > 0 and self._silence_start_time == 0.0:
  637. self._silence_start_time = current_time
  638. # Check stopping conditions
  639. recording_duration = current_time - self._recording_start_time
  640. silence_duration = (current_time - self._silence_start_time) if self._silence_start_time > 0 else 0
  641. if (recording_duration >= self.config.max_recording_seconds or
  642. silence_duration >= self.config.silence_timeout_seconds):
  643. await self._complete_recording()
  644. def _detect_voice_activity(self, audio_data: np.ndarray) -> bool:
  645. """Simple voice activity detection based on energy."""
  646. energy = np.mean(audio_data ** 2)
  647. threshold = 0.01 # Adjust based on requirements
  648. return energy > threshold
  649. async def _complete_recording(self) -> None:
  650. """Complete audio recording and process result."""
  651. try:
  652. # Combine audio buffer
  653. if not self._audio_buffer:
  654. self.logger.warning("No audio data recorded")
  655. self._reset_recording_state()
  656. return
  657. combined_audio = np.concatenate(self._audio_buffer)
  658. recording_duration = time.time() - self._recording_start_time
  659. # Process with voice recognition
  660. speaker_result = None
  661. if self._voice_recognition_detector:
  662. speaker_result = self._voice_recognition_detector.identify_speaker(combined_audio)
  663. # Create processing result
  664. result = AudioProcessingResult(
  665. conversation_id=None, # Will be set by server
  666. audio_data=combined_audio,
  667. speaker_id=speaker_result.speaker_id if speaker_result else None,
  668. speaker_name=speaker_result.speaker_name if speaker_result else None,
  669. confidence=speaker_result.confidence if speaker_result else 0.0,
  670. timestamp=time.time(),
  671. duration=recording_duration
  672. )
  673. # Trigger audio processing event
  674. if self.event_handler:
  675. await self._trigger_event("raw_audio_input_received", {
  676. "audio_data": result.audio_data,
  677. "speaker_id": result.speaker_id,
  678. "speaker_name": result.speaker_name,
  679. "confidence": result.confidence,
  680. "timestamp": result.timestamp,
  681. "duration": result.duration
  682. })
  683. # Send to server if in client mode
  684. if self.config.system_mode == MLSystemMode.CLIENT:
  685. await self._send_audio_to_server(result)
  686. # Update statistics
  687. self._processing_stats["voice_recognitions"] += 1
  688. self.logger.info(f"Audio processing completed: speaker={result.speaker_name}, confidence={result.confidence:.2f}")
  689. except Exception as e:
  690. self.logger.error(f"Error completing recording: {e}")
  691. self._processing_stats["processing_errors"] += 1
  692. finally:
  693. self._reset_recording_state()
  694. def _reset_recording_state(self) -> None:
  695. """Reset recording state back to listening."""
  696. self._audio_buffer.clear()
  697. self._recording_start_time = 0.0
  698. self._silence_start_time = 0.0
  699. self._last_audio_activity = 0.0
  700. self._state = MLProcessingState.LISTENING
  701. # Resume wakeword detection
  702. if self._wakeword_detector:
  703. self._wakeword_detector.resume()
  704. async def _handle_wakeword_event(self, detection_result: DetectionResult) -> None:
  705. """Handle wakeword detection event."""
  706. try:
  707. self.logger.info(f"Wakeword detected: {detection_result.wakeword_type}, confidence: {detection_result.confidence:.2f}")
  708. # Pause wakeword detection
  709. if self._wakeword_detector:
  710. self._wakeword_detector.pause()
  711. # Start recording
  712. self._state = MLProcessingState.RECORDING
  713. self._recording_start_time = time.time()
  714. self._last_audio_activity = self._recording_start_time
  715. # Create wakeword event
  716. wakeword_event = WakewordEvent(
  717. wakeword_type=detection_result.wakeword_type,
  718. confidence=detection_result.confidence,
  719. speaker_id=None, # Will be determined during recording
  720. speaker_name=None,
  721. timestamp=detection_result.timestamp,
  722. volume=getattr(detection_result, 'volume', 0.0)
  723. )
  724. # Trigger wakeword event
  725. if self.event_handler:
  726. await self._trigger_event("wakeword_received", {
  727. "wakeword_id": detection_result.wakeword_type.value,
  728. "wakeword_type": detection_result.wakeword_type.value,
  729. "confidence": detection_result.confidence,
  730. "timestamp": detection_result.timestamp,
  731. "volume": wakeword_event.volume
  732. })
  733. # Send to server if in client mode
  734. if self.config.system_mode == MLSystemMode.CLIENT:
  735. await self._send_wakeword_to_server(wakeword_event)
  736. # Update statistics
  737. self._processing_stats["wakeword_detections"] += 1
  738. except Exception as e:
  739. self.logger.error(f"Error handling wakeword event: {e}")
  740. self._processing_stats["processing_errors"] += 1
  741. self._reset_recording_state()
  742. async def _handle_voice_recognition_event(self, result: IdentificationResult) -> None:
  743. """Handle voice recognition event."""
  744. self.logger.info(f"Speaker identified: {result.speaker_name}, confidence: {result.confidence:.2f}")
  745. async def _send_wakeword_to_server(self, wakeword_event: WakewordEvent) -> None:
  746. """Send wakeword detection to server."""
  747. if not self._network_client:
  748. return
  749. try:
  750. # Create wakeword command for server
  751. # This would integrate with the network protocol
  752. self.logger.debug(f"Sending wakeword to server: {wakeword_event.wakeword_type}")
  753. except Exception as e:
  754. self.logger.error(f"Error sending wakeword to server: {e}")
  755. async def _send_audio_to_server(self, result: AudioProcessingResult) -> None:
  756. """Send audio recording to server."""
  757. if not self._network_client:
  758. return
  759. try:
  760. # Send audio data through raw audio input stream
  761. self.logger.debug(f"Sending audio to server: {result.duration:.2f}s")
  762. except Exception as e:
  763. self.logger.error(f"Error sending audio to server: {e}")
  764. def _initialize_network_client(self) -> None:
  765. """Initialize network client for server communication."""
  766. # This would initialize the network client for command socket
  767. # and audio streaming connections
  768. self.logger.info("Network client initialization placeholder")
  769. async def _apply_optimizations(self) -> None:
  770. """Apply performance optimizations."""
  771. try:
  772. # Model optimization
  773. if self._wakeword_detector and hasattr(self._wakeword_detector, 'optimize'):
  774. self._wakeword_detector.optimize()
  775. if self._voice_recognition_detector and hasattr(self._voice_recognition_detector, 'optimize'):
  776. self._voice_recognition_detector.optimize()
  777. self.logger.info("ML optimizations applied")
  778. except Exception as e:
  779. self.logger.error(f"Error applying optimizations: {e}")
  780. async def _trigger_event(self, event_name: str, event_data: Dict[str, Any]) -> None:
  781. """Trigger event through event handler."""
  782. if self.event_handler:
  783. try:
  784. # This would integrate with the actual event handler
  785. self.logger.debug(f"Triggering event: {event_name}")
  786. except Exception as e:
  787. self.logger.error(f"Error triggering event {event_name}: {e}")
  788. def get_state(self) -> MLProcessingState:
  789. """Get current processing state."""
  790. return self._state
  791. def get_statistics(self) -> Dict[str, Any]:
  792. """Get processing statistics."""
  793. stats = self._processing_stats.copy()
  794. if self._running:
  795. stats["uptime"] = time.time() - self._start_time
  796. stats["current_state"] = self._state.value
  797. return stats
  798. def is_running(self) -> bool:
  799. """Check if ML manager is running."""
  800. return self._running
  801. async def reload_models(self) -> None:
  802. """Reload ML models."""
  803. try:
  804. self.logger.info("Reloading ML models...")
  805. # Stop current processing
  806. was_running = self._running
  807. if was_running:
  808. await self.stop()
  809. # Reinitialize
  810. await self.initialize()
  811. # Restart if it was running
  812. if was_running:
  813. await self.start()
  814. self.logger.info("ML models reloaded successfully")
  815. except Exception as e:
  816. self.logger.error(f"Error reloading models: {e}")
  817. raise MLManagerError(f"Model reload failed: {e}")
  818. async def update_configuration(self, new_config: MLConfig) -> None:
  819. """Update ML configuration."""
  820. try:
  821. self.logger.info("Updating ML configuration...")
  822. # Validate new configuration
  823. if not new_config.validate():
  824. raise MLManagerError("Invalid configuration")
  825. # Stop current processing
  826. was_running = self._running
  827. if was_running:
  828. await self.stop()
  829. # Update configuration
  830. self.config = new_config
  831. # Reinitialize with new config
  832. await self.initialize()
  833. # Restart if it was running
  834. if was_running:
  835. await self.start()
  836. self.logger.info("ML configuration updated successfully")
  837. except Exception as e:
  838. self.logger.error(f"Error updating configuration: {e}")
  839. raise MLManagerError(f"Configuration update failed: {e}")
  840. def get_performance_report(self, window_hours: int = 1) -> Dict[str, Any]:
  841. """
  842. Get comprehensive performance report.
  843. Args:
  844. window_hours: Time window in hours for the report
  845. Returns:
  846. Performance report dictionary
  847. """
  848. from datetime import timedelta
  849. window = timedelta(hours=window_hours)
  850. report = self._performance_monitor.get_performance_report(window)
  851. return {
  852. 'timestamp': report.timestamp.isoformat(),
  853. 'window_hours': window_hours,
  854. 'system_stats': report.system_stats,
  855. 'component_stats': report.component_stats,
  856. 'active_alerts': report.alerts,
  857. 'recommendations': report.recommendations,
  858. 'ml_manager_stats': self._processing_stats.copy(),
  859. 'uptime_hours': (time.time() - self._start_time) / 3600 if self._start_time > 0 else 0
  860. }
  861. def export_performance_metrics(self, file_path: str, window_hours: int = 24) -> None:
  862. """
  863. Export performance metrics to file.
  864. Args:
  865. file_path: Path to export file
  866. window_hours: Time window in hours
  867. """
  868. from datetime import timedelta
  869. window = timedelta(hours=window_hours)
  870. self._performance_monitor.export_metrics(file_path, window)
  871. self.logger.info(f"Performance metrics exported to {file_path}")
  872. def get_current_metrics(self) -> Dict[str, Any]:
  873. """
  874. Get current real-time performance metrics.
  875. Returns:
  876. Current metrics dictionary
  877. """
  878. return {
  879. 'state': self._state.value,
  880. 'uptime_seconds': time.time() - self._start_time if self._start_time > 0 else 0,
  881. 'processing_stats': self._processing_stats.copy(),
  882. 'active_alerts': len(self._performance_monitor.alert_manager.get_active_alerts()),
  883. 'system_cpu': self._performance_monitor.system_monitor.get_cpu_usage(),
  884. 'system_memory': self._performance_monitor.system_monitor.get_memory_usage(),
  885. 'audio_buffer_size': len(self._audio_buffer),
  886. 'is_monitoring': self._performance_monitor.monitoring_active
  887. }
  888. def create_ml_manager(
  889. config: MLConfig,
  890. event_handler: Optional[Any] = None,
  891. app_container: Optional[Any] = None,
  892. logger: Optional[logging.Logger] = None
  893. ) -> MLManager:
  894. """
  895. Factory function to create ML Manager.
  896. Args:
  897. config: ML configuration
  898. event_handler: Event handler for triggering events
  899. app_container: Application container for dependency injection
  900. logger: Logger instance
  901. Returns:
  902. MLManager: Configured ML manager instance
  903. """
  904. return MLManager(config, event_handler, app_container, logger)
  905. def create_default_ml_config() -> MLConfig:
  906. """
  907. Create default ML configuration.
  908. Returns:
  909. MLConfig: Default configuration
  910. """
  911. return MLConfig(
  912. wakeword_model_path="./models/wakeword/default/model.pth",
  913. wakeword_model_password="",
  914. voice_recognition_model_path="./models/voice_recognition/default/model.pth",
  915. voice_recognition_model_password=""
  916. )
  917. def create_optimized_ml_config() -> MLConfig:
  918. """
  919. Create optimized ML configuration for production.
  920. Returns:
  921. MLConfig: Optimized configuration
  922. """
  923. config = create_default_ml_config()
  924. config.enable_optimization = True
  925. config.enable_monitoring = True
  926. config.memory_limit_mb = 256
  927. config.processing_timeout_seconds = 15.0
  928. return config
  929. def create_debug_ml_config() -> MLConfig:
  930. """
  931. Create debug ML configuration for development.
  932. Returns:
  933. MLConfig: Debug configuration
  934. """
  935. config = create_default_ml_config()
  936. config.debug_mode = True
  937. config.system_mode = MLSystemMode.DEBUG
  938. config.enable_monitoring = True
  939. return config