config.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. """
  2. Configuration management and argument parsing for ML trainers.
  3. This module provides comprehensive configuration management including
  4. file-based configuration, command-line argument parsing, and validation
  5. with support for inheritance and environment variable override.
  6. """
  7. import os
  8. import sys
  9. import json
  10. import yaml
  11. import argparse
  12. import logging
  13. from pathlib import Path
  14. from typing import Any, Dict, List, Optional, Union, Type
  15. from dataclasses import dataclass, field, fields, asdict
  16. from abc import ABC, abstractmethod
  17. import torch
  18. from .base import TrainerConfig, ModelFormat, TrainingState
  19. class ConfigError(Exception):
  20. """Exception raised for configuration-related errors."""
  21. pass
  22. class ConfigValidator(ABC):
  23. """Abstract base class for configuration validators."""
  24. @abstractmethod
  25. def validate(self, config: Dict[str, Any]) -> List[str]:
  26. """Validate configuration and return list of error messages."""
  27. pass
  28. class TrainerConfigValidator(ConfigValidator):
  29. """Validator for trainer configuration."""
  30. def validate(self, config: Dict[str, Any]) -> List[str]:
  31. """Validate trainer configuration."""
  32. errors = []
  33. # Required fields
  34. required_fields = ['trainer_name', 'model_name', 'data_dir']
  35. for field in required_fields:
  36. if field not in config or not config[field]:
  37. errors.append(f"Required field '{field}' is missing or empty")
  38. # Numeric validations
  39. numeric_validations = {
  40. 'batch_size': (1, 1024),
  41. 'learning_rate': (1e-8, 1.0),
  42. 'num_epochs': (1, 10000),
  43. 'validation_split': (0.0, 1.0),
  44. 'test_split': (0.0, 1.0),
  45. 'sample_rate': (8000, 48000),
  46. 'n_mels': (10, 128),
  47. 'audio_length': (0.1, 30.0)
  48. }
  49. for field, (min_val, max_val) in numeric_validations.items():
  50. if field in config:
  51. value = config[field]
  52. if not isinstance(value, (int, float)) or not (min_val <= value <= max_val):
  53. errors.append(f"Field '{field}' must be between {min_val} and {max_val}")
  54. # Validation split + test split check
  55. if 'validation_split' in config and 'test_split' in config:
  56. if config['validation_split'] + config['test_split'] >= 1.0:
  57. errors.append("validation_split + test_split must be less than 1.0")
  58. # Directory existence checks
  59. directory_fields = ['data_dir', 'output_dir', 'config_dir']
  60. for field in directory_fields:
  61. if field in config and config[field]:
  62. path = Path(config[field])
  63. if field == 'data_dir' and not path.exists():
  64. errors.append(f"Data directory does not exist: {config[field]}")
  65. # Model format validation
  66. if 'model_format' in config:
  67. format_value = config['model_format']
  68. if isinstance(format_value, str):
  69. valid_formats = [fmt.value for fmt in ModelFormat]
  70. if format_value not in valid_formats:
  71. errors.append(f"Invalid model format: {format_value}. Valid options: {valid_formats}")
  72. # Device validation
  73. if 'device' in config:
  74. device = config['device']
  75. if device not in ['auto', 'cpu', 'cuda', 'mps']:
  76. errors.append(f"Invalid device: {device}. Valid options: auto, cpu, cuda, mps")
  77. return errors
  78. class WakewordConfigValidator(ConfigValidator):
  79. """Validator for wakeword trainer configuration."""
  80. def validate(self, config: Dict[str, Any]) -> List[str]:
  81. """Validate wakeword-specific configuration."""
  82. errors = []
  83. # Wakeword-specific validations
  84. if 'wakeword_classes' in config:
  85. classes = config['wakeword_classes']
  86. if not isinstance(classes, dict):
  87. errors.append("wakeword_classes must be a dictionary")
  88. elif len(classes) < 2:
  89. errors.append("At least 2 wakeword classes required (including negative)")
  90. # Audio processing for wakewords
  91. if 'audio_length' in config:
  92. if config['audio_length'] < 0.5 or config['audio_length'] > 5.0:
  93. errors.append("Wakeword audio_length should be between 0.5 and 5.0 seconds")
  94. return errors
  95. class VoiceRecognitionConfigValidator(ConfigValidator):
  96. """Validator for voice recognition trainer configuration."""
  97. def validate(self, config: Dict[str, Any]) -> List[str]:
  98. """Validate voice recognition specific configuration."""
  99. errors = []
  100. # Speaker-specific validations
  101. if 'num_speakers' in config:
  102. num_speakers = config['num_speakers']
  103. if not isinstance(num_speakers, int) or num_speakers < 2:
  104. errors.append("num_speakers must be at least 2")
  105. elif num_speakers > 1000:
  106. errors.append("num_speakers should not exceed 1000 for practical purposes")
  107. if 'embedding_dimension' in config:
  108. embedding_dim = config['embedding_dimension']
  109. if not isinstance(embedding_dim, int) or embedding_dim < 64:
  110. errors.append("embedding_dimension must be at least 64")
  111. elif embedding_dim > 2048:
  112. errors.append("embedding_dimension should not exceed 2048")
  113. # Audio processing for voice recognition
  114. if 'audio_length' in config:
  115. if config['audio_length'] < 1.0 or config['audio_length'] > 10.0:
  116. errors.append("Voice recognition audio_length should be between 1.0 and 10.0 seconds")
  117. return errors
  118. class ConfigManager:
  119. """
  120. Comprehensive configuration manager for ML trainers.
  121. Supports loading from files, command-line arguments, environment variables,
  122. and provides validation and inheritance capabilities.
  123. """
  124. def __init__(self, config_dir: str = "./config", logger: Optional[logging.Logger] = None):
  125. """
  126. Initialize configuration manager.
  127. Args:
  128. config_dir: Directory containing configuration files
  129. logger: Optional logger instance
  130. """
  131. self.config_dir = Path(config_dir)
  132. self.logger = logger or logging.getLogger(__name__)
  133. self.validators: Dict[str, ConfigValidator] = {
  134. 'base': TrainerConfigValidator(),
  135. 'wakeword': WakewordConfigValidator(),
  136. 'voice_recognition': VoiceRecognitionConfigValidator()
  137. }
  138. self.loaded_configs: Dict[str, Dict[str, Any]] = {}
  139. def load_config_file(self, filepath: Union[str, Path]) -> Dict[str, Any]:
  140. """
  141. Load configuration from file (JSON or YAML).
  142. Args:
  143. filepath: Path to configuration file
  144. Returns:
  145. Configuration dictionary
  146. """
  147. filepath = Path(filepath)
  148. if not filepath.exists():
  149. raise ConfigError(f"Configuration file not found: {filepath}")
  150. try:
  151. with open(filepath, 'r', encoding='utf-8') as f:
  152. if filepath.suffix.lower() in ['.yml', '.yaml']:
  153. try:
  154. config = yaml.safe_load(f)
  155. except ImportError:
  156. raise ConfigError("PyYAML not available for YAML configuration files")
  157. elif filepath.suffix.lower() == '.json':
  158. config = json.load(f)
  159. else:
  160. raise ConfigError(f"Unsupported configuration file format: {filepath.suffix}")
  161. self.logger.info(f"Loaded configuration from: {filepath}")
  162. return config or {}
  163. except (json.JSONDecodeError, yaml.YAMLError) as e:
  164. raise ConfigError(f"Error parsing configuration file {filepath}: {str(e)}")
  165. except Exception as e:
  166. raise ConfigError(f"Error loading configuration file {filepath}: {str(e)}")
  167. def save_config_file(self, config: Dict[str, Any], filepath: Union[str, Path]):
  168. """
  169. Save configuration to file.
  170. Args:
  171. config: Configuration dictionary
  172. filepath: Path to save configuration
  173. """
  174. filepath = Path(filepath)
  175. filepath.parent.mkdir(parents=True, exist_ok=True)
  176. try:
  177. with open(filepath, 'w', encoding='utf-8') as f:
  178. if filepath.suffix.lower() in ['.yml', '.yaml']:
  179. try:
  180. yaml.safe_dump(config, f, default_flow_style=False, allow_unicode=True)
  181. except ImportError:
  182. raise ConfigError("PyYAML not available for YAML configuration files")
  183. elif filepath.suffix.lower() == '.json':
  184. json.dump(config, f, indent=2, ensure_ascii=False)
  185. else:
  186. raise ConfigError(f"Unsupported configuration file format: {filepath.suffix}")
  187. self.logger.info(f"Saved configuration to: {filepath}")
  188. except Exception as e:
  189. raise ConfigError(f"Error saving configuration file {filepath}: {str(e)}")
  190. def load_trainer_config(self, trainer_name: str, config_name: str = None) -> TrainerConfig:
  191. """
  192. Load trainer configuration with inheritance and validation.
  193. Args:
  194. trainer_name: Name of the trainer (wakeword, voice_recognition, etc.)
  195. config_name: Specific configuration name (defaults to trainer_name)
  196. Returns:
  197. TrainerConfig instance
  198. """
  199. if config_name is None:
  200. config_name = trainer_name
  201. # Load base configuration
  202. base_config = self._load_base_config()
  203. # Load trainer-specific configuration
  204. trainer_config = self._load_trainer_specific_config(trainer_name, config_name)
  205. # Merge configurations (trainer-specific overrides base)
  206. merged_config = self._merge_configs(base_config, trainer_config)
  207. # Apply environment variable overrides
  208. merged_config = self._apply_env_overrides(merged_config)
  209. # Validate configuration
  210. self._validate_config(merged_config, trainer_name)
  211. # Convert to TrainerConfig object
  212. return self._dict_to_trainer_config(merged_config)
  213. def _load_base_config(self) -> Dict[str, Any]:
  214. """Load base trainer configuration."""
  215. base_config_path = self.config_dir / "trainer_base.json"
  216. if base_config_path.exists():
  217. return self.load_config_file(base_config_path)
  218. else:
  219. # Return default base configuration
  220. self.logger.info("Using default base configuration")
  221. return {
  222. "batch_size": 32,
  223. "learning_rate": 0.001,
  224. "num_epochs": 100,
  225. "validation_split": 0.2,
  226. "test_split": 0.1,
  227. "sample_rate": 16000,
  228. "audio_length": 1.5,
  229. "n_mels": 40,
  230. "use_augmentation": True,
  231. "log_level": "INFO",
  232. "device": "auto"
  233. }
  234. def _load_trainer_specific_config(self, trainer_name: str, config_name: str) -> Dict[str, Any]:
  235. """Load trainer-specific configuration."""
  236. config_paths = [
  237. self.config_dir / f"{config_name}.json",
  238. self.config_dir / f"{config_name}.yml",
  239. self.config_dir / f"{config_name}.yaml",
  240. self.config_dir / f"{trainer_name}.json",
  241. self.config_dir / f"{trainer_name}.yml",
  242. self.config_dir / f"{trainer_name}.yaml"
  243. ]
  244. for config_path in config_paths:
  245. if config_path.exists():
  246. return self.load_config_file(config_path)
  247. self.logger.warning(f"No specific configuration found for {trainer_name}/{config_name}")
  248. return {}
  249. def _merge_configs(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
  250. """Merge configuration dictionaries (override takes precedence)."""
  251. merged = base.copy()
  252. for key, value in override.items():
  253. if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
  254. # Recursively merge nested dictionaries
  255. merged[key] = self._merge_configs(merged[key], value)
  256. else:
  257. merged[key] = value
  258. return merged
  259. def _apply_env_overrides(self, config: Dict[str, Any]) -> Dict[str, Any]:
  260. """Apply environment variable overrides."""
  261. env_prefix = "TRIXY_TRAINER_"
  262. for key, value in os.environ.items():
  263. if key.startswith(env_prefix):
  264. config_key = key[len(env_prefix):].lower()
  265. # Convert environment variable value to appropriate type
  266. try:
  267. # Try to parse as JSON first (for complex types)
  268. parsed_value = json.loads(value)
  269. except json.JSONDecodeError:
  270. # Fall back to string/numeric parsing
  271. if value.lower() in ['true', 'false']:
  272. parsed_value = value.lower() == 'true'
  273. elif value.isdigit():
  274. parsed_value = int(value)
  275. else:
  276. try:
  277. parsed_value = float(value)
  278. except ValueError:
  279. parsed_value = value
  280. config[config_key] = parsed_value
  281. self.logger.info(f"Applied environment override: {config_key} = {parsed_value}")
  282. return config
  283. def _validate_config(self, config: Dict[str, Any], trainer_name: str):
  284. """Validate configuration using appropriate validators."""
  285. all_errors = []
  286. # Always validate with base validator
  287. base_errors = self.validators['base'].validate(config)
  288. all_errors.extend(base_errors)
  289. # Validate with trainer-specific validator if available
  290. if trainer_name in self.validators:
  291. trainer_errors = self.validators[trainer_name].validate(config)
  292. all_errors.extend(trainer_errors)
  293. if all_errors:
  294. error_msg = f"Configuration validation failed:\n" + "\n".join(f"- {error}" for error in all_errors)
  295. raise ConfigError(error_msg)
  296. def _dict_to_trainer_config(self, config_dict: Dict[str, Any]) -> TrainerConfig:
  297. """Convert configuration dictionary to TrainerConfig object."""
  298. # Handle enum conversions
  299. if 'model_format' in config_dict:
  300. if isinstance(config_dict['model_format'], str):
  301. config_dict['model_format'] = ModelFormat(config_dict['model_format'])
  302. # Filter only valid TrainerConfig fields
  303. trainer_config_fields = {f.name for f in fields(TrainerConfig)}
  304. filtered_config = {k: v for k, v in config_dict.items() if k in trainer_config_fields}
  305. # Store additional config in custom_params
  306. custom_params = {k: v for k, v in config_dict.items() if k not in trainer_config_fields}
  307. if custom_params:
  308. filtered_config['custom_params'] = custom_params
  309. try:
  310. return TrainerConfig(**filtered_config)
  311. except Exception as e:
  312. raise ConfigError(f"Error creating TrainerConfig: {str(e)}")
  313. def create_argument_parser(self, trainer_name: str = "trainer") -> argparse.ArgumentParser:
  314. """
  315. Create command-line argument parser for trainer.
  316. Args:
  317. trainer_name: Name of the trainer for help text
  318. Returns:
  319. Configured ArgumentParser
  320. """
  321. parser = argparse.ArgumentParser(
  322. description=f"Trixy {trainer_name.title()} Trainer",
  323. formatter_class=argparse.ArgumentDefaultsHelpFormatter
  324. )
  325. # General arguments
  326. parser.add_argument(
  327. '--config', '-c', type=str,
  328. help='Configuration file path'
  329. )
  330. parser.add_argument(
  331. '--trainer-name', type=str, default=trainer_name,
  332. help='Name of the trainer'
  333. )
  334. parser.add_argument(
  335. '--model-name', type=str,
  336. help='Name of the model'
  337. )
  338. parser.add_argument(
  339. '--output-dir', type=str, default='./models',
  340. help='Output directory for models'
  341. )
  342. parser.add_argument(
  343. '--data-dir', type=str, default='./trainer/data',
  344. help='Data directory'
  345. )
  346. # Training parameters
  347. training_group = parser.add_argument_group('Training Parameters')
  348. training_group.add_argument(
  349. '--batch-size', type=int, default=32,
  350. help='Batch size for training'
  351. )
  352. training_group.add_argument(
  353. '--learning-rate', '--lr', type=float, default=0.001,
  354. help='Learning rate'
  355. )
  356. training_group.add_argument(
  357. '--num-epochs', '--epochs', type=int, default=100,
  358. help='Number of training epochs'
  359. )
  360. training_group.add_argument(
  361. '--min-epochs', type=int, default=10,
  362. help='Minimum number of epochs'
  363. )
  364. training_group.add_argument(
  365. '--max-epochs', type=int, default=1000,
  366. help='Maximum number of epochs'
  367. )
  368. training_group.add_argument(
  369. '--early-stopping-patience', type=int, default=10,
  370. help='Early stopping patience'
  371. )
  372. # Model parameters
  373. model_group = parser.add_argument_group('Model Parameters')
  374. model_group.add_argument(
  375. '--model-format', choices=['pth', 'pt', 'onnx'], default='pth',
  376. help='Model file format'
  377. )
  378. model_group.add_argument(
  379. '--use-mixed-precision', action='store_true', default=True,
  380. help='Use mixed precision training'
  381. )
  382. model_group.add_argument(
  383. '--no-mixed-precision', dest='use_mixed_precision', action='store_false',
  384. help='Disable mixed precision training'
  385. )
  386. # Data parameters
  387. data_group = parser.add_argument_group('Data Parameters')
  388. data_group.add_argument(
  389. '--validation-split', type=float, default=0.2,
  390. help='Validation split ratio'
  391. )
  392. data_group.add_argument(
  393. '--test-split', type=float, default=0.1,
  394. help='Test split ratio'
  395. )
  396. data_group.add_argument(
  397. '--sample-rate', type=int, default=16000,
  398. help='Audio sample rate'
  399. )
  400. data_group.add_argument(
  401. '--audio-length', type=float, default=1.5,
  402. help='Audio length in seconds'
  403. )
  404. data_group.add_argument(
  405. '--n-mels', type=int, default=40,
  406. help='Number of mel filterbanks'
  407. )
  408. # Augmentation
  409. aug_group = parser.add_argument_group('Data Augmentation')
  410. aug_group.add_argument(
  411. '--use-augmentation', action='store_true', default=True,
  412. help='Use data augmentation'
  413. )
  414. aug_group.add_argument(
  415. '--no-augmentation', dest='use_augmentation', action='store_false',
  416. help='Disable data augmentation'
  417. )
  418. aug_group.add_argument(
  419. '--noise-factor', type=float, default=0.1,
  420. help='Noise augmentation factor'
  421. )
  422. aug_group.add_argument(
  423. '--speed-factor', type=float, default=0.1,
  424. help='Speed augmentation factor'
  425. )
  426. # Security
  427. security_group = parser.add_argument_group('Security')
  428. security_group.add_argument(
  429. '--password', type=str,
  430. help='Password for model encryption'
  431. )
  432. security_group.add_argument(
  433. '--no-password-protection', dest='use_password_protection',
  434. action='store_false', default=True,
  435. help='Disable password protection'
  436. )
  437. # Hardware
  438. hardware_group = parser.add_argument_group('Hardware')
  439. hardware_group.add_argument(
  440. '--device', choices=['auto', 'cpu', 'cuda', 'mps'], default='auto',
  441. help='Training device'
  442. )
  443. hardware_group.add_argument(
  444. '--num-workers', type=int, default=4,
  445. help='Number of data loader workers'
  446. )
  447. # Logging and checkpoints
  448. logging_group = parser.add_argument_group('Logging and Checkpoints')
  449. logging_group.add_argument(
  450. '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], default='INFO',
  451. help='Logging level'
  452. )
  453. logging_group.add_argument(
  454. '--save-checkpoints', action='store_true', default=True,
  455. help='Save training checkpoints'
  456. )
  457. logging_group.add_argument(
  458. '--no-checkpoints', dest='save_checkpoints', action='store_false',
  459. help='Disable checkpoint saving'
  460. )
  461. logging_group.add_argument(
  462. '--checkpoint-interval', type=int, default=10,
  463. help='Checkpoint save interval (epochs)'
  464. )
  465. # Resume training
  466. resume_group = parser.add_argument_group('Resume Training')
  467. resume_group.add_argument(
  468. '--resume', '--resume-from-checkpoint', type=str,
  469. help='Resume training from checkpoint'
  470. )
  471. # Actions
  472. action_group = parser.add_argument_group('Actions')
  473. action_group.add_argument(
  474. '--train', action='store_true', default=True,
  475. help='Train the model'
  476. )
  477. action_group.add_argument(
  478. '--test-only', action='store_true',
  479. help='Only test the model (requires trained model)'
  480. )
  481. action_group.add_argument(
  482. '--prepare-data-only', action='store_true',
  483. help='Only prepare data without training'
  484. )
  485. action_group.add_argument(
  486. '--validate-config-only', action='store_true',
  487. help='Only validate configuration'
  488. )
  489. return parser
  490. def parse_args_to_config(self, args: argparse.Namespace, trainer_name: str) -> TrainerConfig:
  491. """
  492. Convert command-line arguments to TrainerConfig.
  493. Args:
  494. args: Parsed command-line arguments
  495. trainer_name: Name of the trainer
  496. Returns:
  497. TrainerConfig instance
  498. """
  499. # Start with configuration file if provided
  500. if hasattr(args, 'config') and args.config:
  501. config = self.load_config_file(args.config)
  502. else:
  503. config = self.load_trainer_config(trainer_name).to_dict()
  504. # Override with command-line arguments
  505. arg_dict = vars(args)
  506. for key, value in arg_dict.items():
  507. if value is not None:
  508. # Convert hyphens to underscores for config keys
  509. config_key = key.replace('-', '_')
  510. config[config_key] = value
  511. # Handle special cases
  512. if hasattr(args, 'resume') and args.resume:
  513. config['resume_from_checkpoint'] = True
  514. config['checkpoint_path'] = args.resume
  515. if hasattr(args, 'model_format') and args.model_format:
  516. config['model_format'] = f".{args.model_format}"
  517. # Validate and convert to TrainerConfig
  518. self._validate_config(config, trainer_name)
  519. return self._dict_to_trainer_config(config)
  520. def generate_config_template(self, trainer_name: str, filepath: str = None) -> str:
  521. """
  522. Generate configuration file template.
  523. Args:
  524. trainer_name: Name of the trainer
  525. filepath: Optional file path to save template
  526. Returns:
  527. Configuration template as string
  528. """
  529. template = {
  530. "# Configuration for Trixy ML Trainer": None,
  531. "trainer_name": trainer_name,
  532. "model_name": f"{trainer_name}_model",
  533. "description": f"Configuration for {trainer_name} trainer",
  534. "# Data settings": None,
  535. "data_dir": f"./trainer/data/{trainer_name}",
  536. "output_dir": "./models",
  537. "validation_split": 0.2,
  538. "test_split": 0.1,
  539. "# Training parameters": None,
  540. "batch_size": 32,
  541. "learning_rate": 0.001,
  542. "num_epochs": 100,
  543. "early_stopping_patience": 10,
  544. "# Model settings": None,
  545. "model_format": ".pth",
  546. "use_mixed_precision": True,
  547. "use_password_protection": True,
  548. "# Audio processing": None,
  549. "sample_rate": 16000,
  550. "audio_length": 1.5,
  551. "n_mels": 40,
  552. "n_fft": 512,
  553. "hop_length": 160,
  554. "# Data augmentation": None,
  555. "use_augmentation": True,
  556. "noise_factor": 0.1,
  557. "speed_factor": 0.1,
  558. "pitch_factor": 0.1,
  559. "volume_factor": 0.2,
  560. "# Hardware": None,
  561. "device": "auto",
  562. "num_workers": 4,
  563. "# Logging": None,
  564. "log_level": "INFO",
  565. "save_checkpoints": True,
  566. "checkpoint_interval": 10
  567. }
  568. # Add trainer-specific settings
  569. if trainer_name == "wakeword":
  570. template.update({
  571. "# Wakeword specific": None,
  572. "wakeword_classes": {
  573. 0: "custom",
  574. 1: "system_command",
  575. 2: "negative"
  576. },
  577. "detection_threshold": 0.5
  578. })
  579. elif trainer_name == "voice_recognition":
  580. template.update({
  581. "# Voice recognition specific": None,
  582. "num_speakers": 2,
  583. "embedding_dimension": 256,
  584. "architecture": "ecapa_tdnn"
  585. })
  586. # Convert to JSON string
  587. template_str = "{\n"
  588. for key, value in template.items():
  589. if value is None: # Comment line
  590. template_str += f" \"{key}\",\n"
  591. elif isinstance(value, str):
  592. template_str += f" \"{key}\": \"{value}\",\n"
  593. elif isinstance(value, dict):
  594. template_str += f" \"{key}\": {json.dumps(value, indent=4)},\n"
  595. else:
  596. template_str += f" \"{key}\": {json.dumps(value)},\n"
  597. template_str = template_str.rstrip(',\n') + "\n}"
  598. # Save to file if requested
  599. if filepath:
  600. with open(filepath, 'w', encoding='utf-8') as f:
  601. f.write(template_str)
  602. self.logger.info(f"Configuration template saved to: {filepath}")
  603. return template_str
  604. def list_available_configs(self) -> List[str]:
  605. """List all available configuration files."""
  606. configs = []
  607. if self.config_dir.exists():
  608. for pattern in ['*.json', '*.yml', '*.yaml']:
  609. configs.extend([f.stem for f in self.config_dir.glob(pattern)])
  610. return sorted(list(set(configs)))
  611. def get_config_info(self, config_name: str) -> Dict[str, Any]:
  612. """Get information about a configuration file."""
  613. config_paths = [
  614. self.config_dir / f"{config_name}.json",
  615. self.config_dir / f"{config_name}.yml",
  616. self.config_dir / f"{config_name}.yaml"
  617. ]
  618. for config_path in config_paths:
  619. if config_path.exists():
  620. config = self.load_config_file(config_path)
  621. return {
  622. 'name': config_name,
  623. 'path': str(config_path),
  624. 'format': config_path.suffix,
  625. 'size': config_path.stat().st_size,
  626. 'trainer_name': config.get('trainer_name', 'unknown'),
  627. 'model_name': config.get('model_name', 'unknown'),
  628. 'description': config.get('description', ''),
  629. 'keys': list(config.keys())
  630. }
  631. raise ConfigError(f"Configuration not found: {config_name}")
  632. def create_trainer_cli(trainer_name: str) -> argparse.ArgumentParser:
  633. """
  634. Create command-line interface for a trainer.
  635. Args:
  636. trainer_name: Name of the trainer
  637. Returns:
  638. Configured ArgumentParser
  639. """
  640. config_manager = ConfigManager()
  641. return config_manager.create_argument_parser(trainer_name)
  642. def load_config_from_args(args: argparse.Namespace, trainer_name: str) -> TrainerConfig:
  643. """
  644. Load configuration from command-line arguments.
  645. Args:
  646. args: Parsed command-line arguments
  647. trainer_name: Name of the trainer
  648. Returns:
  649. TrainerConfig instance
  650. """
  651. config_manager = ConfigManager()
  652. return config_manager.parse_args_to_config(args, trainer_name)