config_models.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. """
  2. Configuration Data Models for Trixy Application
  3. This module defines type-safe configuration data structures for all deployment modes:
  4. - ServerConfig: Configuration for server mode
  5. - ClientConfig: Configuration for client/satellite mode
  6. - StandaloneConfig: Configuration for standalone mode
  7. Each configuration class provides:
  8. - Type-safe attribute access
  9. - Validation methods
  10. - Serialization/deserialization
  11. - Environment variable override support
  12. - Thread-safe access patterns
  13. """
  14. from dataclasses import dataclass, field, asdict
  15. from typing import Dict, List, Optional, Union, Any, Set
  16. from pathlib import Path
  17. import os
  18. from enum import Enum
  19. class LogLevel(Enum):
  20. """Logging levels for the application."""
  21. DEBUG = "debug"
  22. INFO = "info"
  23. WARNING = "warning"
  24. ERROR = "error"
  25. CRITICAL = "critical"
  26. class DeploymentMode(Enum):
  27. """Deployment modes for the Trixy application."""
  28. SERVER = "server"
  29. CLIENT = "client"
  30. STANDALONE = "standalone"
  31. @dataclass
  32. class NetworkConfig:
  33. """Network configuration for sockets and connections."""
  34. # Command socket configuration
  35. command_port: int = 2101
  36. command_bind_address: str = "0.0.0.0"
  37. command_max_connections: int = 50
  38. command_timeout: int = 30
  39. command_buffer_size: int = 8192
  40. # Audio stream ports
  41. raw_audio_input_port: int = 2102
  42. raw_audio_output_port: int = 2103
  43. raw_music_output_port: int = 2104
  44. # Audio stream configuration
  45. audio_bind_address: str = "0.0.0.0"
  46. audio_timeout: int = 10
  47. audio_buffer_size: int = 16384
  48. # Connection settings
  49. heartbeat_interval: int = 30
  50. max_retry_attempts: int = 5
  51. retry_delay: int = 5
  52. connection_timeout: int = 10
  53. # Security settings
  54. enable_encryption: bool = False
  55. encryption_key_file: Optional[str] = None
  56. enable_compression: bool = True
  57. compression_level: int = 6
  58. def validate(self) -> List[str]:
  59. """Validate network configuration and return any errors."""
  60. errors = []
  61. # Port validation
  62. ports = [
  63. self.command_port,
  64. self.raw_audio_input_port,
  65. self.raw_audio_output_port,
  66. self.raw_music_output_port
  67. ]
  68. for port in ports:
  69. if not (1024 <= port <= 65535):
  70. errors.append(f"Port {port} must be between 1024 and 65535")
  71. # Check for port conflicts
  72. if len(set(ports)) != len(ports):
  73. errors.append("Network ports must be unique")
  74. # Validate addresses
  75. if not self.command_bind_address:
  76. errors.append("Command bind address cannot be empty")
  77. if not self.audio_bind_address:
  78. errors.append("Audio bind address cannot be empty")
  79. # Validate timeouts and limits
  80. if self.command_timeout <= 0:
  81. errors.append("Command timeout must be positive")
  82. if self.audio_timeout <= 0:
  83. errors.append("Audio timeout must be positive")
  84. if self.command_max_connections <= 0:
  85. errors.append("Max connections must be positive")
  86. return errors
  87. @dataclass
  88. class AudioConfig:
  89. """Audio processing configuration."""
  90. # Audio format settings
  91. sample_rate: int = 16000
  92. channels: int = 1 # Mono
  93. bit_depth: int = 16
  94. frame_size: int = 1024
  95. # Recording settings
  96. max_recording_duration: int = 60 # seconds
  97. silence_threshold: float = 0.01
  98. silence_duration: float = 3.0 # seconds
  99. pre_recording_buffer: float = 1.0 # seconds
  100. # Audio processing
  101. enable_noise_reduction: bool = True
  102. enable_auto_gain: bool = True
  103. gain_level: float = 1.0
  104. # VAD (Voice Activity Detection) settings
  105. vad_enabled: bool = True
  106. vad_aggressiveness: int = 2 # 0-3, higher = more aggressive
  107. vad_frame_duration: int = 30 # milliseconds
  108. # Music playback settings
  109. music_sample_rate: int = 48000
  110. music_channels: int = 2 # Stereo
  111. music_bit_depth: int = 16
  112. def validate(self) -> List[str]:
  113. """Validate audio configuration and return any errors."""
  114. errors = []
  115. # Sample rate validation
  116. valid_sample_rates = [8000, 16000, 22050, 44100, 48000]
  117. if self.sample_rate not in valid_sample_rates:
  118. errors.append(f"Sample rate must be one of {valid_sample_rates}")
  119. if self.music_sample_rate not in valid_sample_rates:
  120. errors.append(f"Music sample rate must be one of {valid_sample_rates}")
  121. # Channels validation
  122. if self.channels not in [1, 2]:
  123. errors.append("Channels must be 1 (mono) or 2 (stereo)")
  124. if self.music_channels not in [1, 2]:
  125. errors.append("Music channels must be 1 (mono) or 2 (stereo)")
  126. # Bit depth validation
  127. if self.bit_depth not in [16, 24, 32]:
  128. errors.append("Bit depth must be 16, 24, or 32")
  129. if self.music_bit_depth not in [16, 24, 32]:
  130. errors.append("Music bit depth must be 16, 24, or 32")
  131. # Duration and threshold validation
  132. if self.max_recording_duration <= 0:
  133. errors.append("Max recording duration must be positive")
  134. if not (0.0 <= self.silence_threshold <= 1.0):
  135. errors.append("Silence threshold must be between 0.0 and 1.0")
  136. if self.silence_duration <= 0:
  137. errors.append("Silence duration must be positive")
  138. if self.pre_recording_buffer < 0:
  139. errors.append("Pre-recording buffer must be non-negative")
  140. # VAD validation
  141. if not (0 <= self.vad_aggressiveness <= 3):
  142. errors.append("VAD aggressiveness must be between 0 and 3")
  143. if self.vad_frame_duration not in [10, 20, 30]:
  144. errors.append("VAD frame duration must be 10, 20, or 30 ms")
  145. return errors
  146. @dataclass
  147. class MLConfig:
  148. """Machine Learning model configuration."""
  149. # Model paths
  150. wakeword_model_path: str = "models/wakeword/default.pth"
  151. voice_recognition_model_path: str = "models/voice_recognition/default.pth"
  152. # Model settings
  153. wakeword_threshold: float = 0.8
  154. voice_recognition_threshold: float = 0.7
  155. model_cache_size: int = 3
  156. # Processing settings
  157. enable_gpu: bool = True
  158. gpu_device_id: int = 0
  159. batch_size: int = 1
  160. max_inference_time: float = 5.0 # seconds
  161. # Training settings
  162. training_data_path: str = "trainer/data"
  163. model_output_path: str = "models"
  164. training_batch_size: int = 32
  165. training_epochs: int = 100
  166. learning_rate: float = 0.001
  167. def validate(self) -> List[str]:
  168. """Validate ML configuration and return any errors."""
  169. errors = []
  170. # Threshold validation
  171. if not (0.0 <= self.wakeword_threshold <= 1.0):
  172. errors.append("Wakeword threshold must be between 0.0 and 1.0")
  173. if not (0.0 <= self.voice_recognition_threshold <= 1.0):
  174. errors.append("Voice recognition threshold must be between 0.0 and 1.0")
  175. # Hardware validation
  176. if self.gpu_device_id < 0:
  177. errors.append("GPU device ID must be non-negative")
  178. if self.batch_size <= 0:
  179. errors.append("Batch size must be positive")
  180. if self.max_inference_time <= 0:
  181. errors.append("Max inference time must be positive")
  182. # Training validation
  183. if self.training_batch_size <= 0:
  184. errors.append("Training batch size must be positive")
  185. if self.training_epochs <= 0:
  186. errors.append("Training epochs must be positive")
  187. if self.learning_rate <= 0:
  188. errors.append("Learning rate must be positive")
  189. # Path validation (basic existence check will be done by validator)
  190. if not self.wakeword_model_path:
  191. errors.append("Wakeword model path cannot be empty")
  192. # Voice recognition model path is optional - if not provided, voice recognition will be disabled
  193. return errors
  194. @dataclass
  195. class PluginConfig:
  196. """Plugin system configuration."""
  197. # Plugin paths and loading
  198. plugin_directories: List[str] = field(default_factory=lambda: ["plugins"])
  199. auto_load_plugins: bool = True
  200. plugin_timeout: int = 30 # seconds
  201. max_plugin_memory: int = 512 # MB
  202. # Plugin security
  203. enable_plugin_sandboxing: bool = True
  204. allowed_plugin_apis: Set[str] = field(default_factory=lambda: {
  205. "event_handler", "satellite_manager", "asset_manager",
  206. "config_manager", "scheduler"
  207. })
  208. # Plugin management
  209. enable_hot_reload: bool = True
  210. reload_check_interval: int = 5 # seconds
  211. max_reload_attempts: int = 3
  212. def validate(self) -> List[str]:
  213. """Validate plugin configuration and return any errors."""
  214. errors = []
  215. if not self.plugin_directories:
  216. errors.append("At least one plugin directory must be specified")
  217. if self.plugin_timeout <= 0:
  218. errors.append("Plugin timeout must be positive")
  219. if self.max_plugin_memory <= 0:
  220. errors.append("Max plugin memory must be positive")
  221. if self.reload_check_interval <= 0:
  222. errors.append("Reload check interval must be positive")
  223. if self.max_reload_attempts <= 0:
  224. errors.append("Max reload attempts must be positive")
  225. return errors
  226. @dataclass
  227. class SatelliteConfig:
  228. """Satellite/client specific configuration."""
  229. # Connection settings
  230. server_host: str = "localhost"
  231. server_port: int = 2101
  232. auto_reconnect: bool = True
  233. reconnect_interval: int = 5 # seconds
  234. # Satellite identification
  235. satellite_id: Optional[str] = None
  236. room_name: str = "default"
  237. alias_name: str = "trixy-satellite"
  238. mac_address: Optional[str] = None
  239. # Local processing
  240. local_wakeword_processing: bool = True
  241. local_voice_recognition: bool = False
  242. buffer_audio_locally: bool = True
  243. # Audio streaming
  244. stream_quality: str = "high" # low, medium, high
  245. compression_enabled: bool = True
  246. def validate(self) -> List[str]:
  247. """Validate satellite configuration and return any errors."""
  248. errors = []
  249. if not self.server_host:
  250. errors.append("Server host cannot be empty")
  251. if not (1024 <= self.server_port <= 65535):
  252. errors.append("Server port must be between 1024 and 65535")
  253. if self.reconnect_interval <= 0:
  254. errors.append("Reconnect interval must be positive")
  255. if not self.room_name:
  256. errors.append("Room name cannot be empty")
  257. if not self.alias_name:
  258. errors.append("Alias name cannot be empty")
  259. if self.stream_quality not in ["low", "medium", "high"]:
  260. errors.append("Stream quality must be 'low', 'medium', or 'high'")
  261. return errors
  262. @dataclass
  263. class SecurityConfig:
  264. """Security and authentication configuration."""
  265. # Encryption settings
  266. enable_encryption: bool = False
  267. encryption_algorithm: str = "AES-256-GCM"
  268. key_rotation_interval: int = 86400 # 24 hours in seconds
  269. # Authentication
  270. require_authentication: bool = False
  271. auth_token_expiry: int = 3600 # 1 hour in seconds
  272. max_auth_attempts: int = 3
  273. auth_lockout_duration: int = 300 # 5 minutes in seconds
  274. # Access control
  275. allowed_clients: List[str] = field(default_factory=list)
  276. blocked_clients: List[str] = field(default_factory=list)
  277. max_connections_per_client: int = 5
  278. # Security features
  279. enable_audit_logging: bool = True
  280. audit_log_file: str = "logs/security_audit.log"
  281. enable_intrusion_detection: bool = False
  282. def validate(self) -> List[str]:
  283. """Validate security configuration and return any errors."""
  284. errors = []
  285. if self.key_rotation_interval <= 0:
  286. errors.append("Key rotation interval must be positive")
  287. if self.auth_token_expiry <= 0:
  288. errors.append("Auth token expiry must be positive")
  289. if self.max_auth_attempts <= 0:
  290. errors.append("Max auth attempts must be positive")
  291. if self.auth_lockout_duration <= 0:
  292. errors.append("Auth lockout duration must be positive")
  293. if self.max_connections_per_client <= 0:
  294. errors.append("Max connections per client must be positive")
  295. # Check for conflicts between allowed and blocked clients
  296. allowed_set = set(self.allowed_clients)
  297. blocked_set = set(self.blocked_clients)
  298. conflicts = allowed_set & blocked_set
  299. if conflicts:
  300. errors.append(f"Clients cannot be both allowed and blocked: {conflicts}")
  301. return errors
  302. @dataclass
  303. class LoggingConfig:
  304. """Logging configuration."""
  305. # Basic logging settings
  306. log_level: LogLevel = LogLevel.INFO
  307. log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
  308. date_format: str = "%Y-%m-%d %H:%M:%S"
  309. # File logging
  310. enable_file_logging: bool = True
  311. log_file_path: str = "logs/trixy.log"
  312. max_log_file_size: int = 10485760 # 10MB
  313. log_file_backup_count: int = 5
  314. # Console logging
  315. enable_console_logging: bool = True
  316. console_log_level: LogLevel = LogLevel.INFO
  317. # Specialized logging
  318. enable_event_logging: bool = True
  319. event_log_file: str = "logs/events.log"
  320. enable_performance_logging: bool = False
  321. performance_log_file: str = "logs/performance.log"
  322. # Debug mode settings
  323. debug_mode: bool = False
  324. verbose_errors: bool = False
  325. log_stack_traces: bool = False
  326. def validate(self) -> List[str]:
  327. """Validate logging configuration and return any errors."""
  328. errors = []
  329. if self.max_log_file_size <= 0:
  330. errors.append("Max log file size must be positive")
  331. if self.log_file_backup_count < 0:
  332. errors.append("Log file backup count must be non-negative")
  333. # Validate log file paths
  334. log_files = [
  335. self.log_file_path,
  336. self.event_log_file,
  337. self.performance_log_file
  338. ]
  339. for log_file in log_files:
  340. if not log_file:
  341. errors.append(f"Log file path cannot be empty: {log_file}")
  342. return errors
  343. @dataclass
  344. class ServerConfig:
  345. """Complete configuration for server mode."""
  346. # Basic settings
  347. mode: DeploymentMode = DeploymentMode.SERVER
  348. debug_mode: bool = False
  349. data_directory: str = "data"
  350. config_file: str = "config/server_config.json"
  351. # Component configurations
  352. network: NetworkConfig = field(default_factory=NetworkConfig)
  353. audio: AudioConfig = field(default_factory=AudioConfig)
  354. ml: MLConfig = field(default_factory=MLConfig)
  355. plugins: PluginConfig = field(default_factory=PluginConfig)
  356. security: SecurityConfig = field(default_factory=SecurityConfig)
  357. logging: LoggingConfig = field(default_factory=LoggingConfig)
  358. # Server-specific settings
  359. registration_mode_timeout: int = 60 # seconds
  360. max_satellites: int = 50
  361. satellite_heartbeat_timeout: int = 60 # seconds
  362. enable_web_interface: bool = False
  363. web_interface_port: int = 8080
  364. # Asset management
  365. asset_profile: str = "default"
  366. asset_cache_size: int = 100
  367. # Conversation management
  368. conversation_timeout: int = 300 # 5 minutes
  369. max_concurrent_conversations: int = 10
  370. def validate(self) -> List[str]:
  371. """Validate complete server configuration."""
  372. errors = []
  373. # Validate component configurations
  374. errors.extend(self.network.validate())
  375. errors.extend(self.audio.validate())
  376. errors.extend(self.ml.validate())
  377. errors.extend(self.plugins.validate())
  378. errors.extend(self.security.validate())
  379. errors.extend(self.logging.validate())
  380. # Validate server-specific settings
  381. if self.registration_mode_timeout <= 0:
  382. errors.append("Registration mode timeout must be positive")
  383. if self.max_satellites <= 0:
  384. errors.append("Max satellites must be positive")
  385. if self.satellite_heartbeat_timeout <= 0:
  386. errors.append("Satellite heartbeat timeout must be positive")
  387. if self.web_interface_port and not (1024 <= self.web_interface_port <= 65535):
  388. errors.append("Web interface port must be between 1024 and 65535")
  389. if self.conversation_timeout <= 0:
  390. errors.append("Conversation timeout must be positive")
  391. if self.max_concurrent_conversations <= 0:
  392. errors.append("Max concurrent conversations must be positive")
  393. if self.asset_cache_size <= 0:
  394. errors.append("Asset cache size must be positive")
  395. # Check for port conflicts with web interface
  396. if (self.enable_web_interface and
  397. self.web_interface_port in [
  398. self.network.command_port,
  399. self.network.raw_audio_input_port,
  400. self.network.raw_audio_output_port,
  401. self.network.raw_music_output_port
  402. ]):
  403. errors.append("Web interface port conflicts with network ports")
  404. return errors
  405. def to_dict(self) -> Dict[str, Any]:
  406. """Convert configuration to dictionary for serialization."""
  407. return asdict(self)
  408. @classmethod
  409. def from_dict(cls, data: Dict[str, Any]) -> 'ServerConfig':
  410. """Create configuration from dictionary."""
  411. # Handle nested configurations
  412. if 'network' in data and isinstance(data['network'], dict):
  413. data['network'] = NetworkConfig(**data['network'])
  414. if 'audio' in data and isinstance(data['audio'], dict):
  415. data['audio'] = AudioConfig(**data['audio'])
  416. if 'ml' in data and isinstance(data['ml'], dict):
  417. data['ml'] = MLConfig(**data['ml'])
  418. if 'plugins' in data and isinstance(data['plugins'], dict):
  419. data['plugins'] = PluginConfig(**data['plugins'])
  420. if 'security' in data and isinstance(data['security'], dict):
  421. data['security'] = SecurityConfig(**data['security'])
  422. if 'logging' in data and isinstance(data['logging'], dict):
  423. data['logging'] = LoggingConfig(**data['logging'])
  424. # Handle enums
  425. if 'mode' in data and isinstance(data['mode'], str):
  426. data['mode'] = DeploymentMode(data['mode'])
  427. return cls(**data)
  428. @dataclass
  429. class ClientConfig:
  430. """Complete configuration for client/satellite mode."""
  431. # Basic settings
  432. mode: DeploymentMode = DeploymentMode.CLIENT
  433. debug_mode: bool = False
  434. data_directory: str = "data"
  435. config_file: str = "config/client_config.json"
  436. # Component configurations
  437. satellite: SatelliteConfig = field(default_factory=SatelliteConfig)
  438. audio: AudioConfig = field(default_factory=AudioConfig)
  439. ml: MLConfig = field(default_factory=MLConfig)
  440. logging: LoggingConfig = field(default_factory=LoggingConfig)
  441. # Client-specific settings
  442. enable_local_processing: bool = True
  443. local_model_cache_size: int = 50
  444. audio_buffer_size: int = 8192
  445. # Status reporting
  446. status_report_interval: int = 30 # seconds
  447. health_check_interval: int = 10 # seconds
  448. def validate(self) -> List[str]:
  449. """Validate complete client configuration."""
  450. errors = []
  451. # Validate component configurations
  452. errors.extend(self.satellite.validate())
  453. errors.extend(self.audio.validate())
  454. errors.extend(self.ml.validate())
  455. errors.extend(self.logging.validate())
  456. # Validate client-specific settings
  457. if self.local_model_cache_size <= 0:
  458. errors.append("Local model cache size must be positive")
  459. if self.audio_buffer_size <= 0:
  460. errors.append("Audio buffer size must be positive")
  461. if self.status_report_interval <= 0:
  462. errors.append("Status report interval must be positive")
  463. if self.health_check_interval <= 0:
  464. errors.append("Health check interval must be positive")
  465. return errors
  466. def to_dict(self) -> Dict[str, Any]:
  467. """Convert configuration to dictionary for serialization."""
  468. return asdict(self)
  469. @classmethod
  470. def from_dict(cls, data: Dict[str, Any]) -> 'ClientConfig':
  471. """Create configuration from dictionary."""
  472. # Handle nested configurations
  473. if 'satellite' in data and isinstance(data['satellite'], dict):
  474. data['satellite'] = SatelliteConfig(**data['satellite'])
  475. if 'audio' in data and isinstance(data['audio'], dict):
  476. data['audio'] = AudioConfig(**data['audio'])
  477. if 'ml' in data and isinstance(data['ml'], dict):
  478. data['ml'] = MLConfig(**data['ml'])
  479. if 'logging' in data and isinstance(data['logging'], dict):
  480. data['logging'] = LoggingConfig(**data['logging'])
  481. # Handle enums
  482. if 'mode' in data and isinstance(data['mode'], str):
  483. data['mode'] = DeploymentMode(data['mode'])
  484. return cls(**data)
  485. @dataclass
  486. class StandaloneConfig:
  487. """Complete configuration for standalone mode."""
  488. # Basic settings
  489. mode: DeploymentMode = DeploymentMode.STANDALONE
  490. debug_mode: bool = False
  491. data_directory: str = "data"
  492. config_file: str = "config/standalone_config.json"
  493. # Component configurations (full server capabilities)
  494. network: NetworkConfig = field(default_factory=NetworkConfig)
  495. audio: AudioConfig = field(default_factory=AudioConfig)
  496. ml: MLConfig = field(default_factory=MLConfig)
  497. plugins: PluginConfig = field(default_factory=PluginConfig)
  498. security: SecurityConfig = field(default_factory=SecurityConfig)
  499. logging: LoggingConfig = field(default_factory=LoggingConfig)
  500. # Standalone-specific settings
  501. enable_server_sync: bool = False
  502. sync_server_host: Optional[str] = None
  503. sync_server_port: Optional[int] = None
  504. sync_interval: int = 300 # 5 minutes
  505. # Local capabilities (same as server but with defaults for standalone)
  506. registration_mode_timeout: int = 60
  507. max_satellites: int = 10 # Lower default for standalone
  508. satellite_heartbeat_timeout: int = 60
  509. enable_web_interface: bool = True # More likely to be used in standalone
  510. web_interface_port: int = 8080
  511. # Asset management
  512. asset_profile: str = "default"
  513. asset_cache_size: int = 50 # Lower default for standalone
  514. # Conversation management
  515. conversation_timeout: int = 300
  516. max_concurrent_conversations: int = 5 # Lower default for standalone
  517. def validate(self) -> List[str]:
  518. """Validate complete standalone configuration."""
  519. errors = []
  520. # Validate component configurations
  521. errors.extend(self.network.validate())
  522. errors.extend(self.audio.validate())
  523. errors.extend(self.ml.validate())
  524. errors.extend(self.plugins.validate())
  525. errors.extend(self.security.validate())
  526. errors.extend(self.logging.validate())
  527. # Validate standalone-specific settings
  528. if self.enable_server_sync:
  529. if not self.sync_server_host:
  530. errors.append("Sync server host must be specified when server sync is enabled")
  531. if not self.sync_server_port or not (1024 <= self.sync_server_port <= 65535):
  532. errors.append("Sync server port must be between 1024 and 65535 when server sync is enabled")
  533. if self.sync_interval <= 0:
  534. errors.append("Sync interval must be positive")
  535. if self.registration_mode_timeout <= 0:
  536. errors.append("Registration mode timeout must be positive")
  537. if self.max_satellites <= 0:
  538. errors.append("Max satellites must be positive")
  539. if self.satellite_heartbeat_timeout <= 0:
  540. errors.append("Satellite heartbeat timeout must be positive")
  541. if self.web_interface_port and not (1024 <= self.web_interface_port <= 65535):
  542. errors.append("Web interface port must be between 1024 and 65535")
  543. if self.conversation_timeout <= 0:
  544. errors.append("Conversation timeout must be positive")
  545. if self.max_concurrent_conversations <= 0:
  546. errors.append("Max concurrent conversations must be positive")
  547. if self.asset_cache_size <= 0:
  548. errors.append("Asset cache size must be positive")
  549. # Check for port conflicts
  550. network_ports = [
  551. self.network.command_port,
  552. self.network.raw_audio_input_port,
  553. self.network.raw_audio_output_port,
  554. self.network.raw_music_output_port
  555. ]
  556. if self.enable_web_interface and self.web_interface_port in network_ports:
  557. errors.append("Web interface port conflicts with network ports")
  558. if self.enable_server_sync and self.sync_server_port in network_ports:
  559. errors.append("Sync server port conflicts with network ports")
  560. return errors
  561. def to_dict(self) -> Dict[str, Any]:
  562. """Convert configuration to dictionary for serialization."""
  563. return asdict(self)
  564. @classmethod
  565. def from_dict(cls, data: Dict[str, Any]) -> 'StandaloneConfig':
  566. """Create configuration from dictionary."""
  567. # Handle nested configurations
  568. if 'network' in data and isinstance(data['network'], dict):
  569. data['network'] = NetworkConfig(**data['network'])
  570. if 'audio' in data and isinstance(data['audio'], dict):
  571. data['audio'] = AudioConfig(**data['audio'])
  572. if 'ml' in data and isinstance(data['ml'], dict):
  573. data['ml'] = MLConfig(**data['ml'])
  574. if 'plugins' in data and isinstance(data['plugins'], dict):
  575. data['plugins'] = PluginConfig(**data['plugins'])
  576. if 'security' in data and isinstance(data['security'], dict):
  577. data['security'] = SecurityConfig(**data['security'])
  578. if 'logging' in data and isinstance(data['logging'], dict):
  579. data['logging'] = LoggingConfig(**data['logging'])
  580. # Handle enums
  581. if 'mode' in data and isinstance(data['mode'], str):
  582. data['mode'] = DeploymentMode(data['mode'])
  583. return cls(**data)
  584. # Type aliases for convenience
  585. ConfigType = Union[ServerConfig, ClientConfig, StandaloneConfig]
  586. ConfigDict = Dict[str, Any]
  587. # Configuration class mapping
  588. CONFIG_CLASSES = {
  589. DeploymentMode.SERVER: ServerConfig,
  590. DeploymentMode.CLIENT: ClientConfig,
  591. DeploymentMode.STANDALONE: StandaloneConfig,
  592. }
  593. def get_config_class(mode: Union[str, DeploymentMode]) -> type:
  594. """Get the appropriate configuration class for a deployment mode."""
  595. if isinstance(mode, str):
  596. mode = DeploymentMode(mode.lower())
  597. return CONFIG_CLASSES[mode]
  598. def create_default_config(mode: Union[str, DeploymentMode]) -> ConfigType:
  599. """Create a default configuration for the specified mode."""
  600. config_class = get_config_class(mode)
  601. return config_class()
  602. def merge_configs(base_config: ConfigType, override_config: Dict[str, Any]) -> ConfigType:
  603. """Merge override configuration into base configuration."""
  604. base_dict = base_config.to_dict()
  605. # Deep merge the dictionaries
  606. def deep_merge(base: Dict, override: Dict) -> Dict:
  607. result = base.copy()
  608. for key, value in override.items():
  609. if key in result and isinstance(result[key], dict) and isinstance(value, dict):
  610. result[key] = deep_merge(result[key], value)
  611. else:
  612. result[key] = value
  613. return result
  614. merged_dict = deep_merge(base_dict, override_config)
  615. return base_config.__class__.from_dict(merged_dict)