workflow.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. """
  2. Complete Wakeword to Voice Recognition Workflow for Trixy ML System.
  3. This module implements the complete conversation workflow from wakeword detection
  4. through voice recognition and server communication. It orchestrates the entire
  5. process according to the specifications in project_description.md and CLAUDE.md.
  6. Key Features:
  7. - Complete wakeword detection -> voice recognition workflow
  8. - Server communication and arbitration integration
  9. - Audio buffer management during detection phases
  10. - Conversation session management
  11. - Multi-satellite arbitration support
  12. - Timeout handling and error recovery
  13. - Real-time audio streaming to server
  14. - Integration with network protocol
  15. Workflow Steps:
  16. 1. Continuous wakeword detection
  17. 2. Wakeword detected -> pause detection, start buffering
  18. 3. Enhance wakeword with speaker identification
  19. 4. Send wakeword command to server with speaker info
  20. 5. Wait for server selection (1 second arbitration window)
  21. 6. If selected: start recording and streaming audio
  22. 7. Handle silence detection and recording timeout
  23. 8. Send recording completion command
  24. 9. Return to wakeword detection mode
  25. Usage:
  26. from trixy_core.ml.workflow import MLWorkflowManager, WorkflowConfig
  27. # Create workflow manager
  28. workflow = MLWorkflowManager(
  29. ml_manager=ml_manager,
  30. network_manager=network_manager,
  31. config=workflow_config
  32. )
  33. # Start workflow
  34. workflow.start()
  35. """
  36. import threading
  37. import time
  38. import queue
  39. import logging
  40. from typing import Dict, List, Optional, Any, Callable, Union, Tuple
  41. from dataclasses import dataclass, field
  42. from datetime import datetime
  43. from enum import Enum
  44. import uuid
  45. import numpy as np
  46. # Import ML components
  47. from .ml_manager import MLManager, MLProcessingState as MLState
  48. from .wakeword.audio_buffer import AudioBuffer
  49. from .audio_pipeline import AudioPipeline, AudioChunk, AudioPipelineState as SilenceState
  50. # Import network components
  51. try:
  52. from ..network import NetworkManager, ClientSocket
  53. NETWORK_AVAILABLE = True
  54. except ImportError:
  55. NETWORK_AVAILABLE = False
  56. NetworkManager = object
  57. ClientSocket = object
  58. # Import event system
  59. try:
  60. from ..events import EventHandler, TrixyEvent, EventDataFactory
  61. EVENTS_AVAILABLE = True
  62. except ImportError:
  63. EVENTS_AVAILABLE = False
  64. EventHandler = object
  65. TrixyEvent = lambda events: lambda func: func
  66. EventDataFactory = object
  67. # Configure logging
  68. logger = logging.getLogger(__name__)
  69. class WorkflowState(Enum):
  70. """Workflow execution states."""
  71. IDLE = "idle"
  72. DETECTING = "detecting"
  73. WAKEWORD_PROCESSING = "wakeword_processing"
  74. WAITING_FOR_SERVER = "waiting_for_server"
  75. RECORDING = "recording"
  76. STREAMING = "streaming"
  77. COMPLETING = "completing"
  78. ERROR = "error"
  79. class WorkflowStep(Enum):
  80. """Individual workflow steps."""
  81. WAKEWORD_DETECTION = "wakeword_detection"
  82. SPEAKER_IDENTIFICATION = "speaker_identification"
  83. SERVER_COMMUNICATION = "server_communication"
  84. ARBITRATION_WAIT = "arbitration_wait"
  85. RECORDING_START = "recording_start"
  86. AUDIO_STREAMING = "audio_streaming"
  87. SILENCE_DETECTION = "silence_detection"
  88. RECORDING_COMPLETION = "recording_completion"
  89. CLEANUP = "cleanup"
  90. class TimeoutType(Enum):
  91. """Types of timeouts in the workflow."""
  92. SERVER_SELECTION = "server_selection"
  93. RECORDING_MAX_DURATION = "recording_max_duration"
  94. SILENCE_TIMEOUT = "silence_timeout"
  95. NETWORK_RESPONSE = "network_response"
  96. @dataclass
  97. class WorkflowConfig:
  98. """Configuration for the workflow manager."""
  99. # Timing settings
  100. server_selection_timeout: float = 10.0
  101. arbitration_window_seconds: float = 1.0
  102. max_recording_duration: float = 60.0
  103. silence_timeout_seconds: float = 3.0
  104. network_timeout_seconds: float = 5.0
  105. # Buffer settings
  106. pre_trigger_buffer_seconds: float = 1.0
  107. post_trigger_buffer_seconds: float = 1.0
  108. streaming_chunk_size: int = 4096
  109. # Quality settings
  110. min_wakeword_confidence: float = 0.7
  111. min_speaker_confidence: float = 0.5
  112. enable_quality_gates: bool = True
  113. # Recovery settings
  114. max_retry_attempts: int = 3
  115. retry_delay_seconds: float = 1.0
  116. enable_automatic_recovery: bool = True
  117. # Debug settings
  118. enable_workflow_logging: bool = True
  119. log_audio_metrics: bool = False
  120. save_debug_audio: bool = False
  121. @dataclass
  122. class ConversationSession:
  123. """Represents an active conversation session."""
  124. session_id: str
  125. start_time: float
  126. wakeword_type: str
  127. wakeword_confidence: float
  128. speaker_id: Optional[str]
  129. speaker_name: Optional[str]
  130. speaker_confidence: float
  131. audio_buffer: Optional[AudioBuffer]
  132. is_selected: bool = False
  133. is_recording: bool = False
  134. is_streaming: bool = False
  135. bytes_streamed: int = 0
  136. last_activity: float = field(default_factory=time.time)
  137. metadata: Dict[str, Any] = field(default_factory=dict)
  138. @dataclass
  139. class WorkflowMetrics:
  140. """Metrics for workflow performance monitoring."""
  141. # Workflow execution metrics
  142. total_wakewords: int = 0
  143. successful_workflows: int = 0
  144. failed_workflows: int = 0
  145. timeout_workflows: int = 0
  146. # Timing metrics
  147. avg_wakeword_to_recording_ms: float = 0.0
  148. avg_recording_duration_seconds: float = 0.0
  149. avg_silence_detection_ms: float = 0.0
  150. avg_server_response_ms: float = 0.0
  151. # Quality metrics
  152. avg_wakeword_confidence: float = 0.0
  153. avg_speaker_confidence: float = 0.0
  154. quality_gate_failures: int = 0
  155. # Network metrics
  156. server_selections: int = 0
  157. server_rejections: int = 0
  158. network_errors: int = 0
  159. bytes_transmitted: int = 0
  160. # Error tracking
  161. error_counts: Dict[str, int] = field(default_factory=dict)
  162. last_error: Optional[str] = None
  163. # Timestamp
  164. last_update: str = field(default_factory=lambda: datetime.now().isoformat())
  165. class WorkflowError(Exception):
  166. """Workflow execution errors."""
  167. pass
  168. class MLWorkflowManager:
  169. """
  170. Complete ML workflow manager implementing the full conversation flow.
  171. This class orchestrates the entire process from wakeword detection through
  172. voice recognition and server communication, handling all the steps outlined
  173. in the project specification.
  174. """
  175. def __init__(
  176. self,
  177. ml_manager: MLManager,
  178. network_manager: Optional[Any] = None,
  179. event_handler: Optional[EventHandler] = None,
  180. config: Optional[WorkflowConfig] = None
  181. ):
  182. """
  183. Initialize workflow manager.
  184. Args:
  185. ml_manager: ML manager instance
  186. network_manager: Network manager for server communication
  187. event_handler: Event handler for system integration
  188. config: Workflow configuration
  189. """
  190. self.ml_manager = ml_manager
  191. self.network_manager = network_manager
  192. self.event_handler = event_handler
  193. self.config = config or WorkflowConfig()
  194. # Workflow state
  195. self._state = WorkflowState.IDLE
  196. self._current_step = WorkflowStep.WAKEWORD_DETECTION
  197. self._state_lock = threading.RLock()
  198. # Active session
  199. self._current_session: Optional[ConversationSession] = None
  200. self._session_lock = threading.RLock()
  201. # Workflow control
  202. self._is_running = False
  203. self._workflow_thread: Optional[threading.Thread] = None
  204. self._stop_event = threading.Event()
  205. # Timeout management
  206. self._active_timeouts: Dict[str, threading.Timer] = {}
  207. self._timeout_lock = threading.RLock()
  208. # Metrics
  209. self._metrics = WorkflowMetrics()
  210. self._start_time = time.time()
  211. # Audio streaming
  212. self._streaming_queue = queue.Queue()
  213. self._streaming_thread: Optional[threading.Thread] = None
  214. # Callbacks
  215. self.on_workflow_started: Optional[Callable] = None
  216. self.on_workflow_completed: Optional[Callable] = None
  217. self.on_workflow_failed: Optional[Callable] = None
  218. self.on_session_created: Optional[Callable] = None
  219. self.on_recording_started: Optional[Callable] = None
  220. self.on_recording_completed: Optional[Callable] = None
  221. logger.info("ML Workflow Manager initialized")
  222. # Set up ML manager callbacks
  223. self._setup_ml_callbacks()
  224. # Register event handlers
  225. if EVENTS_AVAILABLE and self.event_handler:
  226. self._register_event_handlers()
  227. def start(self) -> None:
  228. """Start the workflow manager."""
  229. if self._is_running:
  230. logger.warning("Workflow manager already running")
  231. return
  232. logger.info("Starting ML workflow manager...")
  233. self._is_running = True
  234. self._stop_event.clear()
  235. try:
  236. # Start workflow monitoring thread
  237. self._workflow_thread = threading.Thread(
  238. target=self._workflow_monitoring_loop,
  239. daemon=True,
  240. name="MLWorkflow-Monitor"
  241. )
  242. self._workflow_thread.start()
  243. # Start streaming thread
  244. self._streaming_thread = threading.Thread(
  245. target=self._audio_streaming_loop,
  246. daemon=True,
  247. name="MLWorkflow-Streaming"
  248. )
  249. self._streaming_thread.start()
  250. # Set initial state
  251. with self._state_lock:
  252. self._state = WorkflowState.DETECTING
  253. self._current_step = WorkflowStep.WAKEWORD_DETECTION
  254. # Trigger started callback
  255. if self.on_workflow_started:
  256. self.on_workflow_started()
  257. logger.info("ML workflow manager started successfully")
  258. except Exception as e:
  259. self._is_running = False
  260. logger.error(f"Failed to start workflow manager: {e}")
  261. raise WorkflowError(f"Failed to start workflow manager: {e}") from e
  262. def stop(self) -> None:
  263. """Stop the workflow manager."""
  264. if not self._is_running:
  265. return
  266. logger.info("Stopping ML workflow manager...")
  267. self._is_running = False
  268. self._stop_event.set()
  269. try:
  270. # Cancel any active timeouts
  271. self._cancel_all_timeouts()
  272. # Complete current session if active
  273. with self._session_lock:
  274. if self._current_session:
  275. self._complete_session("workflow_stopped")
  276. # Stop monitoring thread
  277. if self._workflow_thread and self._workflow_thread.is_alive():
  278. self._workflow_thread.join(timeout=5.0)
  279. # Stop streaming thread
  280. if self._streaming_thread and self._streaming_thread.is_alive():
  281. self._streaming_thread.join(timeout=2.0)
  282. with self._state_lock:
  283. self._state = WorkflowState.IDLE
  284. logger.info("ML workflow manager stopped")
  285. except Exception as e:
  286. logger.error(f"Error stopping workflow manager: {e}")
  287. def get_current_state(self) -> WorkflowState:
  288. """Get current workflow state."""
  289. with self._state_lock:
  290. return self._state
  291. def get_current_session(self) -> Optional[ConversationSession]:
  292. """Get current conversation session."""
  293. with self._session_lock:
  294. return self._current_session
  295. def get_metrics(self) -> WorkflowMetrics:
  296. """Get workflow metrics."""
  297. self._metrics.last_update = datetime.now().isoformat()
  298. return self._metrics
  299. def handle_server_selection_response(self, session_id: str, selected: bool) -> None:
  300. """
  301. Handle server selection response.
  302. Args:
  303. session_id: Session ID from server
  304. selected: Whether this client was selected
  305. """
  306. logger.info(f"Received server selection: session_id={session_id}, selected={selected}")
  307. with self._session_lock:
  308. if not self._current_session or self._current_session.session_id != session_id:
  309. logger.warning(f"Received selection for unknown session: {session_id}")
  310. return
  311. # Cancel selection timeout
  312. self._cancel_timeout("server_selection")
  313. if selected:
  314. # We were selected - start recording
  315. self._current_session.is_selected = True
  316. self._metrics.server_selections += 1
  317. self._start_recording_phase()
  318. else:
  319. # We were not selected - clean up and return to detection
  320. self._metrics.server_rejections += 1
  321. self._complete_session("not_selected")
  322. def _setup_ml_callbacks(self) -> None:
  323. """Set up callbacks for ML manager events."""
  324. # Set wakeword detection callback
  325. self.ml_manager.on_wakeword_detected = self._handle_wakeword_detected
  326. # Set speaker identification callback
  327. self.ml_manager.on_speaker_identified = self._handle_speaker_identified
  328. # Set audio ready callback
  329. self.ml_manager.on_audio_ready = self._handle_audio_ready
  330. # Set error callback
  331. self.ml_manager.on_error = self._handle_ml_error
  332. def _handle_wakeword_detected(self, detection_result, speaker_id: Optional[str], speaker_name: Optional[str]) -> None:
  333. """Handle wakeword detection from ML manager."""
  334. logger.info(f"Workflow handling wakeword detection: {detection_result.wakeword_type.value}")
  335. with self._state_lock:
  336. if self._state != WorkflowState.DETECTING:
  337. logger.warning("Ignoring wakeword - not in detecting state")
  338. return
  339. self._state = WorkflowState.WAKEWORD_PROCESSING
  340. self._current_step = WorkflowStep.SPEAKER_IDENTIFICATION
  341. # Update metrics
  342. self._metrics.total_wakewords += 1
  343. self._metrics.avg_wakeword_confidence = (
  344. (self._metrics.avg_wakeword_confidence * (self._metrics.total_wakewords - 1) +
  345. detection_result.confidence) / self._metrics.total_wakewords
  346. )
  347. # Check quality gates
  348. if self.config.enable_quality_gates:
  349. if detection_result.confidence < self.config.min_wakeword_confidence:
  350. logger.warning(f"Wakeword confidence too low: {detection_result.confidence}")
  351. self._metrics.quality_gate_failures += 1
  352. self._return_to_detection("quality_gate_failure")
  353. return
  354. # Create conversation session
  355. session = ConversationSession(
  356. session_id=str(uuid.uuid4()),
  357. start_time=time.time(),
  358. wakeword_type=detection_result.wakeword_type.value,
  359. wakeword_confidence=detection_result.confidence,
  360. speaker_id=speaker_id,
  361. speaker_name=speaker_name,
  362. speaker_confidence=0.0, # Will be updated by speaker identification
  363. audio_buffer=None
  364. )
  365. with self._session_lock:
  366. self._current_session = session
  367. # Trigger session created callback
  368. if self.on_session_created:
  369. try:
  370. self.on_session_created(session)
  371. except Exception as e:
  372. logger.error(f"Error in session created callback: {e}")
  373. # Continue with workflow
  374. self._continue_wakeword_workflow(session, detection_result)
  375. def _continue_wakeword_workflow(self, session: ConversationSession, detection_result) -> None:
  376. """Continue workflow after wakeword detection."""
  377. start_time = time.time()
  378. try:
  379. # Step 1: Speaker identification (already done in ML manager)
  380. if session.speaker_id:
  381. # Update metrics
  382. if hasattr(detection_result, 'speaker_confidence'):
  383. session.speaker_confidence = detection_result.speaker_confidence
  384. self._metrics.avg_speaker_confidence = (
  385. (self._metrics.avg_speaker_confidence * self._metrics.total_wakewords +
  386. session.speaker_confidence) / (self._metrics.total_wakewords + 1)
  387. )
  388. # Check speaker confidence quality gate
  389. if self.config.enable_quality_gates:
  390. if session.speaker_confidence < self.config.min_speaker_confidence:
  391. logger.warning(f"Speaker confidence too low: {session.speaker_confidence}")
  392. self._metrics.quality_gate_failures += 1
  393. self._complete_session("speaker_quality_gate_failure")
  394. return
  395. # Step 2: Send wakeword command to server
  396. with self._state_lock:
  397. self._current_step = WorkflowStep.SERVER_COMMUNICATION
  398. self._send_wakeword_command(session)
  399. # Step 3: Wait for server selection
  400. with self._state_lock:
  401. self._state = WorkflowState.WAITING_FOR_SERVER
  402. self._current_step = WorkflowStep.ARBITRATION_WAIT
  403. # Set timeout for server selection
  404. self._set_timeout(
  405. "server_selection",
  406. self.config.server_selection_timeout,
  407. lambda: self._handle_timeout(TimeoutType.SERVER_SELECTION)
  408. )
  409. # Update timing metrics
  410. processing_time = (time.time() - start_time) * 1000
  411. self._metrics.avg_wakeword_to_recording_ms = (
  412. (self._metrics.avg_wakeword_to_recording_ms * (self._metrics.total_wakewords - 1) +
  413. processing_time) / self._metrics.total_wakewords
  414. )
  415. except Exception as e:
  416. logger.error(f"Error in wakeword workflow: {e}")
  417. self._handle_workflow_error(f"Wakeword workflow error: {e}")
  418. def _send_wakeword_command(self, session: ConversationSession) -> None:
  419. """Send wakeword detected command to server."""
  420. if not self.network_manager:
  421. logger.warning("No network manager - cannot send wakeword command")
  422. return
  423. try:
  424. # Prepare command data
  425. command_data = {
  426. 'command': 'wakeword_detected',
  427. 'session_id': session.session_id,
  428. 'wakeword_type': session.wakeword_type,
  429. 'wakeword_confidence': session.wakeword_confidence,
  430. 'speaker_id': session.speaker_id,
  431. 'speaker_name': session.speaker_name,
  432. 'speaker_confidence': session.speaker_confidence,
  433. 'timestamp': datetime.now().isoformat(),
  434. 'client_id': getattr(self.network_manager, 'client_id', 'unknown')
  435. }
  436. # Send command via network manager
  437. # In a real implementation, this would use the actual Trixy protocol
  438. logger.info(f"Sending wakeword command: {command_data}")
  439. # Simulate network send for now
  440. # self.network_manager.send_command(command_data)
  441. except Exception as e:
  442. logger.error(f"Error sending wakeword command: {e}")
  443. self._metrics.network_errors += 1
  444. raise
  445. def _start_recording_phase(self) -> None:
  446. """Start the recording phase after server selection."""
  447. logger.info("Starting recording phase")
  448. with self._state_lock:
  449. self._state = WorkflowState.RECORDING
  450. self._current_step = WorkflowStep.RECORDING_START
  451. with self._session_lock:
  452. if not self._current_session:
  453. logger.error("No active session for recording")
  454. return
  455. self._current_session.is_recording = True
  456. self._current_session.last_activity = time.time()
  457. # Set recording timeout
  458. self._set_timeout(
  459. "recording_duration",
  460. self.config.max_recording_duration,
  461. lambda: self._handle_timeout(TimeoutType.RECORDING_MAX_DURATION)
  462. )
  463. # Start recording in ML manager (this would start the audio buffer)
  464. # In practice, the ML manager would handle the actual recording
  465. logger.info("Recording started - audio buffer active")
  466. # Trigger recording started callback
  467. if self.on_recording_started:
  468. try:
  469. self.on_recording_started(self._current_session)
  470. except Exception as e:
  471. logger.error(f"Error in recording started callback: {e}")
  472. # Transition to streaming
  473. self._start_streaming_phase()
  474. def _start_streaming_phase(self) -> None:
  475. """Start streaming audio to server."""
  476. logger.info("Starting audio streaming phase")
  477. with self._state_lock:
  478. self._state = WorkflowState.STREAMING
  479. self._current_step = WorkflowStep.AUDIO_STREAMING
  480. with self._session_lock:
  481. if self._current_session:
  482. self._current_session.is_streaming = True
  483. # In a real implementation, this would start streaming audio chunks
  484. # to the server via the raw audio input socket
  485. logger.info("Audio streaming to server started")
  486. def _handle_audio_ready(self, audio_data: np.ndarray, conversation_id: str) -> None:
  487. """Handle audio ready for streaming."""
  488. if not self._current_session or self._current_session.session_id != conversation_id:
  489. return
  490. # Add audio to streaming queue
  491. try:
  492. audio_chunk = {
  493. 'data': audio_data,
  494. 'session_id': conversation_id,
  495. 'timestamp': time.time(),
  496. 'sequence': getattr(self, '_stream_sequence', 0)
  497. }
  498. if not self._streaming_queue.full():
  499. self._streaming_queue.put(audio_chunk)
  500. self._stream_sequence = getattr(self, '_stream_sequence', 0) + 1
  501. else:
  502. logger.warning("Streaming queue full - dropping audio chunk")
  503. except Exception as e:
  504. logger.error(f"Error queuing audio for streaming: {e}")
  505. def _audio_streaming_loop(self) -> None:
  506. """Audio streaming loop for sending data to server."""
  507. logger.debug("Audio streaming loop started")
  508. while not self._stop_event.is_set():
  509. try:
  510. # Get audio chunk from queue
  511. try:
  512. chunk = self._streaming_queue.get(timeout=0.1)
  513. except queue.Empty:
  514. continue
  515. # Stream to server
  516. self._stream_audio_chunk(chunk)
  517. # Update metrics
  518. with self._session_lock:
  519. if self._current_session:
  520. self._current_session.bytes_streamed += len(chunk['data']) * 4 # float32
  521. self._metrics.bytes_transmitted += len(chunk['data']) * 4
  522. # Mark task done
  523. self._streaming_queue.task_done()
  524. except Exception as e:
  525. logger.error(f"Error in audio streaming loop: {e}")
  526. time.sleep(0.1)
  527. logger.debug("Audio streaming loop stopped")
  528. def _stream_audio_chunk(self, chunk: Dict[str, Any]) -> None:
  529. """Stream individual audio chunk to server."""
  530. try:
  531. # In a real implementation, this would send the audio data
  532. # through the raw audio input socket to the server
  533. logger.debug(f"Streaming audio chunk: {len(chunk['data'])} samples")
  534. # Simulate network transmission
  535. # self.network_manager.stream_audio(chunk['data'])
  536. except Exception as e:
  537. logger.error(f"Error streaming audio chunk: {e}")
  538. self._metrics.network_errors += 1
  539. def _handle_silence_timeout(self) -> None:
  540. """Handle silence timeout during recording."""
  541. logger.info("Silence timeout detected - completing recording")
  542. with self._session_lock:
  543. if self._current_session:
  544. self._complete_recording("silence_timeout")
  545. def _complete_recording(self, reason: str) -> None:
  546. """Complete the recording phase."""
  547. logger.info(f"Completing recording: {reason}")
  548. with self._state_lock:
  549. self._state = WorkflowState.COMPLETING
  550. self._current_step = WorkflowStep.RECORDING_COMPLETION
  551. # Cancel recording timeout
  552. self._cancel_timeout("recording_duration")
  553. with self._session_lock:
  554. if not self._current_session:
  555. return
  556. recording_duration = time.time() - self._current_session.start_time
  557. self._current_session.is_recording = False
  558. self._current_session.is_streaming = False
  559. # Send recording completion command to server
  560. self._send_recording_completion_command(reason)
  561. # Update metrics
  562. self._metrics.avg_recording_duration_seconds = (
  563. (self._metrics.avg_recording_duration_seconds * self._metrics.successful_workflows +
  564. recording_duration) / (self._metrics.successful_workflows + 1)
  565. )
  566. # Trigger recording completed callback
  567. if self.on_recording_completed:
  568. try:
  569. self.on_recording_completed(self._current_session, reason)
  570. except Exception as e:
  571. logger.error(f"Error in recording completed callback: {e}")
  572. # Complete the session
  573. self._complete_session(reason)
  574. def _send_recording_completion_command(self, reason: str) -> None:
  575. """Send recording completion command to server."""
  576. if not self.network_manager or not self._current_session:
  577. return
  578. try:
  579. command_data = {
  580. 'command': 'recording_completed',
  581. 'session_id': self._current_session.session_id,
  582. 'reason': reason,
  583. 'duration_seconds': time.time() - self._current_session.start_time,
  584. 'bytes_streamed': self._current_session.bytes_streamed,
  585. 'timestamp': datetime.now().isoformat()
  586. }
  587. logger.info(f"Sending recording completion command: {command_data}")
  588. # In real implementation: self.network_manager.send_command(command_data)
  589. except Exception as e:
  590. logger.error(f"Error sending recording completion command: {e}")
  591. self._metrics.network_errors += 1
  592. def _complete_session(self, reason: str) -> None:
  593. """Complete the current conversation session."""
  594. logger.info(f"Completing session: {reason}")
  595. with self._session_lock:
  596. if not self._current_session:
  597. return
  598. session = self._current_session
  599. self._current_session = None
  600. # Update metrics based on completion reason
  601. if reason in ["silence_timeout", "recording_completed"]:
  602. self._metrics.successful_workflows += 1
  603. elif "timeout" in reason:
  604. self._metrics.timeout_workflows += 1
  605. else:
  606. self._metrics.failed_workflows += 1
  607. # Trigger workflow completed callback
  608. if self.on_workflow_completed:
  609. try:
  610. self.on_workflow_completed(session, reason)
  611. except Exception as e:
  612. logger.error(f"Error in workflow completed callback: {e}")
  613. # Return to detection mode
  614. self._return_to_detection(reason)
  615. def _return_to_detection(self, reason: str) -> None:
  616. """Return to wakeword detection mode."""
  617. logger.info(f"Returning to detection mode: {reason}")
  618. # Cancel all timeouts
  619. self._cancel_all_timeouts()
  620. # Clear streaming queue
  621. while not self._streaming_queue.empty():
  622. try:
  623. self._streaming_queue.get_nowait()
  624. except queue.Empty:
  625. break
  626. with self._state_lock:
  627. self._state = WorkflowState.DETECTING
  628. self._current_step = WorkflowStep.WAKEWORD_DETECTION
  629. logger.info("Returned to wakeword detection mode")
  630. def _handle_timeout(self, timeout_type: TimeoutType) -> None:
  631. """Handle workflow timeouts."""
  632. logger.warning(f"Workflow timeout: {timeout_type.value}")
  633. if timeout_type == TimeoutType.SERVER_SELECTION:
  634. self._complete_session("server_selection_timeout")
  635. elif timeout_type == TimeoutType.RECORDING_MAX_DURATION:
  636. self._complete_recording("max_duration_timeout")
  637. elif timeout_type == TimeoutType.SILENCE_TIMEOUT:
  638. self._handle_silence_timeout()
  639. # Update timeout metrics
  640. self._metrics.timeout_workflows += 1
  641. def _handle_workflow_error(self, error_message: str) -> None:
  642. """Handle workflow errors."""
  643. logger.error(f"Workflow error: {error_message}")
  644. # Update error metrics
  645. error_type = error_message.split(':')[0] if ':' in error_message else "unknown"
  646. self._metrics.error_counts[error_type] = self._metrics.error_counts.get(error_type, 0) + 1
  647. self._metrics.last_error = error_message
  648. # Trigger workflow failed callback
  649. if self.on_workflow_failed:
  650. try:
  651. self.on_workflow_failed(error_message)
  652. except Exception as e:
  653. logger.error(f"Error in workflow failed callback: {e}")
  654. # Complete current session if active
  655. with self._session_lock:
  656. if self._current_session:
  657. self._complete_session(f"error: {error_message}")
  658. else:
  659. self._return_to_detection(f"error: {error_message}")
  660. def _handle_speaker_identified(self, result) -> None:
  661. """Handle continuous speaker identification."""
  662. # This is for continuous recognition mode
  663. if self._state == WorkflowState.DETECTING and self.config.enable_workflow_logging:
  664. logger.debug(f"Continuous speaker identification: {result.speaker_name}")
  665. def _handle_ml_error(self, error_message: str) -> None:
  666. """Handle ML manager errors."""
  667. self._handle_workflow_error(f"ML error: {error_message}")
  668. def _set_timeout(self, name: str, delay: float, callback: Callable) -> None:
  669. """Set a named timeout."""
  670. with self._timeout_lock:
  671. # Cancel existing timeout with same name
  672. if name in self._active_timeouts:
  673. self._active_timeouts[name].cancel()
  674. # Create new timeout
  675. timer = threading.Timer(delay, callback)
  676. timer.start()
  677. self._active_timeouts[name] = timer
  678. logger.debug(f"Set timeout '{name}' for {delay} seconds")
  679. def _cancel_timeout(self, name: str) -> None:
  680. """Cancel a named timeout."""
  681. with self._timeout_lock:
  682. if name in self._active_timeouts:
  683. self._active_timeouts[name].cancel()
  684. del self._active_timeouts[name]
  685. logger.debug(f"Cancelled timeout '{name}'")
  686. def _cancel_all_timeouts(self) -> None:
  687. """Cancel all active timeouts."""
  688. with self._timeout_lock:
  689. for name, timer in self._active_timeouts.items():
  690. timer.cancel()
  691. logger.debug(f"Cancelled timeout '{name}'")
  692. self._active_timeouts.clear()
  693. def _workflow_monitoring_loop(self) -> None:
  694. """Background monitoring loop for workflow health."""
  695. logger.debug("Workflow monitoring loop started")
  696. while not self._stop_event.is_set():
  697. try:
  698. # Check for stuck states
  699. current_time = time.time()
  700. with self._session_lock:
  701. if self._current_session:
  702. session_age = current_time - self._current_session.start_time
  703. activity_age = current_time - self._current_session.last_activity
  704. # Check for session timeout
  705. if session_age > 300: # 5 minutes
  706. logger.warning("Session timeout - completing session")
  707. self._complete_session("session_timeout")
  708. # Check for activity timeout
  709. elif activity_age > 60: # 1 minute
  710. logger.warning("Activity timeout - completing session")
  711. self._complete_session("activity_timeout")
  712. # Sleep for monitoring interval
  713. self._stop_event.wait(5.0)
  714. except Exception as e:
  715. logger.error(f"Error in workflow monitoring: {e}")
  716. time.sleep(5.0)
  717. logger.debug("Workflow monitoring loop stopped")
  718. def _register_event_handlers(self) -> None:
  719. """Register event handlers with the event system."""
  720. if not EVENTS_AVAILABLE or not self.event_handler:
  721. return
  722. try:
  723. # In a real implementation, these would be proper event handler methods
  724. # with @TrixyEvent decorators for handling server responses
  725. logger.info("Event handlers registered for workflow manager")
  726. except Exception as e:
  727. logger.error(f"Error registering event handlers: {e}")
  728. def create_workflow_manager(
  729. ml_manager: MLManager,
  730. network_manager: Optional[Any] = None,
  731. event_handler: Optional[EventHandler] = None,
  732. config: Optional[WorkflowConfig] = None
  733. ) -> MLWorkflowManager:
  734. """
  735. Factory function to create a workflow manager.
  736. Args:
  737. ml_manager: ML manager instance
  738. network_manager: Network manager for server communication
  739. event_handler: Event handler for system integration
  740. config: Workflow configuration
  741. Returns:
  742. Configured workflow manager
  743. """
  744. return MLWorkflowManager(
  745. ml_manager=ml_manager,
  746. network_manager=network_manager,
  747. event_handler=event_handler,
  748. config=config
  749. )
  750. def create_default_workflow_config(**kwargs) -> WorkflowConfig:
  751. """
  752. Create default workflow configuration.
  753. Args:
  754. **kwargs: Configuration overrides
  755. Returns:
  756. Workflow configuration
  757. """
  758. return WorkflowConfig(**kwargs)
  759. # Export main classes and functions
  760. __all__ = [
  761. 'MLWorkflowManager',
  762. 'WorkflowConfig',
  763. 'ConversationSession',
  764. 'WorkflowMetrics',
  765. 'WorkflowState',
  766. 'WorkflowStep',
  767. 'TimeoutType',
  768. 'WorkflowError',
  769. 'create_workflow_manager',
  770. 'create_default_workflow_config'
  771. ]