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 |
+
}
|