main.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. #!/usr/bin/env python3
  2. """
  3. Trixy Voice Assistant - Main Entry Point
  4. This module serves as the entry point for the Trixy voice assistant application,
  5. supporting three deployment modes: server, client/satellite, and standalone.
  6. Usage:
  7. python3 main.py [server|client|standalone] [--debug] [--config custom_config.json]
  8. Modes:
  9. server - Central hub with event handler, plugins, satellite management
  10. client - Satellite mode with wakeword detection and audio streaming
  11. standalone - Full system that can optionally connect to server for sync
  12. Arguments:
  13. --debug - Enable development mode (no TUI, print statements enabled)
  14. --config - Specify custom configuration file path
  15. Author: Generated with Claude Code
  16. """
  17. import sys
  18. import os
  19. import argparse
  20. import signal
  21. import logging
  22. import traceback
  23. from pathlib import Path
  24. from typing import Optional, Dict, Any
  25. # Import trainer CLI functionality for training mode
  26. try:
  27. from trainer.cli import (
  28. add_wakeword_arguments, add_voice_recognition_arguments,
  29. add_validation_arguments, add_list_arguments, add_convert_arguments,
  30. train_wakeword, train_voice_recognition, validate_model,
  31. list_models, convert_model, setup_cli_logging
  32. )
  33. TRAINER_AVAILABLE = True
  34. except ImportError:
  35. TRAINER_AVAILABLE = False
  36. # Note: Trainer modules not available - training mode disabled
  37. # Global application container reference for cleanup
  38. _application_container = None
  39. _debug_mode = False
  40. def pprint(message: str) -> None:
  41. """
  42. Adaptive print function that behaves differently in debug vs production mode.
  43. In debug mode: Prints directly to stdout
  44. In production mode: Uses logging system with Textual TUI
  45. Args:
  46. message: The message to print/log
  47. """
  48. if _debug_mode:
  49. print(f"[TRIXY] {message}")
  50. else:
  51. # In production mode, use logging system
  52. # The TUI will capture and display log messages appropriately
  53. logging.info(message)
  54. def create_directory_structure() -> None:
  55. """
  56. Create the required directory structure for Trixy if it doesn't exist.
  57. Creates all necessary directories as specified in CLAUDE.md:
  58. - trixy_core/ with all component subdirectories
  59. - plugins/
  60. - models/ with subdirectories
  61. - config/
  62. - assets/ with default profile
  63. - trainer/ with data subdirectories
  64. """
  65. directories = [
  66. # Core system modules
  67. "trixy_core",
  68. "trixy_core/events",
  69. "trixy_core/network",
  70. "trixy_core/network/cmd",
  71. "trixy_core/satellites",
  72. "trixy_core/plugins",
  73. "trixy_core/config",
  74. "trixy_core/assets",
  75. "trixy_core/scheduler",
  76. "trixy_core/conversation",
  77. "trixy_core/arbitration",
  78. # Plugin directory
  79. "plugins",
  80. # ML model storage
  81. "models",
  82. "models/wakeword",
  83. "models/voice_recognition",
  84. # Configuration files
  85. "config",
  86. # Profile-based assets
  87. "assets",
  88. "assets/default",
  89. # ML training system
  90. "trainer",
  91. "trainer/data",
  92. "trainer/data/wakeword",
  93. "trainer/data/wakeword/raw",
  94. "trainer/data/voice_recognition",
  95. "trainer/data/voice_recognition/raw",
  96. "trainer/wakeword",
  97. "trainer/voice_recognition",
  98. ]
  99. base_path = Path.cwd()
  100. created_dirs = []
  101. for directory in directories:
  102. dir_path = base_path / directory
  103. if not dir_path.exists():
  104. try:
  105. dir_path.mkdir(parents=True, exist_ok=True)
  106. created_dirs.append(str(dir_path))
  107. pprint(f"Created directory: {dir_path}")
  108. except OSError as e:
  109. pprint(f"Failed to create directory {dir_path}: {e}")
  110. sys.exit(1)
  111. if created_dirs:
  112. pprint(f"Successfully created {len(created_dirs)} directories")
  113. else:
  114. pprint("All required directories already exist")
  115. def setup_logging(debug: bool = False) -> None:
  116. """
  117. Initialize the logging system based on the mode.
  118. In debug mode: Basic console logging
  119. In production mode: Structured logging for TUI integration
  120. Args:
  121. debug: Whether debug mode is enabled
  122. """
  123. global _debug_mode
  124. _debug_mode = debug
  125. if debug:
  126. # Debug mode: Simple console logging
  127. logging.basicConfig(
  128. level=logging.DEBUG,
  129. format='[%(asctime)s] [%(levelname)s] %(name)s: %(message)s',
  130. datefmt='%H:%M:%S'
  131. )
  132. pprint("Debug mode enabled - console logging active")
  133. else:
  134. # Production mode: Structured logging for TUI
  135. logging.basicConfig(
  136. level=logging.INFO,
  137. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  138. handlers=[
  139. logging.StreamHandler(sys.stdout)
  140. ]
  141. )
  142. pprint("Production mode enabled - TUI logging active")
  143. def add_training_arguments(parser: argparse.ArgumentParser) -> None:
  144. """
  145. Add training-specific arguments to the parser.
  146. Args:
  147. parser: The argument parser to add training arguments to
  148. """
  149. if not TRAINER_AVAILABLE:
  150. return
  151. # Add global training arguments
  152. parser.add_argument(
  153. '--log-level', choices=["DEBUG", "INFO", "WARNING", "ERROR"],
  154. default="INFO", help="Logging level for training"
  155. )
  156. parser.add_argument(
  157. '--log-file', type=str,
  158. help="Path to log file for training (default: console only)"
  159. )
  160. parser.add_argument(
  161. '--device', choices=["auto", "cpu", "cuda"],
  162. default="auto", help="Device to use for training/inference"
  163. )
  164. # Create subparsers for training types
  165. train_subparsers = parser.add_subparsers(dest="train_type", help="Type of model to train")
  166. # Wakeword training
  167. wakeword_parser = train_subparsers.add_parser("wakeword", help="Train wakeword detection model")
  168. add_wakeword_arguments(wakeword_parser)
  169. # Voice recognition training
  170. voice_parser = train_subparsers.add_parser("voice-recognition", help="Train voice recognition model")
  171. add_voice_recognition_arguments(voice_parser)
  172. # Validation command
  173. validate_parser = train_subparsers.add_parser("validate", help="Validate a trained model")
  174. add_validation_arguments(validate_parser)
  175. # List models command
  176. list_parser = train_subparsers.add_parser("list-models", help="List available trained models")
  177. add_list_arguments(list_parser)
  178. # Convert model command
  179. convert_parser = train_subparsers.add_parser("convert", help="Convert model between formats")
  180. add_convert_arguments(convert_parser)
  181. def parse_arguments() -> argparse.Namespace:
  182. """
  183. Parse command line arguments.
  184. Returns:
  185. Parsed arguments namespace
  186. """
  187. parser = argparse.ArgumentParser(
  188. description="Trixy Voice Assistant - Professional voice assistant system",
  189. formatter_class=argparse.RawDescriptionHelpFormatter,
  190. epilog="""
  191. Examples:
  192. python3 main.py server --debug
  193. python3 main.py client --config /path/to/client_config.json
  194. python3 main.py standalone
  195. python3 main.py train wakeword --data-dir ./data --num-epochs 100
  196. python3 main.py train voice-recognition --data-dir ./data --model-type ecapa_tdnn
  197. python3 main.py train validate ./models/my_model.pth --test-data ./data/test
  198. python3 main.py train list-models --models-dir ./models
  199. """
  200. )
  201. # Create subparsers for different modes
  202. subparsers = parser.add_subparsers(dest="mode", help="Deployment mode")
  203. # Server mode
  204. server_parser = subparsers.add_parser("server", help="Central hub with event handler, plugins, satellite management")
  205. server_parser.add_argument('--debug', action='store_true', help='Enable debug mode (no TUI, print statements enabled)')
  206. server_parser.add_argument('--config', type=str, metavar='CONFIG_FILE', help='Path to custom configuration file (overrides default config)')
  207. # Client mode
  208. client_parser = subparsers.add_parser("client", help="Satellite mode with wakeword detection and audio streaming")
  209. client_parser.add_argument('--debug', action='store_true', help='Enable debug mode (no TUI, print statements enabled)')
  210. client_parser.add_argument('--config', type=str, metavar='CONFIG_FILE', help='Path to custom configuration file (overrides default config)')
  211. # Standalone mode
  212. standalone_parser = subparsers.add_parser("standalone", help="Full system that can optionally connect to server for sync")
  213. standalone_parser.add_argument('--debug', action='store_true', help='Enable debug mode (no TUI, print statements enabled)')
  214. standalone_parser.add_argument('--config', type=str, metavar='CONFIG_FILE', help='Path to custom configuration file (overrides default config)')
  215. # Training mode
  216. train_parser = subparsers.add_parser("train", help="ML model training")
  217. train_parser.add_argument('--debug', action='store_true', help='Enable debug mode (no TUI, print statements enabled)')
  218. train_parser.add_argument('--config', type=str, metavar='CONFIG_FILE', help='Path to custom configuration file (overrides default config)')
  219. add_training_arguments(train_parser)
  220. return parser.parse_args()
  221. def validate_environment() -> None:
  222. """
  223. Validate the runtime environment and dependencies.
  224. Raises:
  225. SystemExit: If critical dependencies are missing
  226. """
  227. python_version = sys.version_info
  228. if python_version < (3, 8):
  229. pprint(f"Error: Python 3.8+ required, found {python_version.major}.{python_version.minor}")
  230. sys.exit(1)
  231. pprint(f"Python {python_version.major}.{python_version.minor}.{python_version.micro} detected")
  232. # Check for write permissions in current directory
  233. try:
  234. test_file = Path.cwd() / ".trixy_write_test"
  235. test_file.touch()
  236. test_file.unlink()
  237. pprint("Write permissions verified")
  238. except (OSError, PermissionError) as e:
  239. pprint(f"Error: No write permissions in current directory: {e}")
  240. sys.exit(1)
  241. def initialize_application_container(mode: str, config_path: Optional[str] = None):
  242. """
  243. Initialize and return the application container based on the deployment mode.
  244. This is a placeholder for the actual application container implementation.
  245. The container will be implemented in trixy_core/ modules.
  246. Args:
  247. mode: Deployment mode (server, client, standalone)
  248. config_path: Optional path to custom configuration file
  249. Returns:
  250. Application container instance
  251. Raises:
  252. ImportError: If core modules are not yet implemented
  253. """
  254. try:
  255. # This import will be available once the core modules are implemented
  256. from trixy_core.application import ApplicationContainer
  257. pprint(f"Initializing {mode} mode application container")
  258. container = ApplicationContainer(mode=mode, config_path=config_path)
  259. pprint("Application container initialized successfully")
  260. return container
  261. except ImportError as e:
  262. pprint(f"Core modules not yet implemented: {e}")
  263. pprint("This is expected during initial setup phase")
  264. pprint("The application container will be available once core modules are created")
  265. return None
  266. def setup_signal_handlers() -> None:
  267. """
  268. Setup signal handlers for graceful shutdown.
  269. """
  270. def signal_handler(signum, frame):
  271. pprint(f"Received signal {signum}, initiating graceful shutdown...")
  272. shutdown_application()
  273. sys.exit(0)
  274. # Handle common termination signals
  275. signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
  276. signal.signal(signal.SIGTERM, signal_handler) # Termination signal
  277. # Handle SIGQUIT on Unix systems
  278. if hasattr(signal, 'SIGQUIT'):
  279. signal.signal(signal.SIGQUIT, signal_handler)
  280. pprint("Signal handlers configured for graceful shutdown")
  281. def execute_training_mode(args: argparse.Namespace) -> bool:
  282. """
  283. Execute training mode operations.
  284. Args:
  285. args: Parsed command line arguments
  286. Returns:
  287. bool: True if training completed successfully, False otherwise
  288. """
  289. if not TRAINER_AVAILABLE:
  290. pprint("Error: Training modules not available")
  291. pprint("Please ensure the trainer module is properly installed")
  292. return False
  293. # Clear root logger handlers to prevent duplicate logging
  294. # The root logger was configured earlier via logging.basicConfig()
  295. # but we want clean training-specific logging only
  296. root_logger = logging.getLogger()
  297. root_logger.handlers.clear()
  298. # Setup training-specific logging
  299. logger = setup_cli_logging(
  300. level=getattr(args, 'log_level', 'INFO'),
  301. log_file=getattr(args, 'log_file', None)
  302. )
  303. # Handle device selection for training
  304. if hasattr(args, 'device') and args.device == "auto":
  305. try:
  306. import torch
  307. device = "cuda" if torch.cuda.is_available() else "cpu"
  308. args.device = device
  309. logger.info(f"Auto-selected device: {device}")
  310. except ImportError:
  311. args.device = "cpu"
  312. logger.warning("PyTorch not available, defaulting to CPU")
  313. success = False
  314. try:
  315. if not hasattr(args, 'train_type') or args.train_type is None:
  316. pprint("Error: Please specify training type (wakeword, voice-recognition, validate, list-models, convert)")
  317. pprint("Usage: python3 main.py train <type> [arguments...]")
  318. return False
  319. if args.train_type == "wakeword":
  320. success = train_wakeword(args, logger)
  321. elif args.train_type == "voice-recognition":
  322. success = train_voice_recognition(args, logger)
  323. elif args.train_type == "validate":
  324. success = validate_model(args, logger)
  325. elif args.train_type == "list-models":
  326. success = list_models(args, logger)
  327. elif args.train_type == "convert":
  328. success = convert_model(args, logger)
  329. else:
  330. pprint(f"Error: Unknown training type '{args.train_type}'")
  331. pprint("Available types: wakeword, voice-recognition, validate, list-models, convert")
  332. return False
  333. except KeyboardInterrupt:
  334. logger.info("Training cancelled by user")
  335. return True
  336. except Exception as e:
  337. logger.error(f"Training failed with error: {e}")
  338. if _debug_mode:
  339. traceback.print_exc()
  340. return False
  341. return success
  342. def shutdown_application() -> None:
  343. """
  344. Perform graceful shutdown of the application.
  345. This function will:
  346. 1. Stop all running services
  347. 2. Close network connections
  348. 3. Save state and configuration
  349. 4. Clean up resources
  350. """
  351. global _application_container
  352. pprint("Starting graceful shutdown sequence...")
  353. if _application_container:
  354. try:
  355. # Call the application container's shutdown method
  356. # This will be implemented when the container is created
  357. if hasattr(_application_container, 'shutdown'):
  358. _application_container.shutdown()
  359. pprint("Application container shutdown completed")
  360. else:
  361. pprint("Application container shutdown method not available")
  362. except Exception as e:
  363. pprint(f"Error during application shutdown: {e}")
  364. if _debug_mode:
  365. traceback.print_exc()
  366. pprint("Graceful shutdown completed")
  367. def main() -> None:
  368. """
  369. Main entry point for the Trixy voice assistant application.
  370. This function:
  371. 1. Parses command line arguments
  372. 2. Validates the environment
  373. 3. Creates directory structure
  374. 4. Initializes logging
  375. 5. Sets up signal handlers
  376. 6. Initializes the application container
  377. 7. Starts the application
  378. 8. Handles graceful shutdown
  379. """
  380. global _application_container
  381. try:
  382. # Parse command line arguments
  383. args = parse_arguments()
  384. # Setup logging early
  385. debug_mode = getattr(args, 'debug', False)
  386. setup_logging(debug_mode)
  387. # Print startup banner
  388. pprint("="*50)
  389. pprint("Trixy Voice Assistant")
  390. pprint("Professional Voice Assistant System")
  391. pprint("="*50)
  392. pprint(f"Mode: {args.mode.upper()}")
  393. pprint(f"Debug: {'ENABLED' if debug_mode else 'DISABLED'}")
  394. config_path = getattr(args, 'config', None)
  395. if config_path:
  396. pprint(f"Config: {config_path}")
  397. pprint("="*50)
  398. # Validate environment
  399. pprint("Validating runtime environment...")
  400. validate_environment()
  401. # Create directory structure
  402. pprint("Creating directory structure...")
  403. create_directory_structure()
  404. # Setup signal handlers for graceful shutdown
  405. pprint("Setting up signal handlers...")
  406. setup_signal_handlers()
  407. # Handle different modes
  408. if args.mode == 'train':
  409. # Training mode - bypass application container
  410. pprint("Starting Trixy training mode...")
  411. success = execute_training_mode(args)
  412. if success:
  413. pprint("Training completed successfully!")
  414. sys.exit(0)
  415. else:
  416. pprint("Training failed!")
  417. sys.exit(1)
  418. else:
  419. # Standard modes (server, client, standalone)
  420. # Initialize application container
  421. pprint("Initializing application container...")
  422. _application_container = initialize_application_container(
  423. mode=args.mode,
  424. config_path=config_path
  425. )
  426. if _application_container:
  427. # Start the application
  428. pprint(f"Starting Trixy {args.mode} mode...")
  429. # The actual application start will be implemented
  430. # when the application container is created
  431. if hasattr(_application_container, 'start'):
  432. _application_container.start()
  433. else:
  434. pprint("Application container start method not available")
  435. pprint("Core system implementation is required")
  436. pprint("This main.py serves as the foundation for future development")
  437. else:
  438. pprint("Application container not available")
  439. pprint("Core modules need to be implemented")
  440. pprint("This main.py is ready for integration with core system")
  441. except KeyboardInterrupt:
  442. pprint("Received keyboard interrupt")
  443. shutdown_application()
  444. sys.exit(0)
  445. except Exception as e:
  446. pprint(f"Fatal error during startup: {e}")
  447. if _debug_mode:
  448. traceback.print_exc()
  449. shutdown_application()
  450. sys.exit(1)
  451. if __name__ == "__main__":
  452. main()