model_formats.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. """
  2. Model format handlers for different file formats (.pth, .pt, .onnx).
  3. This module provides handlers for saving and loading models in various formats,
  4. including password protection and metadata integration as specified in the
  5. project requirements.
  6. """
  7. import os
  8. import json
  9. import zipfile
  10. import tempfile
  11. import hashlib
  12. import io
  13. from abc import ABC, abstractmethod
  14. from typing import Dict, Any, Optional, Union, BinaryIO
  15. from pathlib import Path
  16. import torch
  17. import torch.nn as nn
  18. from cryptography.fernet import Fernet
  19. from cryptography.hazmat.primitives import hashes
  20. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  21. import base64
  22. import logging
  23. try:
  24. import onnx
  25. import onnxruntime as ort
  26. ONNX_AVAILABLE = True
  27. except ImportError:
  28. ONNX_AVAILABLE = False
  29. onnx = None
  30. ort = None
  31. class ModelFormatError(Exception):
  32. """Exception raised for model format related errors."""
  33. pass
  34. class EncryptionManager:
  35. """Handles encryption and decryption for password-protected models."""
  36. @staticmethod
  37. def generate_key_from_password(password: str, salt: bytes = None) -> bytes:
  38. """Generate encryption key from password using PBKDF2."""
  39. if salt is None:
  40. salt = os.urandom(16)
  41. kdf = PBKDF2HMAC(
  42. algorithm=hashes.SHA256(),
  43. length=32,
  44. salt=salt,
  45. iterations=100000,
  46. )
  47. key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
  48. return key, salt
  49. @staticmethod
  50. def encrypt_data(data: bytes, password: str) -> bytes:
  51. """Encrypt data with password."""
  52. key, salt = EncryptionManager.generate_key_from_password(password)
  53. f = Fernet(key)
  54. encrypted_data = f.encrypt(data)
  55. # Prepend salt to encrypted data
  56. return salt + encrypted_data
  57. @staticmethod
  58. def decrypt_data(encrypted_data: bytes, password: str) -> bytes:
  59. """Decrypt data with password."""
  60. # Extract salt from the beginning
  61. salt = encrypted_data[:16]
  62. encrypted_content = encrypted_data[16:]
  63. key, _ = EncryptionManager.generate_key_from_password(password, salt)
  64. f = Fernet(key)
  65. return f.decrypt(encrypted_content)
  66. class ModelFormatHandler(ABC):
  67. """Abstract base class for model format handlers."""
  68. def __init__(self, logger: Optional[logging.Logger] = None):
  69. self.logger = logger or logging.getLogger(__name__)
  70. @abstractmethod
  71. def save_model(self, model: nn.Module, filepath: str, metadata: Dict[str, Any],
  72. password: Optional[str] = None, **kwargs) -> bool:
  73. """Save model with metadata and optional password protection."""
  74. pass
  75. @abstractmethod
  76. def load_model(self, filepath: str, password: Optional[str] = None,
  77. map_location: Optional[str] = None) -> tuple:
  78. """Load model and return (model_state_dict, metadata)."""
  79. pass
  80. @abstractmethod
  81. def extract_metadata(self, filepath: str, password: Optional[str] = None) -> Dict[str, Any]:
  82. """Extract metadata from model file without loading the full model."""
  83. pass
  84. @abstractmethod
  85. def update_metadata(self, filepath: str, new_metadata: Dict[str, Any],
  86. password: Optional[str] = None) -> bool:
  87. """Update metadata in existing model file."""
  88. pass
  89. def _generate_file_hash(self, filepath: str) -> str:
  90. """Generate SHA256 hash of the file."""
  91. hash_sha256 = hashlib.sha256()
  92. with open(filepath, "rb") as f:
  93. for chunk in iter(lambda: f.read(4096), b""):
  94. hash_sha256.update(chunk)
  95. return hash_sha256.hexdigest()
  96. class PTHHandler(ModelFormatHandler):
  97. """Handler for .pth files (ZIP archives with JSON metadata)."""
  98. def save_model(self, model: nn.Module, filepath: str, metadata: Dict[str, Any],
  99. password: Optional[str] = None, **kwargs) -> bool:
  100. """
  101. Save model as .pth file (ZIP archive) with metadata.json.
  102. Args:
  103. model: PyTorch model to save
  104. filepath: Path to save the model
  105. metadata: Metadata dictionary to include
  106. password: Optional password for encryption
  107. **kwargs: Additional arguments
  108. Returns:
  109. bool: True if successful
  110. """
  111. try:
  112. # Ensure directory exists
  113. os.makedirs(os.path.dirname(filepath), exist_ok=True)
  114. # Create temporary directory for building the archive
  115. with tempfile.TemporaryDirectory() as temp_dir:
  116. # Save model state dict
  117. model_path = os.path.join(temp_dir, "model.pth")
  118. torch.save(model.state_dict(), model_path)
  119. # Save metadata
  120. metadata_path = os.path.join(temp_dir, "metafile.json")
  121. metadata['file_format'] = 'pth'
  122. metadata['pytorch_version'] = torch.__version__
  123. metadata['file_hash'] = None # Will be calculated after creation
  124. with open(metadata_path, 'w', encoding='utf-8') as f:
  125. json.dump(metadata, f, indent=2, ensure_ascii=False)
  126. # Create ZIP archive
  127. with zipfile.ZipFile(filepath, 'w', zipfile.ZIP_DEFLATED) as zipf:
  128. zipf.write(model_path, "model.pth")
  129. zipf.write(metadata_path, "metafile.json")
  130. # Add any additional files from kwargs
  131. for key, value in kwargs.items():
  132. if key.startswith('add_file_'):
  133. file_path = value.get('path')
  134. archive_name = value.get('name', os.path.basename(file_path))
  135. if os.path.exists(file_path):
  136. zipf.write(file_path, archive_name)
  137. # Apply password protection if requested
  138. if password:
  139. self._encrypt_file(filepath, password)
  140. # Update file hash in metadata
  141. file_hash = self._generate_file_hash(filepath)
  142. self._update_file_hash(filepath, file_hash, password)
  143. self.logger.info(f"Model saved successfully: {filepath}")
  144. return True
  145. except Exception as e:
  146. self.logger.error(f"Failed to save model: {str(e)}")
  147. return False
  148. def load_model(self, filepath: str, password: Optional[str] = None,
  149. map_location: Optional[str] = None) -> tuple:
  150. """
  151. Load model from .pth file.
  152. Returns:
  153. tuple: (model_state_dict, metadata)
  154. """
  155. try:
  156. # Handle password protection
  157. if password:
  158. filepath = self._decrypt_file(filepath, password)
  159. with zipfile.ZipFile(filepath, 'r') as zipf:
  160. # Load metadata
  161. with zipf.open("metafile.json") as f:
  162. metadata = json.load(f)
  163. # Load model
  164. with zipf.open("model.pth") as f:
  165. model_data = f.read()
  166. model_state_dict = torch.load(
  167. io.BytesIO(model_data),
  168. map_location=map_location
  169. )
  170. self.logger.info(f"Model loaded successfully: {filepath}")
  171. return model_state_dict, metadata
  172. except Exception as e:
  173. self.logger.error(f"Failed to load model: {str(e)}")
  174. raise ModelFormatError(f"Failed to load model: {str(e)}")
  175. def extract_metadata(self, filepath: str, password: Optional[str] = None) -> Dict[str, Any]:
  176. """Extract metadata from .pth file without loading the model."""
  177. try:
  178. # Handle password protection
  179. if password:
  180. filepath = self._decrypt_file(filepath, password)
  181. with zipfile.ZipFile(filepath, 'r') as zipf:
  182. with zipf.open("metafile.json") as f:
  183. metadata = json.load(f)
  184. return metadata
  185. except Exception as e:
  186. self.logger.error(f"Failed to extract metadata: {str(e)}")
  187. return {}
  188. def update_metadata(self, filepath: str, new_metadata: Dict[str, Any],
  189. password: Optional[str] = None) -> bool:
  190. """Update metadata in existing .pth file."""
  191. try:
  192. # Load existing data
  193. model_state_dict, existing_metadata = self.load_model(filepath, password)
  194. # Merge metadata
  195. existing_metadata.update(new_metadata)
  196. # Create temporary model for saving
  197. temp_model = nn.Module()
  198. temp_model.load_state_dict = lambda: None
  199. temp_model.state_dict = lambda: model_state_dict
  200. # Save with updated metadata
  201. return self.save_model(temp_model, filepath, existing_metadata, password)
  202. except Exception as e:
  203. self.logger.error(f"Failed to update metadata: {str(e)}")
  204. return False
  205. def _encrypt_file(self, filepath: str, password: str):
  206. """Encrypt file in place."""
  207. with open(filepath, 'rb') as f:
  208. data = f.read()
  209. encrypted_data = EncryptionManager.encrypt_data(data, password)
  210. with open(filepath, 'wb') as f:
  211. f.write(encrypted_data)
  212. def _decrypt_file(self, filepath: str, password: str) -> str:
  213. """Decrypt file to temporary location and return temp path."""
  214. with open(filepath, 'rb') as f:
  215. encrypted_data = f.read()
  216. try:
  217. decrypted_data = EncryptionManager.decrypt_data(encrypted_data, password)
  218. except Exception:
  219. raise ModelFormatError("Invalid password or corrupted file")
  220. # Save to temporary file
  221. temp_fd, temp_path = tempfile.mkstemp(suffix='.pth')
  222. with os.fdopen(temp_fd, 'wb') as f:
  223. f.write(decrypted_data)
  224. return temp_path
  225. def _update_file_hash(self, filepath: str, file_hash: str, password: Optional[str] = None):
  226. """Update file hash in metadata."""
  227. try:
  228. # Load and update metadata
  229. metadata = self.extract_metadata(filepath, password)
  230. metadata['file_hash'] = file_hash
  231. self.update_metadata(filepath, metadata, password)
  232. except Exception as e:
  233. self.logger.warning(f"Failed to update file hash: {str(e)}")
  234. class PTHandler(ModelFormatHandler):
  235. """Handler for .pt files using torch.package."""
  236. def save_model(self, model: nn.Module, filepath: str, metadata: Dict[str, Any],
  237. password: Optional[str] = None, **kwargs) -> bool:
  238. """
  239. Save model as .pt file using torch.package.
  240. Args:
  241. model: PyTorch model to save
  242. filepath: Path to save the model
  243. metadata: Metadata dictionary to include
  244. password: Optional password for encryption
  245. **kwargs: Additional arguments
  246. Returns:
  247. bool: True if successful
  248. """
  249. try:
  250. # Ensure directory exists
  251. os.makedirs(os.path.dirname(filepath), exist_ok=True)
  252. # Create package
  253. with torch.package.PackageExporter(filepath) as exp:
  254. # Add model
  255. exp.intern('**')
  256. exp.save_pickle('model', 'model.pkl', model.state_dict())
  257. # Add metadata
  258. metadata['file_format'] = 'pt'
  259. metadata['pytorch_version'] = torch.__version__
  260. metadata['package_version'] = '1.0'
  261. # Save metadata as JSON string in package
  262. exp.save_text('metadata', 'metafile.json',
  263. json.dumps(metadata, indent=2, ensure_ascii=False))
  264. # Add custom resources if specified
  265. for key, value in kwargs.items():
  266. if key.startswith('add_resource_'):
  267. resource_name = key.replace('add_resource_', '')
  268. if isinstance(value, str):
  269. exp.save_text('resources', f'{resource_name}.txt', value)
  270. elif isinstance(value, (dict, list)):
  271. exp.save_text('resources', f'{resource_name}.json',
  272. json.dumps(value, indent=2))
  273. # Apply password protection if requested
  274. if password:
  275. self._encrypt_file(filepath, password)
  276. # Update file hash
  277. file_hash = self._generate_file_hash(filepath)
  278. self._update_file_hash(filepath, file_hash, password)
  279. self.logger.info(f"Model saved successfully: {filepath}")
  280. return True
  281. except Exception as e:
  282. self.logger.error(f"Failed to save model: {str(e)}")
  283. return False
  284. def load_model(self, filepath: str, password: Optional[str] = None,
  285. map_location: Optional[str] = None) -> tuple:
  286. """
  287. Load model from .pt file.
  288. Returns:
  289. tuple: (model_state_dict, metadata)
  290. """
  291. try:
  292. # Handle password protection
  293. if password:
  294. filepath = self._decrypt_file(filepath, password)
  295. # Load package
  296. imp = torch.package.PackageImporter(filepath)
  297. # Load model state dict
  298. model_state_dict = imp.load_pickle('model', 'model.pkl',
  299. map_location=map_location)
  300. # Load metadata
  301. metadata_json = imp.load_text('metadata', 'metafile.json')
  302. metadata = json.loads(metadata_json)
  303. self.logger.info(f"Model loaded successfully: {filepath}")
  304. return model_state_dict, metadata
  305. except Exception as e:
  306. self.logger.error(f"Failed to load model: {str(e)}")
  307. raise ModelFormatError(f"Failed to load model: {str(e)}")
  308. def extract_metadata(self, filepath: str, password: Optional[str] = None) -> Dict[str, Any]:
  309. """Extract metadata from .pt file without loading the model."""
  310. try:
  311. # Handle password protection
  312. if password:
  313. filepath = self._decrypt_file(filepath, password)
  314. imp = torch.package.PackageImporter(filepath)
  315. metadata_json = imp.load_text('metadata', 'metafile.json')
  316. metadata = json.loads(metadata_json)
  317. return metadata
  318. except Exception as e:
  319. self.logger.error(f"Failed to extract metadata: {str(e)}")
  320. return {}
  321. def update_metadata(self, filepath: str, new_metadata: Dict[str, Any],
  322. password: Optional[str] = None) -> bool:
  323. """Update metadata in existing .pt file."""
  324. try:
  325. # Load existing data
  326. model_state_dict, existing_metadata = self.load_model(filepath, password)
  327. # Merge metadata
  328. existing_metadata.update(new_metadata)
  329. # Create temporary model for saving
  330. temp_model = nn.Module()
  331. temp_model.load_state_dict = lambda: None
  332. temp_model.state_dict = lambda: model_state_dict
  333. # Save with updated metadata
  334. return self.save_model(temp_model, filepath, existing_metadata, password)
  335. except Exception as e:
  336. self.logger.error(f"Failed to update metadata: {str(e)}")
  337. return False
  338. def _encrypt_file(self, filepath: str, password: str):
  339. """Encrypt file in place."""
  340. with open(filepath, 'rb') as f:
  341. data = f.read()
  342. encrypted_data = EncryptionManager.encrypt_data(data, password)
  343. with open(filepath, 'wb') as f:
  344. f.write(encrypted_data)
  345. def _decrypt_file(self, filepath: str, password: str) -> str:
  346. """Decrypt file to temporary location and return temp path."""
  347. with open(filepath, 'rb') as f:
  348. encrypted_data = f.read()
  349. try:
  350. decrypted_data = EncryptionManager.decrypt_data(encrypted_data, password)
  351. except Exception:
  352. raise ModelFormatError("Invalid password or corrupted file")
  353. # Save to temporary file
  354. temp_fd, temp_path = tempfile.mkstemp(suffix='.pt')
  355. with os.fdopen(temp_fd, 'wb') as f:
  356. f.write(decrypted_data)
  357. return temp_path
  358. def _update_file_hash(self, filepath: str, file_hash: str, password: Optional[str] = None):
  359. """Update file hash in metadata."""
  360. try:
  361. metadata = self.extract_metadata(filepath, password)
  362. metadata['file_hash'] = file_hash
  363. self.update_metadata(filepath, metadata, password)
  364. except Exception as e:
  365. self.logger.warning(f"Failed to update file hash: {str(e)}")
  366. class ONNXHandler(ModelFormatHandler):
  367. """Handler for .onnx files with metadata_props."""
  368. def __init__(self, logger: Optional[logging.Logger] = None):
  369. super().__init__(logger)
  370. if not ONNX_AVAILABLE:
  371. raise ImportError("ONNX not available. Install with: pip install onnx onnxruntime")
  372. def save_model(self, model: Union[nn.Module, str], filepath: str, metadata: Dict[str, Any],
  373. password: Optional[str] = None, **kwargs) -> bool:
  374. """
  375. Save model as .onnx file with metadata_props.
  376. Args:
  377. model: PyTorch model or path to existing ONNX model
  378. filepath: Path to save the model
  379. metadata: Metadata dictionary to include
  380. password: Optional password for encryption
  381. **kwargs: Additional arguments for ONNX export
  382. Returns:
  383. bool: True if successful
  384. """
  385. try:
  386. # Ensure directory exists
  387. os.makedirs(os.path.dirname(filepath), exist_ok=True)
  388. if isinstance(model, str):
  389. # Load existing ONNX model
  390. onnx_model = onnx.load(model)
  391. else:
  392. # Export PyTorch model to ONNX
  393. dummy_input = kwargs.get('dummy_input')
  394. if dummy_input is None:
  395. raise ValueError("dummy_input required for PyTorch to ONNX conversion")
  396. # Create temporary ONNX file
  397. temp_fd, temp_onnx_path = tempfile.mkstemp(suffix='.onnx')
  398. os.close(temp_fd)
  399. torch.onnx.export(
  400. model,
  401. dummy_input,
  402. temp_onnx_path,
  403. export_params=True,
  404. opset_version=kwargs.get('opset_version', 11),
  405. do_constant_folding=True,
  406. input_names=kwargs.get('input_names', ['input']),
  407. output_names=kwargs.get('output_names', ['output']),
  408. dynamic_axes=kwargs.get('dynamic_axes', {}),
  409. verbose=False
  410. )
  411. onnx_model = onnx.load(temp_onnx_path)
  412. os.unlink(temp_onnx_path)
  413. # Add metadata to model
  414. metadata['file_format'] = 'onnx'
  415. metadata['onnx_version'] = onnx.__version__
  416. # Clear existing metadata_props
  417. del onnx_model.metadata_props[:]
  418. # Add new metadata as key-value pairs
  419. for key, value in metadata.items():
  420. prop = onnx_model.metadata_props.add()
  421. prop.key = key
  422. if isinstance(value, (dict, list)):
  423. prop.value = json.dumps(value, ensure_ascii=False)
  424. else:
  425. prop.value = str(value)
  426. # Save model
  427. temp_filepath = filepath
  428. if password:
  429. temp_fd, temp_filepath = tempfile.mkstemp(suffix='.onnx')
  430. os.close(temp_fd)
  431. onnx.save(onnx_model, temp_filepath)
  432. # Apply password protection if requested
  433. if password:
  434. self._encrypt_file(temp_filepath, password)
  435. # Move encrypted file to final location
  436. with open(temp_filepath, 'rb') as src, open(filepath, 'wb') as dst:
  437. dst.write(src.read())
  438. os.unlink(temp_filepath)
  439. # Update file hash
  440. file_hash = self._generate_file_hash(filepath)
  441. self._update_file_hash(filepath, file_hash, password)
  442. self.logger.info(f"Model saved successfully: {filepath}")
  443. return True
  444. except Exception as e:
  445. self.logger.error(f"Failed to save model: {str(e)}")
  446. return False
  447. def load_model(self, filepath: str, password: Optional[str] = None,
  448. map_location: Optional[str] = None) -> tuple:
  449. """
  450. Load model from .onnx file.
  451. Returns:
  452. tuple: (onnx_model, metadata)
  453. """
  454. try:
  455. # Handle password protection
  456. if password:
  457. filepath = self._decrypt_file(filepath, password)
  458. # Load ONNX model
  459. onnx_model = onnx.load(filepath)
  460. # Extract metadata from metadata_props
  461. metadata = {}
  462. for prop in onnx_model.metadata_props:
  463. try:
  464. # Try to parse as JSON first
  465. metadata[prop.key] = json.loads(prop.value)
  466. except json.JSONDecodeError:
  467. # If not JSON, store as string
  468. metadata[prop.key] = prop.value
  469. self.logger.info(f"Model loaded successfully: {filepath}")
  470. return onnx_model, metadata
  471. except Exception as e:
  472. self.logger.error(f"Failed to load model: {str(e)}")
  473. raise ModelFormatError(f"Failed to load model: {str(e)}")
  474. def extract_metadata(self, filepath: str, password: Optional[str] = None) -> Dict[str, Any]:
  475. """Extract metadata from .onnx file without loading the full model."""
  476. try:
  477. # Handle password protection
  478. if password:
  479. filepath = self._decrypt_file(filepath, password)
  480. # Load model to extract metadata
  481. onnx_model = onnx.load(filepath)
  482. # Extract metadata
  483. metadata = {}
  484. for prop in onnx_model.metadata_props:
  485. try:
  486. metadata[prop.key] = json.loads(prop.value)
  487. except json.JSONDecodeError:
  488. metadata[prop.key] = prop.value
  489. return metadata
  490. except Exception as e:
  491. self.logger.error(f"Failed to extract metadata: {str(e)}")
  492. return {}
  493. def update_metadata(self, filepath: str, new_metadata: Dict[str, Any],
  494. password: Optional[str] = None) -> bool:
  495. """Update metadata in existing .onnx file."""
  496. try:
  497. # Load existing model and metadata
  498. onnx_model, existing_metadata = self.load_model(filepath, password)
  499. # Merge metadata
  500. existing_metadata.update(new_metadata)
  501. # Save with updated metadata
  502. return self.save_model(onnx_model, filepath, existing_metadata, password)
  503. except Exception as e:
  504. self.logger.error(f"Failed to update metadata: {str(e)}")
  505. return False
  506. def _encrypt_file(self, filepath: str, password: str):
  507. """Encrypt file in place."""
  508. with open(filepath, 'rb') as f:
  509. data = f.read()
  510. encrypted_data = EncryptionManager.encrypt_data(data, password)
  511. with open(filepath, 'wb') as f:
  512. f.write(encrypted_data)
  513. def _decrypt_file(self, filepath: str, password: str) -> str:
  514. """Decrypt file to temporary location and return temp path."""
  515. with open(filepath, 'rb') as f:
  516. encrypted_data = f.read()
  517. try:
  518. decrypted_data = EncryptionManager.decrypt_data(encrypted_data, password)
  519. except Exception:
  520. raise ModelFormatError("Invalid password or corrupted file")
  521. # Save to temporary file
  522. temp_fd, temp_path = tempfile.mkstemp(suffix='.onnx')
  523. with os.fdopen(temp_fd, 'wb') as f:
  524. f.write(decrypted_data)
  525. return temp_path
  526. def _update_file_hash(self, filepath: str, file_hash: str, password: Optional[str] = None):
  527. """Update file hash in metadata."""
  528. try:
  529. metadata = self.extract_metadata(filepath, password)
  530. metadata['file_hash'] = file_hash
  531. self.update_metadata(filepath, metadata, password)
  532. except Exception as e:
  533. self.logger.warning(f"Failed to update file hash: {str(e)}")
  534. class ModelFormatManager:
  535. """Manager class for handling different model formats."""
  536. def __init__(self, logger: Optional[logging.Logger] = None):
  537. self.logger = logger or logging.getLogger(__name__)
  538. self.handlers = {
  539. '.pth': PTHHandler(logger),
  540. '.pt': PTHandler(logger),
  541. }
  542. # Add ONNX handler if available
  543. if ONNX_AVAILABLE:
  544. self.handlers['.onnx'] = ONNXHandler(logger)
  545. else:
  546. self.logger.warning("ONNX support not available")
  547. def get_handler(self, filepath: str) -> ModelFormatHandler:
  548. """Get appropriate handler for file format."""
  549. ext = Path(filepath).suffix.lower()
  550. if ext not in self.handlers:
  551. raise ModelFormatError(f"Unsupported format: {ext}")
  552. return self.handlers[ext]
  553. def save_model(self, model: Union[nn.Module, str], filepath: str,
  554. metadata: Dict[str, Any], password: Optional[str] = None,
  555. **kwargs) -> bool:
  556. """Save model using appropriate handler."""
  557. handler = self.get_handler(filepath)
  558. return handler.save_model(model, filepath, metadata, password, **kwargs)
  559. def load_model(self, filepath: str, password: Optional[str] = None,
  560. map_location: Optional[str] = None) -> tuple:
  561. """Load model using appropriate handler."""
  562. handler = self.get_handler(filepath)
  563. return handler.load_model(filepath, password, map_location)
  564. def extract_metadata(self, filepath: str, password: Optional[str] = None) -> Dict[str, Any]:
  565. """Extract metadata using appropriate handler."""
  566. handler = self.get_handler(filepath)
  567. return handler.extract_metadata(filepath, password)
  568. def update_metadata(self, filepath: str, new_metadata: Dict[str, Any],
  569. password: Optional[str] = None) -> bool:
  570. """Update metadata using appropriate handler."""
  571. handler = self.get_handler(filepath)
  572. return handler.update_metadata(filepath, new_metadata, password)
  573. def get_supported_formats(self) -> List[str]:
  574. """Get list of supported file formats."""
  575. return list(self.handlers.keys())