jejunepixels commited on
Commit
69ff5a5
·
verified ·
1 Parent(s): 3dbcd77

Create hub_models.py

Browse files
Files changed (1) hide show
  1. hub_models.py +219 -0
hub_models.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face discovery and pinned downloads for the embedding viewer."""
2
+ from dataclasses import dataclass
3
+ from hashlib import sha256
4
+ from pathlib import Path, PurePosixPath
5
+ from urllib.parse import unquote, urlsplit
6
+ import io
7
+ import json
8
+ import os
9
+ import re
10
+ import time
11
+
12
+ CACHE_ROOT = Path(__file__).resolve().parent / '.model-cache'
13
+ os.environ.setdefault('HF_XET_CACHE', str(CACHE_ROOT / 'xet'))
14
+
15
+ from huggingface_hub import HfApi, hf_hub_download
16
+ from huggingface_hub.utils import validate_repo_id
17
+ from tqdm.auto import tqdm
18
+
19
+ from embeddings import embedding_name
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class HubLocation:
24
+ repo_id: str
25
+ revision: str = 'main'
26
+ filename: str | None = None
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class HubCatalog:
31
+ repo_id: str
32
+ commit: str
33
+ files: dict[str, int | None]
34
+ choices: tuple[str, ...]
35
+ preferred: str | None = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class DownloadPlan:
40
+ repo_id: str
41
+ commit: str
42
+ selection: str
43
+ files: tuple[str, ...]
44
+ weight_files: tuple[str, ...]
45
+ model_path: Path
46
+ tokenizer_path: Path | None
47
+ directory: Path
48
+ size: int | None
49
+ embeddings_only: bool = False
50
+
51
+
52
+ def safe_filename(name):
53
+ if not isinstance(name, str) or not name or '\\' in name or ':' in name or '\x00' in name:
54
+ raise ValueError('Invalid repository file path.')
55
+ path = PurePosixPath(name)
56
+ if path.is_absolute() or any(part in ('', '.', '..') for part in name.split('/')):
57
+ raise ValueError('Invalid repository file path.')
58
+ return name
59
+
60
+
61
+ def parse_location(value, revision=''):
62
+ value = value.strip()
63
+ filename, url_revision = None, None
64
+ if '://' in value:
65
+ url = urlsplit(value)
66
+ if url.scheme != 'https' or url.netloc.lower() != 'huggingface.co':
67
+ raise ValueError('Use a huggingface.co model URL or a repository ID such as owner/model.')
68
+ pieces = url.path.strip('/').split('/')
69
+ if len(pieces) >= 2 and pieces[0] in ('datasets', 'spaces'):
70
+ raise ValueError('Choose a model repository, rather than a dataset or Space.')
71
+ repo_parts = 1 if len(pieces) < 2 or pieces[1] in ('tree', 'blob', 'resolve') else 2
72
+ repo_id = '/'.join(pieces[:repo_parts])
73
+ if len(pieces) > repo_parts:
74
+ tail = pieces[repo_parts:]
75
+ if tail[0] not in ('tree', 'blob', 'resolve') or len(tail) < 2:
76
+ raise ValueError('Use the model repository URL, or a tree/blob/resolve file URL.')
77
+ url_revision = unquote(tail[1])
78
+ if tail[0] in ('blob', 'resolve') and len(tail) > 2:
79
+ filename = safe_filename(unquote('/'.join(tail[2:])))
80
+ else:
81
+ repo_id = value
82
+ validate_repo_id(repo_id)
83
+ return HubLocation(repo_id, revision.strip() or url_revision or 'main', filename)
84
+
85
+
86
+ def discover_models(value, revision='', token=None):
87
+ location = parse_location(value, revision)
88
+ info = HfApi().model_info(location.repo_id, revision=location.revision,
89
+ files_metadata=True, token=token or False, timeout=20)
90
+ if not info.sha:
91
+ raise ValueError('The repository did not return a commit revision.')
92
+ files = {s.rfilename: s.size for s in info.siblings or []}
93
+ choices = []
94
+ for name in files:
95
+ if name.endswith('.safetensors.index.json'):
96
+ choices.append(name)
97
+ elif name.endswith('.safetensors') and not re.search(r'-\d{5}-of-\d{5}\.safetensors$', name):
98
+ choices.append(name)
99
+ elif name.lower().endswith('.gguf') and not re.search(r'-\d{5}-of-\d{5}\.gguf$', name, re.I):
100
+ choices.append(name)
101
+ choices.sort(key=lambda n: (not n.endswith('.index.json'), n.lower()))
102
+ if location.filename is not None and location.filename not in choices:
103
+ raise ValueError('The linked file is not supported here. Use the repository URL to choose GGUF or safetensors weights.')
104
+ return HubCatalog(location.repo_id, info.sha, files, tuple(choices),
105
+ location.filename if location.filename in choices else None)
106
+
107
+
108
+ def selection_directory(catalog, selection, cache_root=CACHE_ROOT):
109
+ identity = f'{catalog.repo_id}@{catalog.commit}/{selection}'
110
+ directory = Path(cache_root).resolve() / sha256(identity.encode()).hexdigest()[:24]
111
+ return directory
112
+
113
+
114
+ def build_plan(catalog, selection, token=None, cache_root=CACHE_ROOT):
115
+ if selection not in catalog.choices:
116
+ raise ValueError('Choose one of the compatible files from this repository.')
117
+ safe_filename(selection)
118
+ directory = selection_directory(catalog, selection, cache_root)
119
+ files = [selection]
120
+ weights = [selection]
121
+ embeddings_only = False
122
+ if selection.endswith('.index.json'):
123
+ # The small index is fetched before large files so the plan can show its size.
124
+ index_path = hf_hub_download(catalog.repo_id, selection, revision=catalog.commit,
125
+ local_dir=directory, token=token or False)
126
+ with Path(index_path).open('r', encoding='utf-8') as source:
127
+ index = json.load(source)
128
+ mapping = index.get('weight_map')
129
+ if not isinstance(mapping, dict) or not mapping:
130
+ raise ValueError('The safetensors index has no weight map.')
131
+ relevant = [value for key, value in mapping.items() if embedding_name(key)]
132
+ selected_shards = relevant or list(mapping.values())
133
+ embeddings_only = bool(relevant)
134
+ parent = PurePosixPath(selection).parent
135
+ weights = []
136
+ for value in selected_shards:
137
+ safe_filename(value)
138
+ name = str(parent / value)
139
+ safe_filename(name)
140
+ if not name.endswith('.safetensors') or name not in catalog.files:
141
+ raise ValueError('The model index refers to a missing or unsupported weights file.')
142
+ if name not in weights:
143
+ weights.append(name)
144
+ files.extend(weights)
145
+ parent = PurePosixPath(selection).parent
146
+ tokenizer = next((name for name in (str(parent / 'tokenizer.json'), 'tokenizer.json') if name in catalog.files), None)
147
+ if tokenizer:
148
+ files.append(tokenizer)
149
+ files = tuple(dict.fromkeys(files))
150
+ sizes = [catalog.files.get(name) for name in files]
151
+ size = sum(sizes) if all(value is not None for value in sizes) else None
152
+ model_path = directory if selection.endswith('.index.json') else directory / selection
153
+ return DownloadPlan(catalog.repo_id, catalog.commit, selection, files, tuple(weights), model_path,
154
+ directory / tokenizer if tokenizer else None, directory, size, embeddings_only)
155
+
156
+
157
+ def download_model(plan, token=None, progress=None):
158
+ """Progress callbacks may run on SDK threads; callers should use a queue."""
159
+ total_files = len(plan.files)
160
+ for index, filename in enumerate(plan.files):
161
+ safe_filename(filename)
162
+
163
+ def report(fraction, detail='', file_index=index, name=filename):
164
+ if progress:
165
+ progress((file_index + max(0., min(1., fraction))) / total_files,
166
+ f'{file_index + 1}/{total_files} · {name}' + (f' · {detail}' if detail else ''))
167
+
168
+ class DownloadProgress(tqdm):
169
+ def __init__(self, *args, **kwargs):
170
+ kwargs.pop('name', None)
171
+ kwargs['file'] = io.StringIO()
172
+ kwargs['disable'] = False
173
+ self._last_report = 0.
174
+ self._progress_callback = report
175
+ super().__init__(*args, **kwargs)
176
+
177
+ def update(self, n=1):
178
+ changed = super().update(n)
179
+ now = time.monotonic()
180
+ if now - self._last_report > .15 and self.total:
181
+ self._last_report = now
182
+ self._progress_callback(self.n / self.total, f'{format_size(self.n)} / {format_size(self.total)}')
183
+ return changed
184
+
185
+ def close(self):
186
+ if getattr(self, 'total', None):
187
+ self._progress_callback(self.n / self.total, f'{format_size(self.n)} / {format_size(self.total)}')
188
+ super().close()
189
+
190
+ report(0.)
191
+ hf_hub_download(plan.repo_id, filename, revision=plan.commit, token=token or False,
192
+ local_dir=plan.directory, tqdm_class=DownloadProgress)
193
+ report(1.)
194
+ if not plan.model_path.exists() or not all((plan.directory / name).is_file() for name in plan.weight_files):
195
+ raise ValueError('The download finished without a usable model file.')
196
+ return plan.model_path, plan.tokenizer_path
197
+
198
+
199
+ def format_size(value):
200
+ if value is None:
201
+ return 'size unavailable'
202
+ for unit in ('B', 'KB', 'MB', 'GB', 'TB'):
203
+ if value < 1000 or unit == 'TB':
204
+ return f'{value:.1f} {unit}'
205
+ value /= 1000
206
+
207
+
208
+ def friendly_hub_error(error, token=None):
209
+ from huggingface_hub.errors import GatedRepoError, RepositoryNotFoundError, RevisionNotFoundError
210
+ if isinstance(error, GatedRepoError):
211
+ return 'This model is gated. Request or accept access on its Hugging Face page, then use a read token from the approved account.'
212
+ if isinstance(error, RevisionNotFoundError):
213
+ return 'That revision was not found. Check the branch, tag or commit.'
214
+ if isinstance(error, RepositoryNotFoundError):
215
+ return 'The repository was not found or is private. Check its ID and, if needed, provide a read token.'
216
+ message = str(error)
217
+ if token:
218
+ message = message.replace(token, '[redacted]')
219
+ return re.sub(r'hf_[A-Za-z0-9]+', '[redacted]', message)