|
9 | 9 |
|
10 | 10 | import os |
11 | 11 | import logging |
| 12 | +import json |
12 | 13 | from pathlib import Path |
13 | 14 | from typing import Optional, Dict, Any |
14 | 15 | from enum import Enum |
@@ -44,7 +45,51 @@ def __init__(self, cache_dir: Optional[str] = None): |
44 | 45 | """ |
45 | 46 | self.cache_dir = Path(cache_dir) if cache_dir else Path.home() / ".mohawk" / "models" |
46 | 47 | self.cache_dir.mkdir(parents=True, exist_ok=True) |
| 48 | + self.library_file = self.cache_dir / "library.json" |
| 49 | + self._library = self._load_library_index() |
47 | 50 | logger.info(f"ModelLoader initialized with cache_dir={self.cache_dir}") |
| 51 | + |
| 52 | + def _load_library_index(self) -> Dict[str, Dict[str, Any]]: |
| 53 | + """Load persisted model library metadata.""" |
| 54 | + if not self.library_file.exists(): |
| 55 | + return {} |
| 56 | + |
| 57 | + try: |
| 58 | + with self.library_file.open("r", encoding="utf-8") as f: |
| 59 | + data = json.load(f) |
| 60 | + return data if isinstance(data, dict) else {} |
| 61 | + except Exception: |
| 62 | + logger.warning("Failed to read model library index; starting fresh") |
| 63 | + return {} |
| 64 | + |
| 65 | + def _save_library_index(self) -> None: |
| 66 | + """Persist model library metadata to disk.""" |
| 67 | + with self.library_file.open("w", encoding="utf-8") as f: |
| 68 | + json.dump(self._library, f, indent=2, sort_keys=True) |
| 69 | + |
| 70 | + def add_to_library(self, model_id: str, local_path: str, source: str) -> Dict[str, Any]: |
| 71 | + """Register a model in the local model library index.""" |
| 72 | + entry = { |
| 73 | + "model_id": model_id, |
| 74 | + "local_path": str(local_path), |
| 75 | + "source": source, |
| 76 | + } |
| 77 | + self._library[model_id] = entry |
| 78 | + self._save_library_index() |
| 79 | + return entry |
| 80 | + |
| 81 | + def add_local_model(self, model_path: str, alias: Optional[str] = None) -> Dict[str, Any]: |
| 82 | + """Add an existing local model directory or file to the model library.""" |
| 83 | + path = Path(model_path) |
| 84 | + if not path.exists(): |
| 85 | + raise FileNotFoundError(f"Local model path not found: {model_path}") |
| 86 | + |
| 87 | + model_id = alias or path.name |
| 88 | + return self.add_to_library(model_id=model_id, local_path=str(path), source="local") |
| 89 | + |
| 90 | + def list_library(self) -> list: |
| 91 | + """List registered models in the local model library.""" |
| 92 | + return list(self._library.values()) |
48 | 93 |
|
49 | 94 | def detect_format(self, model_path: str) -> ModelFormat: |
50 | 95 | """ |
@@ -134,13 +179,19 @@ def _load_huggingface(self, model_path: str, **kwargs) -> Dict[str, Any]: |
134 | 179 | from transformers import AutoModelForCausalLM, AutoTokenizer |
135 | 180 | except ImportError: |
136 | 181 | raise ImportError("transformers required for HuggingFace models. Install with: pip install transformers") |
137 | | - |
138 | | - tokenizer = AutoTokenizer.from_pretrained(model_path, **kwargs) |
| 182 | + |
| 183 | + tokenizer_kwargs = dict(kwargs.pop("tokenizer_kwargs", {})) |
| 184 | + tokenizer_kwargs.setdefault("local_files_only", kwargs.get("local_files_only", False)) |
| 185 | + |
| 186 | + model_kwargs = dict(kwargs.pop("model_kwargs", {})) |
| 187 | + model_kwargs.setdefault("torch_dtype", kwargs.pop("torch_dtype", "auto")) |
| 188 | + model_kwargs.setdefault("device_map", kwargs.pop("device_map", "auto")) |
| 189 | + model_kwargs.update(kwargs) |
| 190 | + |
| 191 | + tokenizer = AutoTokenizer.from_pretrained(model_path, **tokenizer_kwargs) |
139 | 192 | model = AutoModelForCausalLM.from_pretrained( |
140 | 193 | model_path, |
141 | | - torch_dtype=kwargs.get("torch_dtype", "auto"), |
142 | | - device_map=kwargs.get("device_map", "auto"), |
143 | | - **kwargs, |
| 194 | + **model_kwargs, |
144 | 195 | ) |
145 | 196 |
|
146 | 197 | return {"model": model, "tokenizer": tokenizer, "format": "huggingface"} |
@@ -172,17 +223,26 @@ def download(self, model_id: str, **kwargs) -> str: |
172 | 223 | Returns: |
173 | 224 | Local path to downloaded model |
174 | 225 | """ |
| 226 | + if not model_id or not model_id.strip(): |
| 227 | + raise ValueError("model_id must be a non-empty HuggingFace repository ID") |
| 228 | + |
175 | 229 | from huggingface_hub import snapshot_download |
176 | 230 |
|
177 | | - cache_path = self.cache_dir / model_id.replace("/", "_") |
| 231 | + model_id = model_id.strip() |
| 232 | + cache_path = self.cache_dir / model_id.replace("/", "--") |
178 | 233 |
|
179 | 234 | logger.info(f"Downloading {model_id} to {cache_path}") |
| 235 | + |
| 236 | + download_kwargs = dict(kwargs) |
| 237 | + download_kwargs.setdefault("local_dir", str(cache_path)) |
| 238 | + download_kwargs.setdefault("local_dir_use_symlinks", False) |
180 | 239 |
|
181 | 240 | local_path = snapshot_download( |
182 | 241 | repo_id=model_id, |
183 | | - local_dir=str(cache_path), |
184 | | - **kwargs, |
| 242 | + **download_kwargs, |
185 | 243 | ) |
| 244 | + |
| 245 | + self.add_to_library(model_id=model_id, local_path=local_path, source="huggingface") |
186 | 246 |
|
187 | 247 | return local_path |
188 | 248 |
|
|
0 commit comments