Laura Wagner commited on
Commit
b87a88b
·
1 Parent(s): f7f764e

add LoRA metadata v2

Browse files
jupyter_notebooks/Section_2-4_Figure_9_ectract_LoRA_metadata_v2.ipynb ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "f36422c8",
6
+ "metadata": {},
7
+ "source": [
8
+ "# LoRA metadata"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "raw",
13
+ "id": "8a2feb6e",
14
+ "metadata": {},
15
+ "source": [
16
+ "LoRA Metadata Processing Workflow\n",
17
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
18
+ "│ Load CSV File │ --> │ Read adapter metadata CSV file. │\n",
19
+ "│ Read Model Versions │ │ Extract model version IDs and relevant data. │\n",
20
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
21
+ " ↓\n",
22
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
23
+ "│ Download Adapter │ --> │ Use stored download URLs to fetch adapter files │\n",
24
+ "│ Files Using API │ │ using rotating API keys. │\n",
25
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
26
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
27
+ "│ Parse Metadata │ --> │ Extract safetensors metadata, such as training │\n",
28
+ "│ from SafeTensor │ │ images, model type, and architecture. │\n",
29
+ "│ Files │ │ │\n",
30
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
31
+ " ↓\n",
32
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
33
+ "│ Store Parsed │ --> │ Save extracted metadata into structured JSON │\n",
34
+ "│ Metadata as JSON │ │ files for later analysis. │\n",
35
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
36
+ "\n",
37
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
38
+ "│ Process JSON Files │ --> │ Read saved JSON metadata, extract relevant │\n",
39
+ "│ for Consolidation │ │ details, and filter necessary attributes. │\n",
40
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
41
+ " ↓\n",
42
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
43
+ "│ Extract Training │ --> │ Identify most frequent training tags, architectures│\n",
44
+ "│ Tags & Model Info │ │ and systems used for model creation. │\n",
45
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
46
+ " ↓\n",
47
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
48
+ "│ Save Consolidated │ --> │ Store all processed metadata in a structured CSV │\n",
49
+ "│ Metadata to CSV │ │ format for final analysis. ���\n",
50
+ "└──────────────────────┘ └───────────────────────────────────────────────────┘\n"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "efc9939d",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "import os\n",
61
+ "import re\n",
62
+ "import json\n",
63
+ "import csv\n",
64
+ "import struct\n",
65
+ "import requests\n",
66
+ "from pathlib import Path\n",
67
+ "import pandas as pd\n",
68
+ "from collections import Counter\n",
69
+ "from concurrent.futures import ProcessPoolExecutor\n",
70
+ "from pathlib import Path\n",
71
+ "import matplotlib.pyplot as plt\n",
72
+ "from matplotlib.font_manager import FontProperties\n",
73
+ "from matplotlib import font_manager\n",
74
+ "import pandas as pd\n",
75
+ "from collections import Counter\n",
76
+ "from concurrent.futures import ProcessPoolExecutor\n",
77
+ "\n",
78
+ "# Define the current directory and important file paths\n",
79
+ "current_dir = Path.cwd()\n",
80
+ "\n",
81
+ "# Define frequently used directories\n",
82
+ "\n",
83
+ "data_dir = current_dir.parent / 'data/csv/adapters.csv'\n",
84
+ "fonts_dir = current_dir.parent / 'misc/assets/fonts'\n",
85
+ "plots_dir = current_dir.parent / 'results/plots'\n",
86
+ "raw_data_dir = current_dir.parent / 'data/adapter_metadata/lora' ### location of the LoRA metadata (JSON)\n",
87
+ "temp_dir = current_dir.parent / 'data/raw/adapters_safetensors'\n",
88
+ "misc_dir = current_dir.parent / 'misc'\n",
89
+ "\n",
90
+ "# File paths\n",
91
+ "adapters_csv = current_dir.parent / 'data/csv/adapters.csv'\n",
92
+ "output_json_dir = raw_data_dir\n",
93
+ "api_keys_file = misc_dir / 'credentials/civit.txt'\n",
94
+ "\n",
95
+ "# Ensure directories exist\n",
96
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
97
+ "os.makedirs(temp_dir, exist_ok=True)\n",
98
+ "\n",
99
+ "\n",
100
+ "# Load fonts into Matplotlib\n",
101
+ "for font_path in font_paths:\n",
102
+ " font_manager.fontManager.addfont(font_path)\n",
103
+ "\n",
104
+ "# Set default font family for plots\n",
105
+ "plt.rcParams['font.family'] = ['Noto Sans JP', 'Noto Sans SC', 'sans-serif']\n",
106
+ "\n",
107
+ "print('Paths and fonts initialized successfully.')\n",
108
+ "\n",
109
+ "print('Paths initialized successfully.')"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "markdown",
114
+ "id": "87a58593",
115
+ "metadata": {},
116
+ "source": [
117
+ "## Step 2: Download LoRA and extract *.safetensors metadata\n",
118
+ "This script downloads LoRA adapters from the filtered Civiverse-Models dataset and extracts the metadata found within the *.safetensors' data structure"
119
+ ]
120
+ },
121
+ {
122
+ "cell_type": "code",
123
+ "execution_count": null,
124
+ "id": "abd3a0bc",
125
+ "metadata": {},
126
+ "outputs": [],
127
+ "source": [
128
+ "import os\n",
129
+ "import sys\n",
130
+ "import csv\n",
131
+ "import json\n",
132
+ "import struct\n",
133
+ "import time\n",
134
+ "import requests\n",
135
+ "import signal\n",
136
+ "import contextlib\n",
137
+ "from pathlib import Path\n",
138
+ "import re\n",
139
+ "\n",
140
+ "# === Paste your API keys here ===\n",
141
+ "API_KEYS = [\n",
142
+ " \"REDACTED-CIVITAI-API-KEY-1\", #DISCORD\n",
143
+ " \"REDACTED-CIVITAI-API-KEY-2\", #ASDD 1\n",
144
+ " \"REDACTED-CIVITAI-API-KEY-3\", #ASDD 2\n",
145
+ " \"REDACTED-CIVITAI-API-KEY-4\", #BSDD \n",
146
+ " \"REDACTED-CIVITAI-API-KEY-5\"\n",
147
+ "]\n",
148
+ "if not API_KEYS or any(not isinstance(k, str) or not k.strip() for k in API_KEYS):\n",
149
+ " raise ValueError(\"Please paste at least one valid API key into API_KEYS.\")\n",
150
+ "\n",
151
+ "# === Config (adjust paths as needed) ===\n",
152
+ "current_dir = Path.cwd()\n",
153
+ "output_json_dir = current_dir.parent / \"data/adapter_metadata/lora\" # where JSON outputs go\n",
154
+ "temp_dir = current_dir.parent / \"data/raw/adapters_safetensors\" # where downloads go\n",
155
+ "csv_path = current_dir.parent / \"data/csv/adapters_poi_false_sfw.csv\"\n",
156
+ "\n",
157
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
158
+ "os.makedirs(temp_dir, exist_ok=True)\n",
159
+ "\n",
160
+ "# === API key state ===\n",
161
+ "current_key_index = 0\n",
162
+ "\n",
163
+ "\n",
164
+ "\n",
165
+ "def safe_filename(name: str, max_length: int = 100) -> str:\n",
166
+ " # Replace unsafe chars\n",
167
+ " sanitized = re.sub(r'[^a-zA-Z0-9_\\-]', '_', name)\n",
168
+ " # Truncate if too long\n",
169
+ " if len(sanitized) > max_length:\n",
170
+ " sanitized = sanitized[:max_length]\n",
171
+ " return sanitized\n",
172
+ "\n",
173
+ "\n",
174
+ "def get_headers():\n",
175
+ " global current_key_index\n",
176
+ " return {\n",
177
+ " \"Accept\": \"application/json\",\n",
178
+ " \"Authorization\": f\"Bearer {API_KEYS[current_key_index].strip()}\"\n",
179
+ " }\n",
180
+ "\n",
181
+ "def rotate_api_key():\n",
182
+ " global current_key_index\n",
183
+ " if current_key_index < len(API_KEYS) - 1:\n",
184
+ " current_key_index += 1\n",
185
+ " print(f\"🔁 Rotated to API key #{current_key_index + 1}\")\n",
186
+ " else:\n",
187
+ " raise Exception(\"All API keys have been exhausted.\")\n",
188
+ "\n",
189
+ "# === Utilities ===\n",
190
+ "def save_json(data, filename):\n",
191
+ " with open(filename, 'w', encoding=\"utf-8\") as f:\n",
192
+ " json.dump(data, f, indent=4, ensure_ascii=False)\n",
193
+ "\n",
194
+ "def parse_safetensors(file_path):\n",
195
+ " # Minimal, tolerant metadata reader; returns {} on failure.\n",
196
+ " try:\n",
197
+ " with open(file_path, 'rb') as f:\n",
198
+ " file_data = f.read()\n",
199
+ " # Many safetensors use 8-byte header length; this code follows your original logic\n",
200
+ " # (4-byte) but keeps the 8-byte skip. Keep if it's working in your dataset.\n",
201
+ " metadata_size = struct.unpack('<I', file_data[:4])[0]\n",
202
+ " metadata_bytes = file_data[8:8 + metadata_size]\n",
203
+ " metadata_str = metadata_bytes.decode('utf-8', errors='replace')\n",
204
+ " metadata = json.loads(metadata_str)\n",
205
+ " return metadata.get('__metadata__', {})\n",
206
+ " except Exception as e:\n",
207
+ " print(f\"Error parsing safetensors file: {e}\")\n",
208
+ " return {}\n",
209
+ "\n",
210
+ "# === Timeout context ===\n",
211
+ "class TimeoutException(Exception):\n",
212
+ " pass\n",
213
+ "\n",
214
+ "@contextlib.contextmanager\n",
215
+ "def time_limit(seconds):\n",
216
+ " def signal_handler(signum, frame):\n",
217
+ " raise TimeoutException(f\"Timed out after {seconds} seconds\")\n",
218
+ " # Note: SIGALRM works on Unix-like OS; on Windows this will be a no-op.\n",
219
+ " try:\n",
220
+ " signal.signal(signal.SIGALRM, signal_handler)\n",
221
+ " signal.alarm(seconds)\n",
222
+ " except Exception:\n",
223
+ " # Fallback: no hard alarm on non-Unix systems\n",
224
+ " pass\n",
225
+ " try:\n",
226
+ " yield\n",
227
+ " finally:\n",
228
+ " try:\n",
229
+ " signal.alarm(0)\n",
230
+ " except Exception:\n",
231
+ " pass\n",
232
+ "\n",
233
+ "# === Download with timeout, retries, backoff, and key rotation ===\n",
234
+ "def download_file(url, output_folder, timeout=30, overall_timeout=120, max_retries=3):\n",
235
+ " filename = url.split(\"/\")[-1]\n",
236
+ " output_path = os.path.join(output_folder, filename)\n",
237
+ "\n",
238
+ " global current_key_index\n",
239
+ " retries = 0\n",
240
+ " backoff = 2\n",
241
+ "\n",
242
+ " while current_key_index < len(API_KEYS):\n",
243
+ " try:\n",
244
+ " with time_limit(overall_timeout): # global cap per download\n",
245
+ " #print(f\"➡️ GET {url} using key #{current_key_index + 1}\")\n",
246
+ " resp = requests.get(\n",
247
+ " url,\n",
248
+ " headers=get_headers(),\n",
249
+ " stream=True,\n",
250
+ " timeout=(10, timeout), # (connect timeout, per-chunk read timeout)\n",
251
+ " )\n",
252
+ "\n",
253
+ " # Auth errors → rotate key\n",
254
+ " if resp.status_code in (401, 403):\n",
255
+ " print(f\"❌ Auth {resp.status_code} with key #{current_key_index + 1}. Rotating.\")\n",
256
+ " rotate_api_key()\n",
257
+ " retries = 0\n",
258
+ " backoff = 2\n",
259
+ " continue\n",
260
+ "\n",
261
+ " # Not found → bubble up as FileNotFoundError (do not rotate)\n",
262
+ " if resp.status_code == 404:\n",
263
+ " raise FileNotFoundError(f\"Model not found at {url}\")\n",
264
+ "\n",
265
+ " # Rate limit → either rotate or wait/backoff\n",
266
+ " if resp.status_code == 429:\n",
267
+ " print(\"⏳ Rate limited (429).\", end=\" \")\n",
268
+ " if current_key_index < len(API_KEYS) - 1:\n",
269
+ " print(\"Rotating key.\")\n",
270
+ " rotate_api_key()\n",
271
+ " retries = 0\n",
272
+ " backoff = 2\n",
273
+ " continue\n",
274
+ " else:\n",
275
+ " print(f\"Waiting {backoff}s (no other keys).\")\n",
276
+ " time.sleep(backoff)\n",
277
+ " backoff = min(backoff * 2, 60)\n",
278
+ " continue\n",
279
+ "\n",
280
+ " # Other HTTP errors → raise to RequestException path\n",
281
+ " resp.raise_for_status()\n",
282
+ "\n",
283
+ " # Save file\n",
284
+ " with open(output_path, 'wb') as fh:\n",
285
+ " for chunk in resp.iter_content(chunk_size=8192):\n",
286
+ " if chunk:\n",
287
+ " fh.write(chunk)\n",
288
+ "\n",
289
+ " return output_path, filename\n",
290
+ "\n",
291
+ " except TimeoutException as e:\n",
292
+ " # Hard overall timeout → propagate\n",
293
+ " raise e\n",
294
+ " except requests.exceptions.RequestException as e:\n",
295
+ " # Network-ish errors: retry same key with backoff up to max_retries\n",
296
+ " retries += 1\n",
297
+ " if retries <= max_retries:\n",
298
+ " print(f\"🌐 Network error (try {retries}/{max_retries}) with key #{current_key_index + 1}: {e}\")\n",
299
+ " time.sleep(backoff)\n",
300
+ " backoff = min(backoff * 2, 60)\n",
301
+ " continue\n",
302
+ " else:\n",
303
+ " raise Exception(f\"Failed to download {url} after {max_retries} retries: {e}\")\n",
304
+ "\n",
305
+ " # If we exit the loop, we truly ran out\n",
306
+ " raise Exception(\"All API keys have been exhausted or failed.\")\n",
307
+ "\n",
308
+ "# === Main processing ===\n",
309
+ "def process_csv(csv_path):\n",
310
+ " with open(csv_path, newline='', encoding='utf-8') as csvfile:\n",
311
+ " reader = csv.DictReader(csvfile)\n",
312
+ " for index, row in enumerate(reader):\n",
313
+ " # Collect up to 20 version IDs; use the most recent\n",
314
+ " version_ids = []\n",
315
+ " for i in range(1, 21):\n",
316
+ " k = f'version_id_{i}'\n",
317
+ " if k in row and row[k]:\n",
318
+ " try:\n",
319
+ " version_ids.append(int(float(row[k])))\n",
320
+ " except ValueError:\n",
321
+ " print(f\"Invalid version_id value '{row[k]}' in row: {row}\")\n",
322
+ "\n",
323
+ " if not version_ids:\n",
324
+ " print(f\"No valid version IDs found in row: {row}\")\n",
325
+ " continue\n",
326
+ "\n",
327
+ " most_recent_version_id = str(max(version_ids))\n",
328
+ " name = row.get('name', 'unknown')\n",
329
+ " sanitized_name = safe_filename(name, max_length=100)\n",
330
+ " new_json_file = os.path.join(\n",
331
+ " output_json_dir,\n",
332
+ " f\"{index:08d}_{most_recent_version_id}_{sanitized_name}.json\"\n",
333
+ " )\n",
334
+ "\n",
335
+ " # Skip if JSON already exists\n",
336
+ " if os.path.exists(new_json_file):\n",
337
+ " #print(f\"↩️ Skipping versionID {most_recent_version_id} (JSON already exists)\")\n",
338
+ " continue\n",
339
+ "\n",
340
+ " try:\n",
341
+ " adapter_file, fname = download_file(\n",
342
+ " row['downloadUrl'], str(temp_dir),\n",
343
+ " timeout=30, overall_timeout=180\n",
344
+ " )\n",
345
+ " metadata = parse_safetensors(adapter_file)\n",
346
+ "\n",
347
+ " civitaidata = {\n",
348
+ " k: (int(v) if str(v).isdigit() else v)\n",
349
+ " for k, v in row.items()\n",
350
+ " }\n",
351
+ " new_json_data = {\n",
352
+ " \"civitaidata\": civitaidata,\n",
353
+ " \"metadata\": metadata,\n",
354
+ " \"versionID\": most_recent_version_id\n",
355
+ " }\n",
356
+ " save_json(new_json_data, new_json_file)\n",
357
+ " #print(f\"✅ Created JSON for versionID {most_recent_version_id} with file {fname}\")\n",
358
+ "\n",
359
+ " except FileNotFoundError as e:\n",
360
+ " print(f\"⚠️ {e} — saving empty metadata.\")\n",
361
+ " civitaidata = {\n",
362
+ " k: (int(v) if str(v).isdigit() else v)\n",
363
+ " for k, v in row.items()\n",
364
+ " }\n",
365
+ " empty_json = {\n",
366
+ " \"civitaidata\": civitaidata,\n",
367
+ " \"metadata\": {},\n",
368
+ " \"versionID\": most_recent_version_id,\n",
369
+ " \"error\": \"Model not found (404)\"\n",
370
+ " }\n",
371
+ " save_json(empty_json, new_json_file)\n",
372
+ " except Exception as e:\n",
373
+ " print(f\"⚠️ Error processing versionID {most_recent_version_id}: {e}\")\n",
374
+ " civitaidata = {\n",
375
+ " k: (int(v) if str(v).isdigit() else v)\n",
376
+ " for k, v in row.items()\n",
377
+ " }\n",
378
+ " empty_json = {\n",
379
+ " \"civitaidata\": civitaidata,\n",
380
+ " \"metadata\": {},\n",
381
+ " \"versionID\": most_recent_version_id,\n",
382
+ " \"error\": str(e)\n",
383
+ " }\n",
384
+ " save_json(empty_json, new_json_file)\n",
385
+ " print(f\"💾 Saved empty JSON for versionID {most_recent_version_id} due to failure.\")\n",
386
+ "\n",
387
+ "# === Run ===\n",
388
+ "if __name__ == \"__main__\":\n",
389
+ " process_csv(csv_path)\n"
390
+ ]
391
+ }
392
+ ],
393
+ "metadata": {
394
+ "language_info": {
395
+ "name": "python"
396
+ }
397
+ },
398
+ "nbformat": 4,
399
+ "nbformat_minor": 5
400
+ }