| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122 |
- """
- ML Manager for Client-side ML Integration in Trixy Voice Assistant.
- This module coordinates wakeword detection and voice recognition systems,
- providing a unified interface for client-side AI processing. It manages
- the complete workflow from audio input to event generation.
- Key Features:
- - Unified coordination of wakeword and voice recognition
- - Real-time audio pipeline management
- - Event system integration
- - Performance monitoring and optimization
- - Error handling and fallback mechanisms
- - Configuration management for ML models
- - Network protocol integration
- - Application container support
- Usage:
- from trixy_core.ml import MLManager, MLConfig
-
- # Create configuration
- config = MLConfig(
- wakeword_model_path="/path/to/wakeword.pth",
- voice_recognition_model_path="/path/to/voice.pth",
- audio_sample_rate=16000
- )
-
- # Initialize ML manager
- ml_manager = MLManager(config, event_handler, app_container)
-
- # Start ML processing
- await ml_manager.start()
-
- # Process audio chunk
- await ml_manager.process_audio_chunk(audio_data)
- """
- import asyncio
- import logging
- import time
- import threading
- from typing import Optional, Dict, Any, List, Callable, Union
- from dataclasses import dataclass
- from enum import Enum
- import numpy as np
- from .wakeword import (
- WakewordDetector, WakewordConfig, create_wakeword_detector,
- WakewordDetectorError, DetectionResult, WakewordType
- )
- from .wakeword.config import (
- ModelConfig as WakewordModelConfig,
- AudioConfig as WakewordAudioConfig,
- PerformanceConfig as WakewordPerformanceConfig
- )
- from .wakeword.detection_engine import DetectionConfig as WakewordDetectionConfig
- from .wakeword.audio_features import SpectrogramConfig
- from .performance_monitor import PerformanceMonitor, MetricType
- from .voice_recognition import (
- VoiceRecognitionDetector, VoiceRecognitionConfig, create_voice_recognition_detector,
- VoiceRecognitionDetectorError, IdentificationResult
- )
- from .voice_recognition.config import (
- ModelConfig as VoiceModelConfig,
- AudioConfig as VoiceAudioConfig,
- RecognitionConfig,
- PerformanceConfig as VoicePerformanceConfig
- )
- class MLProcessingState(Enum):
- """ML processing state enumeration."""
- IDLE = "idle"
- LISTENING = "listening"
- WAKEWORD_DETECTED = "wakeword_detected"
- RECORDING = "recording"
- PROCESSING = "processing"
- ERROR = "error"
- class MLSystemMode(Enum):
- """ML system operating mode."""
- CLIENT = "client"
- STANDALONE = "standalone"
- DEBUG = "debug"
- @dataclass
- class MLConfig:
- """Configuration for ML Manager."""
- # Wakeword configuration
- wakeword_model_path: str = ""
- wakeword_model_password: str = ""
- wakeword_confidence_threshold: float = 0.7
-
- # Voice recognition configuration
- voice_recognition_model_path: str = ""
- voice_recognition_model_password: str = ""
- voice_recognition_confidence_threshold: float = 0.8
-
- # Audio configuration
- audio_sample_rate: int = 16000
- audio_chunk_size: int = 1024
- audio_channels: int = 1
- audio_format: str = "int16"
-
- # Processing configuration
- buffer_duration_seconds: float = 10.0
- silence_timeout_seconds: float = 3.0
- max_recording_seconds: float = 60.0
-
- # Performance configuration
- enable_optimization: bool = True
- enable_monitoring: bool = True
- memory_limit_mb: int = 512
- processing_timeout_seconds: float = 30.0
-
- # System configuration
- system_mode: MLSystemMode = MLSystemMode.CLIENT
- debug_mode: bool = False
-
- # Network configuration
- server_host: Optional[str] = None
- server_port: Optional[int] = None
-
- def validate(self) -> bool:
- """Validate configuration parameters."""
- # Wakeword model is required, voice recognition is optional
- if not self.wakeword_model_path:
- return False
- if self.audio_sample_rate <= 0 or self.audio_chunk_size <= 0:
- return False
- if self.buffer_duration_seconds <= 0 or self.max_recording_seconds <= 0:
- return False
- if self.wakeword_confidence_threshold < 0 or self.wakeword_confidence_threshold > 1:
- return False
- if self.voice_recognition_confidence_threshold < 0 or self.voice_recognition_confidence_threshold > 1:
- return False
- return True
- @dataclass
- class WakewordEvent:
- """Wakeword detection event data."""
- wakeword_type: WakewordType
- confidence: float
- speaker_id: Optional[str]
- speaker_name: Optional[str]
- timestamp: float
- audio_buffer: Optional[np.ndarray] = None
- volume: float = 0.0
- @dataclass
- class AudioProcessingResult:
- """Audio processing result data."""
- conversation_id: Optional[str]
- audio_data: np.ndarray
- speaker_id: Optional[str]
- speaker_name: Optional[str]
- confidence: float
- timestamp: float
- duration: float
- class MLManagerError(Exception):
- """Base exception for ML Manager errors."""
- pass
- def build_wakeword_config_from_flat_json(flat_config: Dict[str, Any]) -> WakewordConfig:
- """
- Build complete WakewordConfig from flat JSON configuration.
- This helper function constructs a fully-nested WakewordConfig with all
- required sub-configurations from a flat dictionary structure (like from
- client_config.json's "ml" section).
- Args:
- flat_config: Flat configuration dictionary with keys like:
- - wakeword_model_path: str
- - wakeword_model_password: str (optional)
- - wakeword_threshold: float (optional)
- - audio_sample_rate: int (optional)
- - enable_gpu: bool (optional)
- etc.
- Returns:
- WakewordConfig: Complete configuration with all sub-configs
- Example:
- >>> flat_config = {
- ... "wakeword_model_path": "models/wakeword/model.pth",
- ... "wakeword_threshold": 0.8,
- ... "audio_sample_rate": 16000
- ... }
- >>> config = build_wakeword_config_from_flat_json(flat_config)
- """
- from pathlib import Path
- # Convert relative model path to absolute if needed
- model_path = flat_config.get("wakeword_model_path", "")
- if model_path and not Path(model_path).is_absolute():
- # Assume relative to project root (where main.py is)
- model_path = str(Path.cwd() / model_path)
- # Build ModelConfig
- model_config = WakewordModelConfig(
- model_path=model_path,
- model_password=flat_config.get("wakeword_model_password", ""),
- device="cuda" if flat_config.get("enable_gpu", True) else "cpu",
- use_model_optimization=flat_config.get("enable_optimization", True),
- verify_model_hash=True,
- use_half_precision=False,
- enable_model_compilation=True,
- fallback_on_error=True
- )
- # Build SpectrogramConfig
- spectrogram_config = SpectrogramConfig(
- sample_rate=flat_config.get("audio_sample_rate", 16000),
- n_fft=512,
- hop_length=160, # 10ms at 16kHz
- win_length=400, # 25ms at 16kHz
- window_fn="hann",
- n_mels=40,
- f_min=80.0,
- f_max=None, # Will default to sample_rate/2
- power=2.0,
- normalized=True,
- center=True,
- pad_mode="constant",
- log_offset=1e-8,
- time_frames=151
- )
- # Build AudioConfig
- audio_config = WakewordAudioConfig(
- sample_rate=flat_config.get("audio_sample_rate", 16000),
- channels=flat_config.get("audio_channels", 1),
- bit_depth=16,
- buffer_duration=2.0,
- chunk_duration=1.0,
- overlap_ratio=0.5,
- spectrogram_config=spectrogram_config,
- enable_noise_reduction=flat_config.get("enable_noise_reduction", False),
- enable_gain_control=True,
- enable_voice_activity_detection=False,
- max_chunks_in_memory=10,
- use_circular_buffer=True
- )
- # Build DetectionConfig
- detection_config = WakewordDetectionConfig(
- custom_threshold=flat_config.get("wakeword_threshold", 0.7),
- system_command_threshold=flat_config.get("wakeword_threshold", 0.8),
- negative_threshold=0.3,
- use_temporal_smoothing=True,
- smoothing_window_size=5,
- min_detections_in_window=3,
- use_half_precision=False,
- enable_model_optimization=flat_config.get("enable_optimization", True),
- batch_inference=False,
- max_batch_size=flat_config.get("batch_size", 1),
- log_all_predictions=False,
- save_detection_features=False
- )
- # Build PerformanceConfig
- performance_config = WakewordPerformanceConfig(
- enable_threading=True,
- max_worker_threads=2,
- thread_priority="normal",
- max_memory_usage_mb=512.0,
- enable_memory_pool=True,
- gc_frequency=100,
- enable_batch_inference=False,
- max_batch_size=flat_config.get("batch_size", 1),
- inference_timeout_ms=100.0,
- enable_performance_monitoring=True,
- log_performance_stats=False,
- stats_update_interval=10
- )
- # Build complete WakewordConfig
- wakeword_config = WakewordConfig(
- model_config=model_config,
- audio_config=audio_config,
- detection_config=detection_config,
- performance_config=performance_config,
- enable_event_system=True,
- event_handler=None, # Will be set later
- satellite_id="",
- debug_mode=flat_config.get("debug_mode", False),
- save_debug_audio=False,
- debug_output_path="/tmp/trixy_wakeword_debug",
- enable_voice_recognition_integration=True,
- enable_conversation_context=True,
- enable_adaptive_thresholds=False
- )
- return wakeword_config
- def build_voice_recognition_config_from_flat_json(flat_config: Dict[str, Any]) -> VoiceRecognitionConfig:
- """
- Build complete VoiceRecognitionConfig from flat JSON configuration.
- This helper function constructs a fully-nested VoiceRecognitionConfig with
- all required sub-configurations from a flat dictionary structure.
- Args:
- flat_config: Flat configuration dictionary with keys like:
- - voice_recognition_model_path: str
- - voice_recognition_model_password: str (optional)
- - voice_recognition_threshold: float (optional)
- - audio_sample_rate: int (optional)
- - enable_gpu: bool (optional)
- etc.
- Returns:
- VoiceRecognitionConfig: Complete configuration with all sub-configs
- Example:
- >>> flat_config = {
- ... "voice_recognition_model_path": "models/voice/model.pth",
- ... "voice_recognition_threshold": 0.7,
- ... "audio_sample_rate": 16000
- ... }
- >>> config = build_voice_recognition_config_from_flat_json(flat_config)
- """
- from pathlib import Path
- # Convert relative model path to absolute if needed
- model_path = flat_config.get("voice_recognition_model_path", "")
- if model_path and not Path(model_path).is_absolute():
- # Assume relative to project root (where main.py is)
- model_path = str(Path.cwd() / model_path)
- # Build ModelConfig
- model_config = VoiceModelConfig(
- model_path=model_path,
- model_password=flat_config.get("voice_recognition_model_password", None),
- architecture_type="ecapa_tdnn", # Default architecture
- embedding_dim=192,
- num_speakers=4, # Default, will be updated from model metadata
- speaker_mapping={},
- ecapa_channels=512,
- use_attention_pooling=True,
- dropout_rate=0.1,
- use_batch_norm=True
- )
- # Build AudioConfig
- audio_config = VoiceAudioConfig(
- sample_rate=flat_config.get("audio_sample_rate", 16000),
- audio_length=3.0,
- feature_type="log_mel",
- n_mels=40,
- n_fft=512,
- hop_length=160, # 10ms at 16kHz
- win_length=400, # 25ms at 16kHz
- f_min=80.0,
- f_max=None, # Will default to sample_rate/2
- n_mfcc=40,
- dct_type=2,
- norm="ortho",
- log_mels=False,
- use_vad=flat_config.get("vad_enabled", True),
- vad_type="webrtc",
- vad_aggressiveness=flat_config.get("vad_aggressiveness", 2),
- vad_frame_duration=flat_config.get("vad_frame_duration", 30),
- energy_threshold=0.01,
- use_augmentation=False, # Don't augment during inference
- noise_factor=0.05,
- speed_factor=0.05,
- pitch_factor=0.02,
- volume_factor=0.1,
- reverb_factor=0.03
- )
- # Build RecognitionConfig
- recognition_config = RecognitionConfig(
- confidence_threshold=flat_config.get("voice_recognition_threshold", 0.7),
- verification_threshold=0.6,
- identification_threshold=0.5,
- loss_type="angular_margin", # ArcFace
- angular_margin=0.5,
- angular_scale=64.0,
- ge2e_init_w=10.0,
- ge2e_init_b=-5.0,
- contrastive_margin=2.0,
- triplet_margin=0.3,
- similarity_metric="cosine",
- use_score_normalization=True,
- use_temporal_smoothing=True,
- smoothing_window_size=5,
- confidence_decay_factor=0.9,
- use_anti_spoofing=False,
- spoofing_threshold=0.5
- )
- # Build PerformanceConfig
- performance_config = VoicePerformanceConfig(
- device="cuda" if flat_config.get("enable_gpu", True) else "cpu",
- batch_size=flat_config.get("batch_size", 1),
- use_half_precision=False,
- use_torch_compile=False,
- max_memory_usage_mb=512,
- clear_cache_interval=100,
- target_latency_ms=100.0,
- use_streaming_inference=True,
- chunk_size_ms=30.0,
- num_workers=2,
- pin_memory=True,
- use_quantization=False,
- quantization_backend="fbgemm",
- use_tensorrt=False
- )
- # Build complete VoiceRecognitionConfig
- voice_config = VoiceRecognitionConfig(
- model_config=model_config,
- audio_config=audio_config,
- recognition_config=recognition_config,
- performance_config=performance_config,
- debug_mode=flat_config.get("debug_mode", False),
- log_level="INFO",
- save_debug_audio=False,
- debug_audio_dir="./debug/voice_recognition"
- )
- return voice_config
- class MLManager:
- """
- ML Manager for coordinating wakeword detection and voice recognition.
-
- This class provides a unified interface for client-side ML processing,
- coordinating between wakeword detection and voice recognition systems.
- It handles the complete workflow from audio input to event generation.
- """
-
- def __init__(
- self,
- config: MLConfig,
- event_handler: Optional[Any] = None,
- app_container: Optional[Any] = None,
- logger: Optional[logging.Logger] = None
- ):
- """
- Initialize ML Manager.
-
- Args:
- config: ML configuration
- event_handler: Event handler for triggering events
- app_container: Application container for dependency injection
- logger: Logger instance
- """
- self.config = config
- self.event_handler = event_handler
- self.app_container = app_container
- self.logger = logger or logging.getLogger(__name__)
-
- # Validate configuration
- if not self.config.validate():
- raise MLManagerError("Invalid ML configuration")
-
- # Initialize state
- self._state = MLProcessingState.IDLE
- self._running = False
- self._audio_buffer = []
- self._processing_lock = threading.Lock()
-
- # Initialize components
- self._wakeword_detector: Optional[WakewordDetector] = None
- self._voice_recognition_detector: Optional[VoiceRecognitionDetector] = None
-
- # Performance monitoring
- self._start_time = 0.0
- self._processing_stats = {
- "wakeword_detections": 0,
- "voice_recognitions": 0,
- "processing_errors": 0,
- "average_processing_time": 0.0
- }
- self._performance_monitor = PerformanceMonitor(
- config.get('performance_config', {}) if hasattr(config, 'get') else {}
- )
-
- # Audio processing
- self._recording_start_time = 0.0
- self._silence_start_time = 0.0
- self._last_audio_activity = 0.0
-
- # Network integration
- self._network_client = None
- if self.config.server_host and self.config.server_port:
- self._initialize_network_client()
-
- async def initialize(self) -> None:
- """Initialize ML components."""
- try:
- self.logger.info("Initializing ML Manager...")
- # DEBUG: Log the actual config values
- self.logger.info(f"DEBUG - self.config.wakeword_model_path = {self.config.wakeword_model_path}")
- self.logger.info(f"DEBUG - self.config.voice_recognition_model_path = {self.config.voice_recognition_model_path}")
- # Build flat config dict from MLConfig for helper functions
- flat_config = {
- "wakeword_model_path": self.config.wakeword_model_path,
- "wakeword_model_password": self.config.wakeword_model_password,
- "wakeword_threshold": self.config.wakeword_confidence_threshold,
- "voice_recognition_model_path": self.config.voice_recognition_model_path,
- "voice_recognition_model_password": self.config.voice_recognition_model_password,
- "voice_recognition_threshold": self.config.voice_recognition_confidence_threshold,
- "audio_sample_rate": self.config.audio_sample_rate,
- "audio_channels": self.config.audio_channels,
- "batch_size": 1,
- "enable_gpu": True,
- "enable_optimization": self.config.enable_optimization,
- "debug_mode": self.config.debug_mode,
- "vad_enabled": True,
- "vad_aggressiveness": 2,
- "vad_frame_duration": 30,
- "enable_noise_reduction": False
- }
- # Build complete wakeword configuration from flat JSON
- wakeword_config = build_wakeword_config_from_flat_json(flat_config)
- wakeword_config.event_handler = self.event_handler
- # Check if voice recognition is enabled (has model path specified)
- voice_recognition_enabled = (
- self.config.voice_recognition_model_path and
- self.config.voice_recognition_model_path.strip()
- )
- # Build complete voice recognition configuration only if enabled
- voice_config = None
- if voice_recognition_enabled:
- voice_config = build_voice_recognition_config_from_flat_json(flat_config)
- self.logger.info(f"Voice recognition enabled - model path: {voice_config.model_config.model_path}")
- else:
- self.logger.info("Voice recognition disabled - no model path specified")
- # Debug: Log the model paths to verify they're correct
- self.logger.info(f"Wakeword model path: {wakeword_config.model_config.model_path}")
- # Check if model files exist
- from pathlib import Path
- if not Path(wakeword_config.model_config.model_path).exists():
- raise MLManagerError(f"Wakeword model file not found: {wakeword_config.model_config.model_path}")
- if voice_recognition_enabled and not Path(voice_config.model_config.model_path).exists():
- raise MLManagerError(f"Voice recognition model file not found: {voice_config.model_config.model_path}")
- # Validate configurations - validate sub-configs individually since overall
- # WakewordConfig.validate() has a design issue with AudioConfig validation
- if not wakeword_config.model_config.validate():
- raise MLManagerError(f"Invalid wakeword model configuration - model_path validation failed")
- if not wakeword_config.detection_config.validate():
- raise MLManagerError("Invalid wakeword detection configuration")
- if not wakeword_config.performance_config.validate():
- raise MLManagerError("Invalid wakeword performance configuration")
- self.logger.info(f"Wakeword config built successfully: threshold={wakeword_config.detection_config.custom_threshold}")
- if voice_recognition_enabled:
- self.logger.info(f"Voice config built successfully: threshold={voice_config.recognition_config.confidence_threshold}")
- # Initialize detectors
- # For wakeword detector, use direct constructor with config object
- self._wakeword_detector = WakewordDetector(
- config=wakeword_config,
- event_handler=self.event_handler,
- logger=self.logger
- )
- # For voice recognition detector, only initialize if enabled
- if voice_recognition_enabled:
- self._voice_recognition_detector = create_voice_recognition_detector(
- config=voice_config,
- event_handler=self.event_handler,
- audio_callback=None
- )
- else:
- self._voice_recognition_detector = None
- self.logger.info("Voice recognition detector not initialized (disabled)")
- # Apply optimizations if enabled
- if self.config.enable_optimization:
- await self._apply_optimizations()
- self.logger.info("ML Manager initialized successfully")
- except Exception as e:
- self.logger.error(f"Failed to initialize ML Manager: {e}")
- import traceback
- self.logger.error(f"Traceback: {traceback.format_exc()}")
- raise MLManagerError(f"Initialization failed: {e}")
-
- async def start(self) -> None:
- """Start ML processing."""
- if self._running:
- self.logger.warning("ML Manager is already running")
- return
-
- try:
- self.logger.info("Starting ML Manager...")
-
- # Initialize if not already done
- if not self._wakeword_detector:
- await self.initialize()
-
- # Start detectors
- self._wakeword_detector.start()
- if self._voice_recognition_detector:
- self._voice_recognition_detector.start()
- else:
- self.logger.info("Voice recognition detector is disabled, skipping start")
-
- # Set state
- self._running = True
- self._state = MLProcessingState.LISTENING
- self._start_time = time.time()
-
- # Start performance monitoring
- if self.config.enable_monitoring:
- await self._performance_monitor.start_monitoring()
-
- # Trigger system event
- if self.event_handler:
- await self._trigger_event("ml_system_started", {
- "timestamp": time.time(),
- "mode": self.config.system_mode.value
- })
-
- self.logger.info("ML Manager started successfully")
-
- except Exception as e:
- self.logger.error(f"Failed to start ML Manager: {e}")
- self._state = MLProcessingState.ERROR
- raise MLManagerError(f"Start failed: {e}")
-
- async def stop(self) -> None:
- """Stop ML processing."""
- if not self._running:
- return
-
- try:
- self.logger.info("Stopping ML Manager...")
-
- # Stop detectors
- if self._wakeword_detector:
- self._wakeword_detector.stop()
- if self._voice_recognition_detector:
- self._voice_recognition_detector.stop()
-
- # Stop performance monitoring
- if self.config.enable_monitoring:
- await self._performance_monitor.stop_monitoring()
-
- # Clear state
- self._running = False
- self._state = MLProcessingState.IDLE
- self._audio_buffer.clear()
-
- # Trigger system event
- if self.event_handler:
- uptime = time.time() - self._start_time
- await self._trigger_event("ml_system_stopped", {
- "timestamp": time.time(),
- "uptime": uptime,
- "stats": self._processing_stats.copy()
- })
-
- self.logger.info("ML Manager stopped successfully")
-
- except Exception as e:
- self.logger.error(f"Error stopping ML Manager: {e}")
-
- async def process_audio_chunk(self, audio_data: np.ndarray) -> None:
- """
- Process audio chunk through ML pipeline.
-
- Args:
- audio_data: Audio data as numpy array
- """
- if not self._running:
- return
-
- # Start performance timer
- timer_key = self._performance_monitor.start_timer("ml_manager", "process_audio_chunk")
-
- try:
- with self._processing_lock:
- # Process based on current state
- if self._state == MLProcessingState.LISTENING:
- await self._process_listening_state(audio_data)
- elif self._state == MLProcessingState.RECORDING:
- await self._process_recording_state(audio_data)
-
- # Record throughput metric
- self._performance_monitor.record_metric(
- "ml_manager", MetricType.THROUGHPUT,
- len(audio_data), "samples"
- )
-
- except Exception as e:
- self.logger.error(f"Error processing audio chunk: {e}")
- self._processing_stats["processing_errors"] += 1
- self._performance_monitor.record_metric(
- "ml_manager", MetricType.ERROR_RATE, 1, "errors"
- )
- finally:
- # End performance timer
- processing_time = self._performance_monitor.end_timer(timer_key)
- if processing_time > 0:
- self._performance_monitor.record_metric(
- "ml_manager", MetricType.LATENCY,
- processing_time, "milliseconds"
- )
-
- async def _process_listening_state(self, audio_data: np.ndarray) -> None:
- """Process audio in listening state (wakeword detection)."""
- # Feed audio to wakeword detector
- if self._wakeword_detector:
- self._wakeword_detector.process_audio_chunk(audio_data)
-
- async def _process_recording_state(self, audio_data: np.ndarray) -> None:
- """Process audio in recording state (after wakeword detected)."""
- # Add audio to buffer
- self._audio_buffer.append(audio_data)
-
- # Check for voice activity
- has_voice = self._detect_voice_activity(audio_data)
- current_time = time.time()
-
- if has_voice:
- self._last_audio_activity = current_time
- self._silence_start_time = 0.0
- else:
- if self._last_audio_activity > 0 and self._silence_start_time == 0.0:
- self._silence_start_time = current_time
-
- # Check stopping conditions
- recording_duration = current_time - self._recording_start_time
- silence_duration = (current_time - self._silence_start_time) if self._silence_start_time > 0 else 0
-
- if (recording_duration >= self.config.max_recording_seconds or
- silence_duration >= self.config.silence_timeout_seconds):
- await self._complete_recording()
-
- def _detect_voice_activity(self, audio_data: np.ndarray) -> bool:
- """Simple voice activity detection based on energy."""
- energy = np.mean(audio_data ** 2)
- threshold = 0.01 # Adjust based on requirements
- return energy > threshold
-
- async def _complete_recording(self) -> None:
- """Complete audio recording and process result."""
- try:
- # Combine audio buffer
- if not self._audio_buffer:
- self.logger.warning("No audio data recorded")
- self._reset_recording_state()
- return
-
- combined_audio = np.concatenate(self._audio_buffer)
- recording_duration = time.time() - self._recording_start_time
-
- # Process with voice recognition
- speaker_result = None
- if self._voice_recognition_detector:
- speaker_result = self._voice_recognition_detector.identify_speaker(combined_audio)
-
- # Create processing result
- result = AudioProcessingResult(
- conversation_id=None, # Will be set by server
- audio_data=combined_audio,
- speaker_id=speaker_result.speaker_id if speaker_result else None,
- speaker_name=speaker_result.speaker_name if speaker_result else None,
- confidence=speaker_result.confidence if speaker_result else 0.0,
- timestamp=time.time(),
- duration=recording_duration
- )
-
- # Trigger audio processing event
- if self.event_handler:
- await self._trigger_event("raw_audio_input_received", {
- "audio_data": result.audio_data,
- "speaker_id": result.speaker_id,
- "speaker_name": result.speaker_name,
- "confidence": result.confidence,
- "timestamp": result.timestamp,
- "duration": result.duration
- })
-
- # Send to server if in client mode
- if self.config.system_mode == MLSystemMode.CLIENT:
- await self._send_audio_to_server(result)
-
- # Update statistics
- self._processing_stats["voice_recognitions"] += 1
-
- self.logger.info(f"Audio processing completed: speaker={result.speaker_name}, confidence={result.confidence:.2f}")
-
- except Exception as e:
- self.logger.error(f"Error completing recording: {e}")
- self._processing_stats["processing_errors"] += 1
- finally:
- self._reset_recording_state()
-
- def _reset_recording_state(self) -> None:
- """Reset recording state back to listening."""
- self._audio_buffer.clear()
- self._recording_start_time = 0.0
- self._silence_start_time = 0.0
- self._last_audio_activity = 0.0
- self._state = MLProcessingState.LISTENING
-
- # Resume wakeword detection
- if self._wakeword_detector:
- self._wakeword_detector.resume()
-
- async def _handle_wakeword_event(self, detection_result: DetectionResult) -> None:
- """Handle wakeword detection event."""
- try:
- self.logger.info(f"Wakeword detected: {detection_result.wakeword_type}, confidence: {detection_result.confidence:.2f}")
-
- # Pause wakeword detection
- if self._wakeword_detector:
- self._wakeword_detector.pause()
-
- # Start recording
- self._state = MLProcessingState.RECORDING
- self._recording_start_time = time.time()
- self._last_audio_activity = self._recording_start_time
-
- # Create wakeword event
- wakeword_event = WakewordEvent(
- wakeword_type=detection_result.wakeword_type,
- confidence=detection_result.confidence,
- speaker_id=None, # Will be determined during recording
- speaker_name=None,
- timestamp=detection_result.timestamp,
- volume=getattr(detection_result, 'volume', 0.0)
- )
-
- # Trigger wakeword event
- if self.event_handler:
- await self._trigger_event("wakeword_received", {
- "wakeword_id": detection_result.wakeword_type.value,
- "wakeword_type": detection_result.wakeword_type.value,
- "confidence": detection_result.confidence,
- "timestamp": detection_result.timestamp,
- "volume": wakeword_event.volume
- })
-
- # Send to server if in client mode
- if self.config.system_mode == MLSystemMode.CLIENT:
- await self._send_wakeword_to_server(wakeword_event)
-
- # Update statistics
- self._processing_stats["wakeword_detections"] += 1
-
- except Exception as e:
- self.logger.error(f"Error handling wakeword event: {e}")
- self._processing_stats["processing_errors"] += 1
- self._reset_recording_state()
-
- async def _handle_voice_recognition_event(self, result: IdentificationResult) -> None:
- """Handle voice recognition event."""
- self.logger.info(f"Speaker identified: {result.speaker_name}, confidence: {result.confidence:.2f}")
-
- async def _send_wakeword_to_server(self, wakeword_event: WakewordEvent) -> None:
- """Send wakeword detection to server."""
- if not self._network_client:
- return
-
- try:
- # Create wakeword command for server
- # This would integrate with the network protocol
- self.logger.debug(f"Sending wakeword to server: {wakeword_event.wakeword_type}")
-
- except Exception as e:
- self.logger.error(f"Error sending wakeword to server: {e}")
-
- async def _send_audio_to_server(self, result: AudioProcessingResult) -> None:
- """Send audio recording to server."""
- if not self._network_client:
- return
-
- try:
- # Send audio data through raw audio input stream
- self.logger.debug(f"Sending audio to server: {result.duration:.2f}s")
-
- except Exception as e:
- self.logger.error(f"Error sending audio to server: {e}")
-
- def _initialize_network_client(self) -> None:
- """Initialize network client for server communication."""
- # This would initialize the network client for command socket
- # and audio streaming connections
- self.logger.info("Network client initialization placeholder")
-
- async def _apply_optimizations(self) -> None:
- """Apply performance optimizations."""
- try:
- # Model optimization
- if self._wakeword_detector and hasattr(self._wakeword_detector, 'optimize'):
- self._wakeword_detector.optimize()
-
- if self._voice_recognition_detector and hasattr(self._voice_recognition_detector, 'optimize'):
- self._voice_recognition_detector.optimize()
-
- self.logger.info("ML optimizations applied")
-
- except Exception as e:
- self.logger.error(f"Error applying optimizations: {e}")
-
- async def _trigger_event(self, event_name: str, event_data: Dict[str, Any]) -> None:
- """Trigger event through event handler."""
- if self.event_handler:
- try:
- # This would integrate with the actual event handler
- self.logger.debug(f"Triggering event: {event_name}")
-
- except Exception as e:
- self.logger.error(f"Error triggering event {event_name}: {e}")
-
- def get_state(self) -> MLProcessingState:
- """Get current processing state."""
- return self._state
-
- def get_statistics(self) -> Dict[str, Any]:
- """Get processing statistics."""
- stats = self._processing_stats.copy()
- if self._running:
- stats["uptime"] = time.time() - self._start_time
- stats["current_state"] = self._state.value
- return stats
-
- def is_running(self) -> bool:
- """Check if ML manager is running."""
- return self._running
-
- async def reload_models(self) -> None:
- """Reload ML models."""
- try:
- self.logger.info("Reloading ML models...")
-
- # Stop current processing
- was_running = self._running
- if was_running:
- await self.stop()
-
- # Reinitialize
- await self.initialize()
-
- # Restart if it was running
- if was_running:
- await self.start()
-
- self.logger.info("ML models reloaded successfully")
-
- except Exception as e:
- self.logger.error(f"Error reloading models: {e}")
- raise MLManagerError(f"Model reload failed: {e}")
-
- async def update_configuration(self, new_config: MLConfig) -> None:
- """Update ML configuration."""
- try:
- self.logger.info("Updating ML configuration...")
-
- # Validate new configuration
- if not new_config.validate():
- raise MLManagerError("Invalid configuration")
-
- # Stop current processing
- was_running = self._running
- if was_running:
- await self.stop()
-
- # Update configuration
- self.config = new_config
-
- # Reinitialize with new config
- await self.initialize()
-
- # Restart if it was running
- if was_running:
- await self.start()
-
- self.logger.info("ML configuration updated successfully")
-
- except Exception as e:
- self.logger.error(f"Error updating configuration: {e}")
- raise MLManagerError(f"Configuration update failed: {e}")
-
- def get_performance_report(self, window_hours: int = 1) -> Dict[str, Any]:
- """
- Get comprehensive performance report.
-
- Args:
- window_hours: Time window in hours for the report
-
- Returns:
- Performance report dictionary
- """
- from datetime import timedelta
- window = timedelta(hours=window_hours)
- report = self._performance_monitor.get_performance_report(window)
-
- return {
- 'timestamp': report.timestamp.isoformat(),
- 'window_hours': window_hours,
- 'system_stats': report.system_stats,
- 'component_stats': report.component_stats,
- 'active_alerts': report.alerts,
- 'recommendations': report.recommendations,
- 'ml_manager_stats': self._processing_stats.copy(),
- 'uptime_hours': (time.time() - self._start_time) / 3600 if self._start_time > 0 else 0
- }
-
- def export_performance_metrics(self, file_path: str, window_hours: int = 24) -> None:
- """
- Export performance metrics to file.
-
- Args:
- file_path: Path to export file
- window_hours: Time window in hours
- """
- from datetime import timedelta
- window = timedelta(hours=window_hours)
- self._performance_monitor.export_metrics(file_path, window)
- self.logger.info(f"Performance metrics exported to {file_path}")
-
- def get_current_metrics(self) -> Dict[str, Any]:
- """
- Get current real-time performance metrics.
-
- Returns:
- Current metrics dictionary
- """
- return {
- 'state': self._state.value,
- 'uptime_seconds': time.time() - self._start_time if self._start_time > 0 else 0,
- 'processing_stats': self._processing_stats.copy(),
- 'active_alerts': len(self._performance_monitor.alert_manager.get_active_alerts()),
- 'system_cpu': self._performance_monitor.system_monitor.get_cpu_usage(),
- 'system_memory': self._performance_monitor.system_monitor.get_memory_usage(),
- 'audio_buffer_size': len(self._audio_buffer),
- 'is_monitoring': self._performance_monitor.monitoring_active
- }
- def create_ml_manager(
- config: MLConfig,
- event_handler: Optional[Any] = None,
- app_container: Optional[Any] = None,
- logger: Optional[logging.Logger] = None
- ) -> MLManager:
- """
- Factory function to create ML Manager.
-
- Args:
- config: ML configuration
- event_handler: Event handler for triggering events
- app_container: Application container for dependency injection
- logger: Logger instance
-
- Returns:
- MLManager: Configured ML manager instance
- """
- return MLManager(config, event_handler, app_container, logger)
- def create_default_ml_config() -> MLConfig:
- """
- Create default ML configuration.
-
- Returns:
- MLConfig: Default configuration
- """
- return MLConfig(
- wakeword_model_path="./models/wakeword/default/model.pth",
- wakeword_model_password="",
- voice_recognition_model_path="./models/voice_recognition/default/model.pth",
- voice_recognition_model_password=""
- )
- def create_optimized_ml_config() -> MLConfig:
- """
- Create optimized ML configuration for production.
-
- Returns:
- MLConfig: Optimized configuration
- """
- config = create_default_ml_config()
- config.enable_optimization = True
- config.enable_monitoring = True
- config.memory_limit_mb = 256
- config.processing_timeout_seconds = 15.0
- return config
- def create_debug_ml_config() -> MLConfig:
- """
- Create debug ML configuration for development.
-
- Returns:
- MLConfig: Debug configuration
- """
- config = create_default_ml_config()
- config.debug_mode = True
- config.system_mode = MLSystemMode.DEBUG
- config.enable_monitoring = True
- return config
|