wakeword.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. """
  2. Wakeword Detection Command Classes
  3. This module implements commands for wakeword detection and management in the
  4. Trixy system. These commands handle:
  5. - Wakeword detection events from satellites
  6. - Wakeword configuration and model management
  7. - Detection sensitivity and threshold settings
  8. - Custom wakeword training and validation
  9. These commands implement the wakeword workflow specified in CLAUDE.md.
  10. """
  11. import time
  12. from typing import Optional, Dict, Any, List
  13. from dataclasses import dataclass, field
  14. from .base import TrixyCommand, CommandResponse, ValidationError, CommandStatus
  15. def pprint(message: str) -> None:
  16. """Wakeword command logging function."""
  17. print(f"[CMD.WAKEWORD] {message}")
  18. @dataclass
  19. class WakewordDetectedCommand(TrixyCommand):
  20. """
  21. Command sent when a satellite detects the wakeword.
  22. This is a critical command that triggers the conversation workflow.
  23. Contains information about the detection event and speaker.
  24. """
  25. wakeword_id: str = field(default="")
  26. speaker_id: str = field(default="")
  27. speaker_name: str = field(default="")
  28. satellite_id: str = field(default="")
  29. confidence: float = field(default=0.0)
  30. volume: float = field(default=0.0)
  31. audio_buffer_size: int = field(default=0)
  32. def validate(self) -> bool:
  33. """Validate wakeword detection command."""
  34. if not self.wakeword_id:
  35. raise ValidationError("WakewordDetectedCommand must have a wakeword_id")
  36. if not self.satellite_id:
  37. raise ValidationError("WakewordDetectedCommand must have a satellite_id")
  38. if not (0.0 <= self.confidence <= 1.0):
  39. raise ValidationError("Confidence must be between 0.0 and 1.0")
  40. if not (0.0 <= self.volume <= 1.0):
  41. raise ValidationError("Volume must be between 0.0 and 1.0")
  42. return True
  43. def get_command_name(self) -> str:
  44. """Get command name."""
  45. return "WakewordDetectedCommand"
  46. def get_response_class(self):
  47. """Expect a detection response."""
  48. return WakewordDetectedResponse
  49. def get_timeout(self) -> float:
  50. """Short timeout for wakeword detection."""
  51. return 5.0
  52. def get_priority(self) -> int:
  53. """Very high priority for wakeword detection."""
  54. return 1
  55. @dataclass
  56. class WakewordConfigCommand(TrixyCommand):
  57. """
  58. Command to configure wakeword detection settings.
  59. Used to update sensitivity, thresholds, model settings, etc.
  60. """
  61. satellite_id: str = field(default="")
  62. wakeword_models: List[str] = field(default_factory=list)
  63. sensitivity: float = field(default=0.5)
  64. threshold: float = field(default=0.5)
  65. buffer_duration: float = field(default=2.0)
  66. config_changes: Dict[str, Any] = field(default_factory=dict)
  67. def validate(self) -> bool:
  68. """Validate wakeword config command."""
  69. if not (0.0 <= self.sensitivity <= 1.0):
  70. raise ValidationError("Sensitivity must be between 0.0 and 1.0")
  71. if not (0.0 <= self.threshold <= 1.0):
  72. raise ValidationError("Threshold must be between 0.0 and 1.0")
  73. if self.buffer_duration <= 0:
  74. raise ValidationError("Buffer duration must be positive")
  75. return True
  76. def get_command_name(self) -> str:
  77. """Get command name."""
  78. return "WakewordConfigCommand"
  79. def get_response_class(self):
  80. """Expect a config response."""
  81. return WakewordConfigResponse
  82. def get_timeout(self) -> float:
  83. """Timeout for config update."""
  84. return 15.0
  85. def get_priority(self) -> int:
  86. """Normal priority for config."""
  87. return 40
  88. @dataclass
  89. class WakewordStatusCommand(TrixyCommand):
  90. """
  91. Command to request wakeword detection status.
  92. Used for monitoring and debugging wakeword detection.
  93. """
  94. satellite_id: str = field(default="")
  95. status_type: str = field(default="general") # general, models, performance, history
  96. def validate(self) -> bool:
  97. """Validate wakeword status command."""
  98. valid_types = ["general", "models", "performance", "history", "config"]
  99. if self.status_type not in valid_types:
  100. raise ValidationError(f"status_type must be one of: {valid_types}")
  101. return True
  102. def get_command_name(self) -> str:
  103. """Get command name."""
  104. return "WakewordStatusCommand"
  105. def get_timeout(self) -> float:
  106. """Timeout for status request."""
  107. return 10.0
  108. def get_priority(self) -> int:
  109. """Normal priority for status."""
  110. return 50
  111. # Response Classes
  112. @dataclass
  113. class WakewordDetectedResponse(CommandResponse):
  114. """Response to wakeword detection command."""
  115. conversation_id: str = field(default="")
  116. selected_satellite: bool = field(default=False)
  117. recording_requested: bool = field(default=False)
  118. timeout_seconds: float = field(default=10.0)
  119. def validate(self) -> bool:
  120. """Validate detection response."""
  121. if self.recording_requested and not self.conversation_id:
  122. raise ValidationError("Recording request must include conversation_id")
  123. return True
  124. def get_response_name(self) -> str:
  125. """Get response name."""
  126. return "WakewordDetectedResponse"
  127. @dataclass
  128. class WakewordConfigResponse(CommandResponse):
  129. """Response to wakeword config command."""
  130. config_applied: bool = field(default=False)
  131. applied_changes: Dict[str, Any] = field(default_factory=dict)
  132. rejected_changes: Dict[str, str] = field(default_factory=dict) # change -> reason
  133. requires_restart: bool = field(default=False)
  134. def validate(self) -> bool:
  135. """Validate config response."""
  136. return True
  137. def get_response_name(self) -> str:
  138. """Get response name."""
  139. return "WakewordConfigResponse"
  140. # Utility functions
  141. def create_wakeword_detection(
  142. wakeword_id: str,
  143. satellite_id: str,
  144. speaker_id: str = "",
  145. speaker_name: str = "",
  146. confidence: float = 0.8,
  147. volume: float = 0.5,
  148. audio_buffer_size: int = 0
  149. ) -> WakewordDetectedCommand:
  150. """
  151. Create a wakeword detection command.
  152. Args:
  153. wakeword_id: Detected wakeword identifier
  154. satellite_id: Satellite that detected the wakeword
  155. speaker_id: Identified speaker ID
  156. speaker_name: Identified speaker name
  157. confidence: Detection confidence (0.0-1.0)
  158. volume: Detection volume (0.0-1.0)
  159. audio_buffer_size: Size of audio buffer
  160. Returns:
  161. WakewordDetectedCommand: Detection command
  162. """
  163. return WakewordDetectedCommand(
  164. wakeword_id=wakeword_id,
  165. satellite_id=satellite_id,
  166. speaker_id=speaker_id,
  167. speaker_name=speaker_name,
  168. confidence=confidence,
  169. volume=volume,
  170. audio_buffer_size=audio_buffer_size
  171. )
  172. def create_wakeword_config(
  173. satellite_id: str = "",
  174. wakeword_models: Optional[List[str]] = None,
  175. sensitivity: float = 0.5,
  176. threshold: float = 0.5,
  177. buffer_duration: float = 2.0,
  178. config_changes: Optional[Dict[str, Any]] = None
  179. ) -> WakewordConfigCommand:
  180. """
  181. Create a wakeword configuration command.
  182. Args:
  183. satellite_id: Target satellite ID (empty for broadcast)
  184. wakeword_models: List of wakeword model files
  185. sensitivity: Detection sensitivity (0.0-1.0)
  186. threshold: Detection threshold (0.0-1.0)
  187. buffer_duration: Audio buffer duration in seconds
  188. config_changes: Additional configuration changes
  189. Returns:
  190. WakewordConfigCommand: Configuration command
  191. """
  192. return WakewordConfigCommand(
  193. satellite_id=satellite_id,
  194. wakeword_models=wakeword_models or [],
  195. sensitivity=sensitivity,
  196. threshold=threshold,
  197. buffer_duration=buffer_duration,
  198. config_changes=config_changes or {}
  199. )
  200. def create_detection_response(
  201. command_id: str,
  202. conversation_id: str = "",
  203. selected_satellite: bool = False,
  204. recording_requested: bool = False,
  205. timeout_seconds: float = 10.0
  206. ) -> WakewordDetectedResponse:
  207. """
  208. Create a wakeword detection response.
  209. Args:
  210. command_id: Original command ID
  211. conversation_id: Assigned conversation ID
  212. selected_satellite: Whether this satellite was selected
  213. recording_requested: Whether recording is requested
  214. timeout_seconds: Recording timeout
  215. Returns:
  216. WakewordDetectedResponse: Detection response
  217. """
  218. status = CommandStatus.SUCCESS if selected_satellite else CommandStatus.SUCCESS
  219. return WakewordDetectedResponse(
  220. command_id=command_id,
  221. status=status,
  222. conversation_id=conversation_id,
  223. selected_satellite=selected_satellite,
  224. recording_requested=recording_requested,
  225. timeout_seconds=timeout_seconds
  226. )
  227. def create_config_response(
  228. command_id: str,
  229. config_applied: bool,
  230. applied_changes: Optional[Dict[str, Any]] = None,
  231. rejected_changes: Optional[Dict[str, str]] = None,
  232. requires_restart: bool = False
  233. ) -> WakewordConfigResponse:
  234. """
  235. Create a wakeword configuration response.
  236. Args:
  237. command_id: Original command ID
  238. config_applied: Whether config was applied
  239. applied_changes: Successfully applied changes
  240. rejected_changes: Rejected changes with reasons
  241. requires_restart: Whether restart is required
  242. Returns:
  243. WakewordConfigResponse: Configuration response
  244. """
  245. status = CommandStatus.SUCCESS if config_applied else CommandStatus.ERROR
  246. return WakewordConfigResponse(
  247. command_id=command_id,
  248. status=status,
  249. config_applied=config_applied,
  250. applied_changes=applied_changes or {},
  251. rejected_changes=rejected_changes or {},
  252. requires_restart=requires_restart
  253. )
  254. # Module exports
  255. __all__ = [
  256. "WakewordDetectedCommand",
  257. "WakewordConfigCommand",
  258. "WakewordStatusCommand",
  259. "WakewordDetectedResponse",
  260. "WakewordConfigResponse",
  261. "create_wakeword_detection",
  262. "create_wakeword_config",
  263. "create_detection_response",
  264. "create_config_response",
  265. ]