model_loader.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. """
  2. Secure model loader for Voice Recognition system.
  3. This module provides secure loading of PyTorch models with password protection
  4. and metadata management. Supports .pth files as encrypted ZIP archives with
  5. embedded metadata.
  6. """
  7. import os
  8. import json
  9. import logging
  10. import hashlib
  11. import zipfile
  12. import tempfile
  13. import shutil
  14. from typing import Dict, Any, Optional, List, Union, Tuple
  15. from dataclasses import dataclass, asdict
  16. from pathlib import Path
  17. from datetime import datetime
  18. import torch
  19. import torch.nn as nn
  20. from cryptography.fernet import Fernet
  21. from cryptography.hazmat.primitives import hashes
  22. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  23. import base64
  24. from .model_architectures import create_model_from_config, ModelArchitectureType
  25. # Configure logging
  26. logger = logging.getLogger(__name__)
  27. class VRModelLoadError(Exception):
  28. """Exception raised when model loading fails."""
  29. pass
  30. @dataclass
  31. class VRModelMetadata:
  32. """Metadata for voice recognition models."""
  33. # Model information
  34. model_name: str
  35. model_version: str
  36. architecture_type: str
  37. embedding_dim: int
  38. num_speakers: int
  39. # Speaker mapping
  40. speaker_mapping: Dict[str, str] # speaker_id -> speaker_name
  41. speaker_ids: List[str] # ordered list of speaker IDs
  42. # Training information
  43. training_date: str
  44. training_duration_hours: float
  45. training_epochs: int
  46. training_samples: int
  47. final_accuracy: float
  48. final_loss: float
  49. # Audio configuration
  50. sample_rate: int
  51. audio_length_seconds: float
  52. feature_type: str
  53. feature_dim: int
  54. # Model configuration
  55. model_config: Dict[str, Any]
  56. # System information
  57. trained_on_device: str
  58. trained_by: str
  59. creation_timestamp: str
  60. file_hash: str
  61. # Compatibility information
  62. min_trixy_version: str = "1.0.0"
  63. model_format: str = "pytorch"
  64. def __post_init__(self):
  65. """Validate metadata after initialization."""
  66. if self.num_speakers != len(self.speaker_mapping):
  67. logger.warning(f"Mismatch: num_speakers={self.num_speakers}, speaker_mapping={len(self.speaker_mapping)}")
  68. if self.architecture_type not in [arch.value for arch in ModelArchitectureType]:
  69. raise ValueError(f"Unsupported architecture type: {self.architecture_type}")
  70. def to_dict(self) -> Dict[str, Any]:
  71. """Convert metadata to dictionary."""
  72. return asdict(self)
  73. @classmethod
  74. def from_dict(cls, data: Dict[str, Any]) -> 'VRModelMetadata':
  75. """Create metadata from dictionary."""
  76. return cls(**data)
  77. def validate_compatibility(self, required_speakers: Optional[List[str]] = None) -> List[str]:
  78. """
  79. Validate model compatibility.
  80. Args:
  81. required_speakers: List of required speaker names
  82. Returns:
  83. List of compatibility warnings
  84. """
  85. warnings = []
  86. # Check speaker compatibility
  87. if required_speakers:
  88. available_speakers = set(self.speaker_mapping.values())
  89. required_set = set(required_speakers)
  90. missing_speakers = required_set - available_speakers
  91. if missing_speakers:
  92. warnings.append(f"Missing speakers: {list(missing_speakers)}")
  93. extra_speakers = available_speakers - required_set
  94. if extra_speakers:
  95. warnings.append(f"Extra speakers in model: {list(extra_speakers)}")
  96. # Check feature compatibility
  97. if self.feature_dim not in [40, 80]: # Common dimensions
  98. warnings.append(f"Unusual feature dimension: {self.feature_dim}")
  99. if self.sample_rate != 16000:
  100. warnings.append(f"Non-standard sample rate: {self.sample_rate}Hz")
  101. return warnings
  102. class VRModelLoader:
  103. """Basic model loader without encryption."""
  104. def __init__(self, device: str = "auto"):
  105. """
  106. Initialize model loader.
  107. Args:
  108. device: Device to load model on
  109. """
  110. if device == "auto":
  111. if torch.cuda.is_available():
  112. self.device = "cuda"
  113. elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
  114. self.device = "mps"
  115. else:
  116. self.device = "cpu"
  117. else:
  118. self.device = device
  119. logger.info(f"Model loader initialized with device: {self.device}")
  120. def load_metadata(self, password: str = "") -> VRModelMetadata:
  121. """
  122. Load only metadata from model file without loading the full model.
  123. Args:
  124. password: Password for encrypted models (currently unused for basic loader)
  125. Returns:
  126. Model metadata
  127. """
  128. # This is called during initialization with model_path passed to __init__
  129. # We need to extract model_path from self or accept it as parameter
  130. # For now, this is a placeholder that raises an error
  131. raise NotImplementedError("load_metadata requires model_path - use SecureVRModelLoader instead")
  132. def load_model(self, model_path: str, password: str = "") -> Tuple[nn.Module, VRModelMetadata]:
  133. """
  134. Load model and metadata from file (ZIP or direct .pth).
  135. Args:
  136. model_path: Path to model file
  137. password: Password for encrypted ZIP files
  138. Returns:
  139. Tuple of (model, metadata)
  140. """
  141. if not os.path.exists(model_path):
  142. raise VRModelLoadError(f"Model file not found: {model_path}")
  143. logger.info(f"Loading model from: {model_path}")
  144. # Check if it's a ZIP file (trainer format)
  145. try:
  146. with zipfile.ZipFile(model_path, 'r') as zip_file:
  147. # It's a ZIP file - load from metafile.json + model.pth
  148. return self._load_from_zip(zip_file, password)
  149. except zipfile.BadZipFile:
  150. # Not a ZIP - try loading as direct PyTorch checkpoint
  151. return self._load_from_checkpoint(model_path)
  152. def _load_from_zip(self, zip_file: zipfile.ZipFile, password: str) -> Tuple[nn.Module, VRModelMetadata]:
  153. """Load model from ZIP archive with metafile.json."""
  154. import io
  155. # Set password if provided
  156. if password:
  157. zip_file.setpassword(password.encode('utf-8'))
  158. # Load metadata
  159. try:
  160. with zip_file.open('metafile.json') as meta_file:
  161. trainer_metadata = json.load(meta_file)
  162. metadata = self._convert_trainer_metadata(trainer_metadata)
  163. except KeyError:
  164. raise VRModelLoadError("metafile.json not found in model ZIP")
  165. # Load model state
  166. try:
  167. with zip_file.open('model.pth') as model_file:
  168. model_bytes = model_file.read()
  169. state_dict = torch.load(
  170. io.BytesIO(model_bytes),
  171. map_location=self.device,
  172. weights_only=True
  173. )
  174. except KeyError:
  175. raise VRModelLoadError("model.pth not found in model ZIP")
  176. # Create model
  177. model = self._create_model_from_metadata(metadata)
  178. model.load_state_dict(state_dict, strict=False)
  179. model.to(self.device)
  180. model.eval()
  181. logger.info(f"Model loaded from ZIP: {metadata.model_name}")
  182. return model, metadata
  183. def _load_from_checkpoint(self, model_path: str) -> Tuple[nn.Module, VRModelMetadata]:
  184. """Load model from direct PyTorch checkpoint file."""
  185. try:
  186. checkpoint = torch.load(model_path, map_location=self.device)
  187. except Exception as e:
  188. raise VRModelLoadError(f"Failed to load PyTorch model: {e}")
  189. # Extract metadata
  190. if 'metadata' not in checkpoint:
  191. raise VRModelLoadError("Model file missing metadata")
  192. metadata = VRModelMetadata.from_dict(checkpoint['metadata'])
  193. # Create model architecture
  194. model = self._create_model_from_metadata(metadata)
  195. # Load model weights
  196. if 'model_state_dict' in checkpoint:
  197. try:
  198. model.load_state_dict(checkpoint['model_state_dict'])
  199. except Exception as e:
  200. raise VRModelLoadError(f"Failed to load model weights: {e}")
  201. else:
  202. raise VRModelLoadError("Model file missing state_dict")
  203. model.to(self.device)
  204. model.eval()
  205. logger.info(f"Model loaded from checkpoint: {metadata.model_name}")
  206. return model, metadata
  207. def _convert_trainer_metadata(self, trainer_meta: Dict[str, Any]) -> VRModelMetadata:
  208. """Convert trainer metafile.json format to VRModelMetadata format."""
  209. # Extract nested data
  210. training_stats = trainer_meta.get('training_statistics', {})
  211. audio_cfg = trainer_meta.get('audio_config', {})
  212. arch_info = trainer_meta.get('architecture_info', {})
  213. hardware = trainer_meta.get('hardware_info', {})
  214. model_specific = trainer_meta.get('model_specific', {})
  215. dataset_info = trainer_meta.get('dataset_info', {})
  216. # Build speaker mapping from class_names or model_specific
  217. speaker_ids = dataset_info.get('class_names', [])
  218. if not speaker_ids and 'speakers' in model_specific:
  219. speaker_ids = model_specific['speakers']
  220. speaker_mapping = {str(i): name for i, name in enumerate(speaker_ids)}
  221. num_speakers = len(speaker_ids)
  222. # Map to VRModelMetadata fields
  223. # Convert architecture name to lowercase to match enum values
  224. architecture_name = arch_info.get('architecture_name', 'ecapa_tdnn')
  225. if isinstance(architecture_name, str):
  226. architecture_name = architecture_name.lower()
  227. return VRModelMetadata(
  228. model_name=trainer_meta.get('model_name', 'unknown'),
  229. model_version=trainer_meta.get('version', '1.0.0'),
  230. architecture_type=architecture_name,
  231. embedding_dim=arch_info.get('embedding_dim', 192),
  232. num_speakers=num_speakers,
  233. speaker_mapping=speaker_mapping,
  234. speaker_ids=speaker_ids,
  235. training_date=trainer_meta.get('creation_timestamp', ''),
  236. training_duration_hours=training_stats.get('training_duration_seconds', 0.0) / 3600.0,
  237. training_epochs=training_stats.get('total_epochs', 0),
  238. training_samples=dataset_info.get('total_samples', 0),
  239. final_accuracy=training_stats.get('best_val_accuracy', 0.0),
  240. final_loss=training_stats.get('best_val_loss', 0.0),
  241. sample_rate=audio_cfg.get('sample_rate', 16000),
  242. audio_length_seconds=audio_cfg.get('audio_length_seconds', 3.0),
  243. feature_type=audio_cfg.get('feature_type', 'mel_spectrogram'),
  244. feature_dim=audio_cfg.get('n_mels', 80),
  245. model_config=arch_info,
  246. trained_on_device=hardware.get('gpu_names', ['cpu'])[0] if hardware.get('gpu_names') else 'cpu',
  247. trained_by=trainer_meta.get('author', 'Unknown'),
  248. creation_timestamp=trainer_meta.get('creation_timestamp', ''),
  249. file_hash='',
  250. min_trixy_version='1.0.0',
  251. model_format='pytorch'
  252. )
  253. def _create_model_from_metadata(self, metadata: VRModelMetadata) -> nn.Module:
  254. """Create model architecture from metadata."""
  255. # Extract model_config and remove keys that we're passing explicitly
  256. model_config = metadata.model_config.copy() if metadata.model_config else {}
  257. # Remove keys that might conflict with explicit parameters
  258. model_config.pop('architecture_type', None)
  259. model_config.pop('input_dim', None)
  260. model_config.pop('embedding_dim', None)
  261. model_config.pop('num_speakers', None)
  262. return create_model_from_config(
  263. architecture_type=metadata.architecture_type,
  264. input_dim=metadata.feature_dim,
  265. embedding_dim=metadata.embedding_dim,
  266. num_speakers=metadata.num_speakers,
  267. **model_config
  268. )
  269. def save_model(self,
  270. model: nn.Module,
  271. metadata: VRModelMetadata,
  272. save_path: str,
  273. optimizer: Optional[torch.optim.Optimizer] = None) -> None:
  274. """
  275. Save model with metadata.
  276. Args:
  277. model: Model to save
  278. metadata: Model metadata
  279. save_path: Path to save model
  280. optimizer: Optional optimizer state
  281. """
  282. # Prepare checkpoint
  283. checkpoint = {
  284. 'model_state_dict': model.state_dict(),
  285. 'metadata': metadata.to_dict(),
  286. 'save_timestamp': datetime.now().isoformat()
  287. }
  288. if optimizer is not None:
  289. checkpoint['optimizer_state_dict'] = optimizer.state_dict()
  290. # Create directory if needed
  291. Path(save_path).parent.mkdir(parents=True, exist_ok=True)
  292. # Save model
  293. try:
  294. torch.save(checkpoint, save_path)
  295. logger.info(f"Model saved to: {save_path}")
  296. except Exception as e:
  297. raise VRModelLoadError(f"Failed to save model: {e}")
  298. class SecureVRModelLoader(VRModelLoader):
  299. """Secure model loader with password protection and ZIP support."""
  300. def __init__(self, device: str = "auto"):
  301. super().__init__(device)
  302. self.temp_dir = None
  303. def load_model(self,
  304. model_path: str,
  305. password: Optional[str] = None) -> Tuple[nn.Module, VRModelMetadata]:
  306. """
  307. Load encrypted model from ZIP archive.
  308. Args:
  309. model_path: Path to encrypted model file
  310. password: Password for decryption
  311. Returns:
  312. Tuple of (model, metadata)
  313. """
  314. if not os.path.exists(model_path):
  315. raise VRModelLoadError(f"Model file not found: {model_path}")
  316. logger.info(f"Loading secure model from: {model_path}")
  317. # Create temporary directory
  318. self.temp_dir = tempfile.mkdtemp(prefix="trixy_vr_model_")
  319. try:
  320. # Extract and decrypt model
  321. if model_path.endswith('.zip') or self._is_zip_file(model_path):
  322. model_data, metadata_data = self._load_from_zip(model_path, password)
  323. elif model_path.endswith('.pth'):
  324. # Try to load as encrypted ZIP (PTH files can be ZIP archives)
  325. try:
  326. model_data, metadata_data = self._load_from_zip(model_path, password)
  327. except:
  328. # Fall back to regular PyTorch loading
  329. return super().load_model(model_path)
  330. else:
  331. raise VRModelLoadError(f"Unsupported model format: {model_path}")
  332. # Parse metadata
  333. metadata = VRModelMetadata.from_dict(metadata_data)
  334. # Create model architecture
  335. model = self._create_model_from_metadata(metadata)
  336. # Load model weights
  337. try:
  338. model.load_state_dict(model_data['model_state_dict'])
  339. except Exception as e:
  340. raise VRModelLoadError(f"Failed to load model weights: {e}")
  341. model.to(self.device)
  342. model.eval()
  343. logger.info(f"Secure model loaded successfully: {metadata.model_name}")
  344. return model, metadata
  345. finally:
  346. # Clean up temporary directory
  347. if self.temp_dir and os.path.exists(self.temp_dir):
  348. shutil.rmtree(self.temp_dir)
  349. self.temp_dir = None
  350. def _is_zip_file(self, file_path: str) -> bool:
  351. """Check if file is a ZIP archive."""
  352. try:
  353. with zipfile.ZipFile(file_path, 'r') as zf:
  354. return True
  355. except:
  356. return False
  357. def _load_from_zip(self, zip_path: str, password: Optional[str] = None) -> Tuple[Dict[str, Any], Dict[str, Any]]:
  358. """Load model and metadata from ZIP archive."""
  359. try:
  360. with zipfile.ZipFile(zip_path, 'r') as zf:
  361. # List files in archive
  362. file_list = zf.namelist()
  363. logger.debug(f"Files in archive: {file_list}")
  364. # Find metadata file
  365. metadata_file = None
  366. for filename in file_list:
  367. if filename.endswith('metadata.json') or filename == 'metadata.json':
  368. metadata_file = filename
  369. break
  370. if not metadata_file:
  371. raise VRModelLoadError("No metadata.json found in model archive")
  372. # Find model file
  373. model_file = None
  374. for filename in file_list:
  375. if filename.endswith('.pth') or filename.endswith('.pt'):
  376. model_file = filename
  377. break
  378. if not model_file:
  379. raise VRModelLoadError("No model file (.pth/.pt) found in archive")
  380. # Extract files
  381. if password:
  382. # Decrypt files
  383. metadata_data = self._decrypt_file_content(zf.read(metadata_file), password)
  384. model_content = self._decrypt_file_content(zf.read(model_file), password)
  385. else:
  386. metadata_data = zf.read(metadata_file).decode('utf-8')
  387. model_content = zf.read(model_file)
  388. # Parse metadata
  389. metadata_dict = json.loads(metadata_data)
  390. # Load model from bytes
  391. model_temp_path = os.path.join(self.temp_dir, 'model.pth')
  392. with open(model_temp_path, 'wb') as f:
  393. f.write(model_content)
  394. model_data = torch.load(model_temp_path, map_location=self.device)
  395. return model_data, metadata_dict
  396. except zipfile.BadZipFile:
  397. raise VRModelLoadError("Invalid ZIP archive")
  398. except Exception as e:
  399. raise VRModelLoadError(f"Failed to load from ZIP: {e}")
  400. def _decrypt_file_content(self, encrypted_content: bytes, password: str) -> str:
  401. """Decrypt file content using password."""
  402. try:
  403. # Generate key from password
  404. key = self._generate_key_from_password(password)
  405. # Decrypt content
  406. fernet = Fernet(key)
  407. decrypted_content = fernet.decrypt(encrypted_content)
  408. return decrypted_content.decode('utf-8')
  409. except Exception as e:
  410. raise VRModelLoadError(f"Failed to decrypt content: {e}")
  411. def _generate_key_from_password(self, password: str) -> bytes:
  412. """Generate encryption key from password."""
  413. # Use a fixed salt for consistency (in production, should be stored)
  414. salt = b'trixy_voice_recognition_salt_2024'
  415. kdf = PBKDF2HMAC(
  416. algorithm=hashes.SHA256(),
  417. length=32,
  418. salt=salt,
  419. iterations=100000,
  420. )
  421. key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
  422. return key
  423. def save_secure_model(self,
  424. model: nn.Module,
  425. metadata: VRModelMetadata,
  426. save_path: str,
  427. password: Optional[str] = None,
  428. optimizer: Optional[torch.optim.Optimizer] = None) -> None:
  429. """
  430. Save model as encrypted ZIP archive.
  431. Args:
  432. model: Model to save
  433. metadata: Model metadata
  434. save_path: Path to save encrypted model
  435. password: Password for encryption
  436. optimizer: Optional optimizer state
  437. """
  438. # Create temporary directory
  439. temp_dir = tempfile.mkdtemp(prefix="trixy_vr_save_")
  440. try:
  441. # Prepare model checkpoint
  442. checkpoint = {
  443. 'model_state_dict': model.state_dict(),
  444. 'save_timestamp': datetime.now().isoformat()
  445. }
  446. if optimizer is not None:
  447. checkpoint['optimizer_state_dict'] = optimizer.state_dict()
  448. # Save model to temporary file
  449. model_temp_path = os.path.join(temp_dir, 'model.pth')
  450. torch.save(checkpoint, model_temp_path)
  451. # Prepare metadata
  452. metadata_dict = metadata.to_dict()
  453. metadata_json = json.dumps(metadata_dict, indent=2)
  454. # Create ZIP archive
  455. Path(save_path).parent.mkdir(parents=True, exist_ok=True)
  456. with zipfile.ZipFile(save_path, 'w', zipfile.ZIP_DEFLATED) as zf:
  457. if password:
  458. # Encrypt files
  459. encrypted_metadata = self._encrypt_content(metadata_json, password)
  460. with open(model_temp_path, 'rb') as f:
  461. model_content = f.read()
  462. encrypted_model = self._encrypt_content(model_content, password)
  463. # Add encrypted files to archive
  464. zf.writestr('metadata.json', encrypted_metadata)
  465. zf.writestr('model.pth', encrypted_model)
  466. else:
  467. # Add unencrypted files
  468. zf.writestr('metadata.json', metadata_json)
  469. zf.write(model_temp_path, 'model.pth')
  470. # Add readme
  471. readme_content = f"""
  472. Trixy Voice Recognition Model
  473. ============================
  474. Model Name: {metadata.model_name}
  475. Architecture: {metadata.architecture_type}
  476. Speakers: {metadata.num_speakers}
  477. Created: {metadata.creation_timestamp}
  478. This is a secure voice recognition model for the Trixy voice assistant.
  479. Load using the SecureVRModelLoader class.
  480. """
  481. zf.writestr('README.txt', readme_content)
  482. logger.info(f"Secure model saved to: {save_path}")
  483. finally:
  484. # Clean up
  485. if os.path.exists(temp_dir):
  486. shutil.rmtree(temp_dir)
  487. def _encrypt_content(self, content: Union[str, bytes], password: str) -> bytes:
  488. """Encrypt content using password."""
  489. if isinstance(content, str):
  490. content = content.encode('utf-8')
  491. # Generate key from password
  492. key = self._generate_key_from_password(password)
  493. # Encrypt content
  494. fernet = Fernet(key)
  495. encrypted_content = fernet.encrypt(content)
  496. return encrypted_content
  497. def validate_vr_model_file(model_path: str, password: Optional[str] = None) -> Dict[str, Any]:
  498. """
  499. Validate voice recognition model file.
  500. Args:
  501. model_path: Path to model file
  502. password: Password if model is encrypted
  503. Returns:
  504. Validation results
  505. """
  506. results = {
  507. 'is_valid': False,
  508. 'file_exists': False,
  509. 'file_size_mb': 0.0,
  510. 'is_encrypted': False,
  511. 'has_metadata': False,
  512. 'metadata': None,
  513. 'errors': [],
  514. 'warnings': []
  515. }
  516. try:
  517. # Check file existence
  518. if not os.path.exists(model_path):
  519. results['errors'].append(f"File not found: {model_path}")
  520. return results
  521. results['file_exists'] = True
  522. results['file_size_mb'] = os.path.getsize(model_path) / (1024 * 1024)
  523. # Try to load model
  524. loader = SecureVRModelLoader()
  525. try:
  526. model, metadata = loader.load_model(model_path, password)
  527. results['has_metadata'] = True
  528. results['metadata'] = metadata.to_dict()
  529. results['is_valid'] = True
  530. # Validate metadata
  531. validation_warnings = metadata.validate_compatibility()
  532. results['warnings'].extend(validation_warnings)
  533. except VRModelLoadError as e:
  534. if "decrypt" in str(e).lower():
  535. results['is_encrypted'] = True
  536. results['errors'].append("Model is encrypted - password required or incorrect")
  537. else:
  538. results['errors'].append(f"Model loading failed: {e}")
  539. except Exception as e:
  540. results['errors'].append(f"Validation error: {e}")
  541. return results
  542. def create_vr_model_metadata(model_name: str,
  543. architecture_type: str,
  544. embedding_dim: int,
  545. speaker_mapping: Dict[str, str],
  546. **kwargs) -> VRModelMetadata:
  547. """
  548. Create model metadata with defaults.
  549. Args:
  550. model_name: Name of the model
  551. architecture_type: Type of architecture
  552. embedding_dim: Embedding dimension
  553. speaker_mapping: Mapping of speaker IDs to names
  554. **kwargs: Additional metadata fields
  555. Returns:
  556. Model metadata
  557. """
  558. speaker_ids = list(speaker_mapping.keys())
  559. metadata = VRModelMetadata(
  560. model_name=model_name,
  561. model_version=kwargs.get('model_version', '1.0.0'),
  562. architecture_type=architecture_type,
  563. embedding_dim=embedding_dim,
  564. num_speakers=len(speaker_mapping),
  565. speaker_mapping=speaker_mapping,
  566. speaker_ids=speaker_ids,
  567. training_date=kwargs.get('training_date', datetime.now().strftime('%Y-%m-%d')),
  568. training_duration_hours=kwargs.get('training_duration_hours', 0.0),
  569. training_epochs=kwargs.get('training_epochs', 0),
  570. training_samples=kwargs.get('training_samples', 0),
  571. final_accuracy=kwargs.get('final_accuracy', 0.0),
  572. final_loss=kwargs.get('final_loss', 0.0),
  573. sample_rate=kwargs.get('sample_rate', 16000),
  574. audio_length_seconds=kwargs.get('audio_length_seconds', 3.0),
  575. feature_type=kwargs.get('feature_type', 'log_mel'),
  576. feature_dim=kwargs.get('feature_dim', 40),
  577. model_config=kwargs.get('model_config', {}),
  578. trained_on_device=kwargs.get('trained_on_device', 'unknown'),
  579. trained_by=kwargs.get('trained_by', 'trixy_trainer'),
  580. creation_timestamp=datetime.now().isoformat(),
  581. file_hash=kwargs.get('file_hash', ''),
  582. min_trixy_version=kwargs.get('min_trixy_version', '1.0.0'),
  583. model_format=kwargs.get('model_format', 'pytorch')
  584. )
  585. return metadata
  586. # Example usage and testing
  587. def test_model_loader():
  588. """Test model loading functionality."""
  589. from .model_architectures import ECADATDNNModel
  590. # Create sample model
  591. model = ECADATDNNModel(
  592. input_dim=40,
  593. embedding_dim=192,
  594. num_speakers=4
  595. )
  596. # Create sample metadata
  597. speaker_mapping = {
  598. "spk_001": "Alice",
  599. "spk_002": "Bob",
  600. "spk_003": "Charlie",
  601. "spk_004": "Diana"
  602. }
  603. metadata = create_vr_model_metadata(
  604. model_name="test_ecapa_model",
  605. architecture_type="ecapa_tdnn",
  606. embedding_dim=192,
  607. speaker_mapping=speaker_mapping,
  608. training_epochs=100,
  609. final_accuracy=0.95
  610. )
  611. # Test basic saving and loading
  612. temp_path = "/tmp/test_vr_model.pth"
  613. loader = VRModelLoader()
  614. loader.save_model(model, metadata, temp_path)
  615. loaded_model, loaded_metadata = loader.load_model(temp_path)
  616. print(f"Model loaded: {loaded_metadata.model_name}")
  617. print(f"Speakers: {loaded_metadata.speaker_mapping}")
  618. # Test secure saving and loading
  619. secure_loader = SecureVRModelLoader()
  620. secure_path = "/tmp/test_vr_model_secure.pth"
  621. password = "test_password_123"
  622. secure_loader.save_secure_model(model, metadata, secure_path, password)
  623. loaded_secure_model, loaded_secure_metadata = secure_loader.load_model(secure_path, password)
  624. print(f"Secure model loaded: {loaded_secure_metadata.model_name}")
  625. # Clean up
  626. if os.path.exists(temp_path):
  627. os.remove(temp_path)
  628. if os.path.exists(secure_path):
  629. os.remove(secure_path)
  630. if __name__ == "__main__":
  631. test_model_loader()