John6666 commited on
Commit
eadc96f
·
verified ·
1 Parent(s): c57c836

Upload modutils.py

Browse files
Files changed (1) hide show
  1. modutils.py +751 -170
modutils.py CHANGED
@@ -22,6 +22,8 @@ FILENAME_TIMEZONE = timezone(timedelta(hours=9)) # JST
22
  import torch
23
  from safetensors.torch import load_file
24
  import gc
 
 
25
 
26
 
27
  from env import (HF_LORA_PRIVATE_REPOS1, HF_LORA_PRIVATE_REPOS2,
@@ -139,44 +141,431 @@ def download_hf_file(directory, url, force_filename="", hf_token="", progress=gr
139
 
140
 
141
  USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
 
144
- def request_json_data(url):
145
- model_version_id = url.split('/')[-1]
146
- if "?modelVersionId=" in model_version_id:
147
- match = re.search(r'modelVersionId=(\d+)', url)
148
- model_version_id = match.group(1)
 
 
 
 
 
 
 
149
 
150
- endpoint_url = f"https://civitai.com/api/v1/model-versions/{model_version_id}"
151
 
152
- params = {}
153
  headers = {'User-Agent': USER_AGENT, 'content-type': 'application/json'}
154
- session = requests.Session()
155
- retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
156
- session.mount("https://", HTTPAdapter(max_retries=retries))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  try:
159
- result = session.get(endpoint_url, params=params, headers=headers, stream=True, timeout=(3.0, 15))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  result.raise_for_status()
161
  json_data = result.json()
162
- return json_data if json_data else None
 
 
 
 
 
 
 
163
  except Exception as e:
164
- print(f"Error: {e}")
165
  return None
166
 
167
 
168
  class ModelInformation:
169
- def __init__(self, json_data):
 
170
  self.model_version_id = json_data.get("id", "")
171
  self.model_id = json_data.get("modelId", "")
172
- self.download_url = json_data.get("downloadUrl", "")
173
  self.model_url = f"https://civitai.com/models/{self.model_id}?modelVersionId={self.model_version_id}"
174
- self.filename_url = next(
175
- (v.get("name", "") for v in reversed(json_data.get("files", [])) if str(self.model_version_id) in v.get("downloadUrl", "")), ""
176
- )
177
- self.filename_url = self.filename_url if self.filename_url else ""
178
  self.description = json_data.get("description", "")
179
- if self.description is None: self.description = ""
 
180
  self.model_name = json_data.get("model", {}).get("name", "")
181
  self.model_type = json_data.get("model", {}).get("type", "")
182
  self.nsfw = json_data.get("model", {}).get("nsfw", False)
@@ -184,71 +573,235 @@ class ModelInformation:
184
  self.images = [img.get("url", "") for img in json_data.get("images", [])]
185
  self.example_prompt = json_data.get("trainedWords", [""])[0] if json_data.get("trainedWords") else ""
186
  self.original_json = copy.deepcopy(json_data)
 
187
 
188
 
189
  def retrieve_model_info(url):
190
  json_data = request_json_data(url)
191
  if not json_data:
192
  return None
193
- model_descriptor = ModelInformation(json_data)
 
 
 
 
 
 
 
194
  return model_descriptor
195
 
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  def download_things(directory, url, hf_token="", civitai_api_key="", romanize=False):
198
  hf_token = get_token()
199
  url = url.strip()
200
  downloaded_file_path = None
201
 
202
  if "drive.google.com" in url:
 
203
  original_dir = os.getcwd()
204
  os.chdir(directory)
205
  os.system(f"gdown --fuzzy {url}")
206
  os.chdir(original_dir)
 
207
  elif "huggingface.co" in url:
208
  url = url.replace("?download=true", "")
209
- # url = urllib.parse.quote(url, safe=':/') # fix encoding
210
  if "/blob/" in url:
211
  url = url.replace("/blob/", "/resolve/")
212
 
213
  filename = unidecode(url.split('/')[-1]) if romanize else url.split('/')[-1]
214
-
215
  download_hf_file(directory, url, filename, hf_token)
216
-
217
  downloaded_file_path = os.path.join(directory, filename)
218
-
219
- elif "civitai.com" in url:
220
-
221
  if not civitai_api_key:
222
- print("\033[91mYou need an API key to download Civitai models.\033[0m")
223
-
224
- model_profile = retrieve_model_info(url)
225
- if model_profile.download_url and model_profile.filename_url:
 
 
 
 
226
  url = model_profile.download_url
227
- filename = unidecode(model_profile.filename_url) if romanize else model_profile.filename_url
 
 
228
  else:
229
- if "?" in url:
230
- url = url.split("?")[0]
 
 
231
  filename = ""
232
 
233
- url_dl = url + f"?token={civitai_api_key}"
234
- print(f"Filename: {filename}")
235
-
236
- param_filename = ""
237
- if filename:
238
- param_filename = f"-o '{filename}'"
239
-
240
- aria2_command = (
241
- f'aria2c --console-log-level=error --summary-interval=10 -c -x 16 '
242
- f'-k 1M -s 16 -d "{directory}" {param_filename} "{url_dl}"'
243
- )
244
- os.system(aria2_command)
245
-
246
- if param_filename and os.path.exists(os.path.join(directory, filename)):
247
- downloaded_file_path = os.path.join(directory, filename)
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  else:
250
- os.system(f"aria2c --console-log-level=error --summary-interval=10 -c -x 16 -k 1M -s 16 -d {directory} {url}")
 
 
251
 
 
 
252
  return downloaded_file_path
253
 
254
 
@@ -265,13 +818,15 @@ def get_download_file(temp_dir, url, civitai_key="", progress=gr.Progress(track_
265
  else:
266
  print(f"Start downloading: {url}")
267
  before = get_local_model_list(temp_dir)
 
268
  try:
269
- download_things(temp_dir, url.strip(), HF_TOKEN, civitai_key)
270
  except Exception:
271
  print(f"Download failed: {url}")
272
  return ""
273
  after = get_local_model_list(temp_dir)
274
- new_file = list_sub(after, before)[0] if list_sub(after, before) else ""
 
275
  if not new_file:
276
  print(f"Download failed: {url}")
277
  return ""
@@ -576,41 +1131,6 @@ def get_private_lora_model_lists():
576
  private_lora_model_list = get_private_lora_model_lists()
577
 
578
 
579
- def get_civitai_info(path):
580
- global civitai_not_exists_list
581
- default = ["", "", "", "", ""]
582
- if path in set(civitai_not_exists_list): return default
583
- if not Path(path).exists(): return None
584
- user_agent = get_user_agent()
585
- headers = {'User-Agent': user_agent, 'content-type': 'application/json'}
586
- base_url = 'https://civitai.com/api/v1/model-versions/by-hash/'
587
- params = {}
588
- session = requests.Session()
589
- retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
590
- session.mount("https://", HTTPAdapter(max_retries=retries))
591
- import hashlib
592
- with open(path, 'rb') as file:
593
- file_data = file.read()
594
- hash_sha256 = hashlib.sha256(file_data).hexdigest()
595
- url = base_url + hash_sha256
596
- try:
597
- r = session.get(url, params=params, headers=headers, stream=True, timeout=(3.0, 15))
598
- except Exception as e:
599
- print(e)
600
- return default
601
- if not r.ok: return None
602
- json = r.json()
603
- if not 'baseModel' in json:
604
- civitai_not_exists_list.append(path)
605
- return default
606
- items = []
607
- items.append(" / ".join(json['trainedWords']))
608
- items.append(json['baseModel'])
609
- items.append(json['model']['name'])
610
- items.append(f"https://civitai.com/models/{json['modelId']}")
611
- items.append(json['images'][0]['url'])
612
- return items
613
-
614
 
615
  def get_lora_model_list():
616
  loras = list_uniq(get_private_lora_model_lists() + DIFFUSERS_FORMAT_LORAS + get_local_model_list(DIRECTORY_LORAS))
@@ -662,28 +1182,58 @@ def update_lora_dict(path):
662
  loras_dict[key] = items
663
 
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  def download_lora(dl_urls: str):
666
  global loras_url_to_path_dict
667
  dl_path = ""
668
- before = get_local_model_list(DIRECTORY_LORAS)
669
- urls = []
670
- for url in [url.strip() for url in dl_urls.split(',')]:
671
- local_path = f"{DIRECTORY_LORAS}/{url.split('/')[-1]}"
672
- if not Path(local_path).exists():
673
- download_things(DIRECTORY_LORAS, url, HF_TOKEN, CIVITAI_API_KEY)
674
- urls.append(url)
675
- after = get_local_model_list(DIRECTORY_LORAS)
676
- new_files = list_sub(after, before)
677
- i = 0
678
- for file in new_files:
679
- path = Path(file)
680
- if path.exists():
681
- new_path = Path(f'{path.parent.name}/{escape_lora_basename(path.stem)}{path.suffix}')
682
- path.resolve().rename(new_path.resolve())
683
- loras_url_to_path_dict[urls[i]] = str(new_path)
684
- update_lora_dict(str(new_path))
685
- dl_path = str(new_path)
686
- i += 1
687
  return dl_path
688
 
689
 
@@ -746,9 +1296,15 @@ def get_valid_lora_name(query: str, model_name: str):
746
 
747
  def get_valid_lora_path(query: str):
748
  path = None
749
- if not query or query == "None": return None
750
- if to_lora_key(query) in loras_dict.keys(): return query
751
- if Path(path).exists():
 
 
 
 
 
 
752
  return path
753
  else:
754
  return None
@@ -797,7 +1353,7 @@ def set_prompt_loras(prompt, prompt_syntax, model_name, lora1, lora1_wt, lora2,
797
  wt = result[0][1]
798
  path = to_lora_path(key)
799
  if not key in loras_dict.keys() or not Path(path).exists():
800
- path = get_valid_lora_name(path)
801
  if not path or path == "None": continue
802
  if path in lora_paths or key in lora_paths:
803
  continue
@@ -1037,84 +1593,109 @@ CIVITAI_FILETYPE = ["Model", "VAE", "Config", "Training Data"]
1037
  def get_civitai_info(path):
1038
  global civitai_not_exists_list, loras_url_to_path_dict
1039
  default = ["", "", "", "", ""]
1040
- if path in set(civitai_not_exists_list): return default
1041
- if not Path(path).exists(): return None
1042
- user_agent = get_user_agent()
1043
- headers = {'User-Agent': user_agent, 'content-type': 'application/json'}
 
 
1044
  base_url = 'https://civitai.com/api/v1/model-versions/by-hash/'
1045
- params = {}
1046
- session = requests.Session()
1047
- retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
1048
- session.mount("https://", HTTPAdapter(max_retries=retries))
1049
  import hashlib
 
1050
  with open(path, 'rb') as file:
1051
- file_data = file.read()
1052
- hash_sha256 = hashlib.sha256(file_data).hexdigest()
 
1053
  url = base_url + hash_sha256
1054
  try:
1055
- r = session.get(url, params=params, headers=headers, stream=True, timeout=(3.0, 15))
1056
  except Exception as e:
1057
- print(e)
1058
  return default
1059
- else:
1060
- if not r.ok: return None
1061
- json = r.json()
1062
- if 'baseModel' not in json:
1063
  civitai_not_exists_list.append(path)
1064
  return default
1065
- items = []
1066
- items.append(" / ".join(json['trainedWords'])) # The words (prompts) used to trigger the model
1067
- items.append(json['baseModel']) # Base model (SDXL1.0, Pony, ...)
1068
- items.append(json['model']['name']) # The name of the model version
1069
- items.append(f"https://civitai.com/models/{json['modelId']}") # The repo url for the model
1070
- items.append(json['images'][0]['url']) # The url for a sample image
1071
- loras_url_to_path_dict[path] = json['downloadUrl'] # The download url to get the model file for this specific version
1072
- return items
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
 
1074
 
1075
  def search_lora_on_civitai(query: str, allow_model: list[str] = ["Pony", "SDXL 1.0"], limit: int = 100,
1076
  sort: str = "Highest Rated", period: str = "AllTime", tag: str = "", user: str = "", page: int = 1):
1077
- user_agent = get_user_agent()
1078
- headers = {'User-Agent': user_agent, 'content-type': 'application/json'}
1079
- if CIVITAI_API_KEY: headers['Authorization'] = f'Bearer {{{CIVITAI_API_KEY}}}'
1080
  base_url = 'https://civitai.com/api/v1/models'
1081
  params = {'types': ['LORA'], 'sort': sort, 'period': period, 'limit': limit, 'page': int(page), 'nsfw': 'true'}
1082
- if query: params["query"] = query
1083
- if tag: params["tag"] = tag
1084
- if user: params["username"] = user
1085
- session = requests.Session()
1086
- retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
1087
- session.mount("https://", HTTPAdapter(max_retries=retries))
 
1088
  try:
1089
- r = session.get(base_url, params=params, headers=headers, stream=True, timeout=(3.0, 30))
1090
  except Exception as e:
1091
- print(e)
1092
  return None
1093
- else:
1094
- if not r.ok: return None
1095
- json = r.json()
1096
- if 'items' not in json: return None
1097
- items = []
1098
- for j in json['items']:
1099
- for model in j['modelVersions']:
1100
- item = {}
1101
- if len(allow_model) != 0 and model['baseModel'] not in set(allow_model): continue
1102
- item['name'] = j['name']
1103
- item['creator'] = j['creator']['username'] if 'creator' in j.keys() and 'username' in j['creator'].keys() else ""
1104
- item['tags'] = j['tags'] if 'tags' in j.keys() else []
1105
- item['model_name'] = model['name'] if 'name' in model.keys() else ""
1106
- item['base_model'] = model['baseModel'] if 'baseModel' in model.keys() else ""
1107
- item['description'] = model['description'] if 'description' in model.keys() else ""
1108
- item['dl_url'] = model['downloadUrl']
1109
- item['md'] = ""
1110
- if 'images' in model.keys() and len(model["images"]) != 0:
1111
- item['img_url'] = model["images"][0]["url"]
1112
- item['md'] += f'<img src="{model["images"][0]["url"]}#float" alt="thumbnail" width="150" height="240"><br>'
1113
- else: item['img_url'] = "/home/user/app/null.png"
1114
- item['md'] += f'''Model URL: [https://civitai.com/models/{j["id"]}](https://civitai.com/models/{j["id"]})<br>Model Name: {item["name"]}<br>
1115
- Creator: {item["creator"]}<br>Tags: {", ".join(item["tags"])}<br>Base Model: {item["base_model"]}<br>Description: {item["description"]}'''
1116
- items.append(item)
1117
- return items
 
 
 
 
 
 
 
 
 
 
 
1118
 
1119
 
1120
  def search_civitai_lora(query, base_model=[], sort=CIVITAI_SORT[0], period=CIVITAI_PERIOD[0], tag="", user="", gallery=[]):
 
22
  import torch
23
  from safetensors.torch import load_file
24
  import gc
25
+ import html as html_lib
26
+ import subprocess
27
 
28
 
29
  from env import (HF_LORA_PRIVATE_REPOS1, HF_LORA_PRIVATE_REPOS2,
 
141
 
142
 
143
  USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0'
144
+ CIVITAI_DEFAULT_ORIGIN = "https://civitai.com"
145
+ CIVITAI_REFERER = f"{CIVITAI_DEFAULT_ORIGIN}/"
146
+ CIVITAI_HOST_ALIASES = frozenset({"civitai.com", "www.civitai.com", "civitai.green", "www.civitai.green"})
147
+ CIVITAI_RETRY_TOTAL = 5
148
+ CIVITAI_RETRY_BACKOFF = 1.0
149
+ CIVITAI_RESOLVE_RETRY_TOTAL = 4
150
+ CIVITAI_RESOLVE_RETRY_BACKOFF = 0.8
151
+ CIVITAI_STATUS_FORCELIST = [429, 500, 502, 503, 504]
152
+ CIVITAI_RESOLVE_TIMEOUT = (7.0, 25.0)
153
+ CIVITAI_METADATA_TIMEOUT = (3.0, 15.0)
154
+ CIVITAI_SEARCH_TIMEOUT = (3.0, 30.0)
155
+ CIVITAI_NEGATIVE_CACHE_LIMIT = 256
156
+ CIVITAI_RESOLVE_CACHE: dict[str, str] = {}
157
+ CIVITAI_RESOLVE_NEGATIVE_CACHE: dict[str, str] = {}
158
+ CIVITAI_VERSION_JSON_CACHE: dict[str, dict] = {}
159
+ CIVITAI_VERSION_NEGATIVE_CACHE: dict[str, str] = {}
160
+ CIVITAI_ARIA2_CONNECTIONS = 1
161
+ CIVITAI_ARIA2_SPLIT = 1
162
+ CIVITAI_ARIA2_MIN_SPLIT_SIZE = "1M"
163
+ CIVITAI_ARIA2_RESUME = False
164
+ CIVITAI_ARIA2_FRESH_RETRY_LIMIT = 1
165
+
166
+
167
+ def create_retry_session(total=CIVITAI_RETRY_TOTAL, backoff_factor=CIVITAI_RETRY_BACKOFF):
168
+ session = requests.Session()
169
+ retries = Retry(total=total, backoff_factor=backoff_factor, status_forcelist=CIVITAI_STATUS_FORCELIST)
170
+ session.mount("https://", HTTPAdapter(max_retries=retries))
171
+ session.mount("http://", HTTPAdapter(max_retries=retries))
172
+ return session
173
 
174
 
175
+ def cache_put(cache: dict, key: str, value):
176
+ key = str(key or "").strip()
177
+ if not key:
178
+ return
179
+ if key in cache:
180
+ cache.pop(key, None)
181
+ elif len(cache) >= CIVITAI_NEGATIVE_CACHE_LIMIT:
182
+ try:
183
+ cache.pop(next(iter(cache)))
184
+ except Exception:
185
+ cache.clear()
186
+ cache[key] = value
187
 
 
188
 
189
+ def get_civitai_headers(api_key: str = ""):
190
  headers = {'User-Agent': USER_AGENT, 'content-type': 'application/json'}
191
+ if api_key:
192
+ headers['Authorization'] = f'Bearer {api_key}'
193
+ return headers
194
+
195
+
196
+ def get_civitai_url_parts(url: str):
197
+ try:
198
+ return urllib.parse.urlsplit(str(url or "").strip())
199
+ except Exception:
200
+ return urllib.parse.urlsplit("")
201
+
202
+
203
+ def sanitize_url_for_log(url: str):
204
+ raw = str(url or "").strip()
205
+ if not raw:
206
+ return raw
207
+ parts = get_civitai_url_parts(raw)
208
+ if not parts.netloc:
209
+ return raw
210
+ pairs = [
211
+ (k, v)
212
+ for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True)
213
+ if str(k).lower() != "token"
214
+ ]
215
+ query = urllib.parse.urlencode(pairs)
216
+ return urllib.parse.urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment))
217
+
218
+
219
+ def is_civitai_host(netloc: str):
220
+ return str(netloc or "").strip().lower() in CIVITAI_HOST_ALIASES
221
+
222
+
223
+ def is_civitai_url(url: str):
224
+ return is_civitai_host(get_civitai_url_parts(url).netloc)
225
+
226
+
227
+ def is_civitai_download_api_path(path: str):
228
+ return re.match(r'^/api/download/models/\d+$', str(path or "").strip()) is not None
229
+
230
+
231
+ def extract_civitai_model_version_id(url: str):
232
+ try:
233
+ parts = get_civitai_url_parts(url)
234
+ for pattern in [r'^/api/download/models/(\d+)$', r'^/api/v1/model-versions/(\d+)$']:
235
+ m = re.match(pattern, str(parts.path or "").strip())
236
+ if m:
237
+ return m.group(1)
238
+ qs = urllib.parse.parse_qs(parts.query)
239
+ for key in ["modelVersionId", "modelversionid", "versionId", "versionid"]:
240
+ values = qs.get(key, [])
241
+ if not values:
242
+ continue
243
+ value = str(values[0]).strip()
244
+ if value.isdigit():
245
+ return value
246
+ except Exception:
247
+ return ""
248
+ return ""
249
+
250
+
251
+ def get_civitai_query_filters(url: str):
252
+ try:
253
+ parts = get_civitai_url_parts(url)
254
+ qs = urllib.parse.parse_qs(parts.query)
255
+ except Exception:
256
+ return {}
257
+ filters = {}
258
+ for key in ["type", "format", "size", "fp"]:
259
+ values = qs.get(key, [])
260
+ if values:
261
+ filters[key] = str(values[0]).strip()
262
+ return filters
263
+
264
+
265
+ def normalize_civitai_filter_value(key: str, value):
266
+ if value is None:
267
+ return ""
268
+ text = str(value).strip()
269
+ if not text:
270
+ return ""
271
+ if key == "fp":
272
+ return text.replace("-", "").replace("_", "").replace(" ", "").lower()
273
+ return text.lower()
274
 
275
+
276
+ def describe_civitai_file_for_log(file_info):
277
+ if not isinstance(file_info, dict):
278
+ return ""
279
+ parts = []
280
+ for key in ["name", "type", "format", "size", "fp"]:
281
+ value = file_info.get(key)
282
+ if value is None:
283
+ continue
284
+ text = str(value).strip()
285
+ if text:
286
+ parts.append(f"{key}={text}")
287
+ hashes = file_info.get("hashes") if isinstance(file_info.get("hashes"), dict) else {}
288
+ sha256 = str(hashes.get("SHA256") or "").strip()
289
+ if sha256:
290
+ parts.append(f"sha256={sha256[:12]}...")
291
+ return ", ".join(parts)
292
+
293
+
294
+ def build_civitai_download_query_from_url(url: str):
295
  try:
296
+ parts = get_civitai_url_parts(url)
297
+ except Exception:
298
+ return ""
299
+ blocked = {"modelversionid", "versionid"}
300
+ pairs = [
301
+ (k, v)
302
+ for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True)
303
+ if str(k).lower() not in blocked
304
+ ]
305
+ return urllib.parse.urlencode(pairs)
306
+
307
+
308
+ def to_civitai_default_download_url(version_id: str, query: str = ""):
309
+ if not str(version_id or "").isdigit():
310
+ return ""
311
+ base = f"{CIVITAI_DEFAULT_ORIGIN}/api/download/models/{version_id}"
312
+ return f"{base}?{query}" if query else base
313
+
314
+
315
+ def normalize_civitai_download_api_url(url: str):
316
+ parts = get_civitai_url_parts(url)
317
+ if not is_civitai_host(parts.netloc) or not is_civitai_download_api_path(parts.path):
318
+ return str(url or "").strip()
319
+ return urllib.parse.urlunsplit(("https", "civitai.com", parts.path, parts.query, ""))
320
+
321
+
322
+ def extract_first_civitai_download_url_from_html(html: str):
323
+ if not html:
324
+ return ""
325
+ page = html_lib.unescape(str(html))
326
+ patterns = [
327
+ r'https?://(?:www\.)?(?:civitai\.com|civitai\.green)/api/download/models/\d+[^\s\'\"<>\)\]\}]*',
328
+ r'["\'](/api/download/models/\d+[^"\']*)["\']',
329
+ ]
330
+ for pattern in patterns:
331
+ try:
332
+ m = re.search(pattern, page, flags=re.IGNORECASE)
333
+ except re.error:
334
+ m = None
335
+ if not m:
336
+ continue
337
+ candidate = m.group(1) if m.lastindex else m.group(0)
338
+ candidate = str(candidate or "").strip("\"'")
339
+ if candidate.startswith("/"):
340
+ candidate = urllib.parse.urljoin(CIVITAI_DEFAULT_ORIGIN, candidate)
341
+ return normalize_civitai_download_api_url(candidate)
342
+ return ""
343
+
344
+
345
+ def resolve_civitai_model_page_to_download_url(url: str, api_key: str = ""):
346
+ raw = str(url or "").strip()
347
+ if not raw:
348
+ return raw
349
+ cached = CIVITAI_RESOLVE_CACHE.get(raw)
350
+ if cached:
351
+ return cached
352
+ if raw in CIVITAI_RESOLVE_NEGATIVE_CACHE:
353
+ return raw
354
+ parts = get_civitai_url_parts(raw)
355
+ if not is_civitai_host(parts.netloc):
356
+ return raw
357
+ if is_civitai_download_api_path(parts.path):
358
+ normalized = normalize_civitai_download_api_url(raw)
359
+ cache_put(CIVITAI_RESOLVE_CACHE, raw, normalized)
360
+ return normalized
361
+ if not re.match(r'^/models/\d+(?:/[^/?#]+)?/?$', parts.path or ""):
362
+ return raw
363
+ version_id = extract_civitai_model_version_id(raw)
364
+ if version_id:
365
+ normalized = to_civitai_default_download_url(version_id, query=build_civitai_download_query_from_url(raw))
366
+ cache_put(CIVITAI_RESOLVE_CACHE, raw, normalized)
367
+ return normalized
368
+ headers = get_civitai_headers(api_key if parts.netloc.lower().endswith("civitai.com") else "")
369
+ headers['Referer'] = f"{parts.scheme or 'https'}://{parts.netloc}/"
370
+ session = create_retry_session(total=CIVITAI_RESOLVE_RETRY_TOTAL, backoff_factor=CIVITAI_RESOLVE_RETRY_BACKOFF)
371
+ try:
372
+ r = session.get(raw, headers=headers, timeout=CIVITAI_RESOLVE_TIMEOUT)
373
+ if not r.ok:
374
+ print(f"Civitai model page resolve failed: {sanitize_url_for_log(raw)} status={r.status_code}")
375
+ if r.status_code in [400, 401, 403, 404]:
376
+ cache_put(CIVITAI_RESOLVE_NEGATIVE_CACHE, raw, f"status={r.status_code}")
377
+ return raw
378
+ extracted = extract_first_civitai_download_url_from_html(r.text)
379
+ if extracted:
380
+ normalized = normalize_civitai_download_api_url(extracted)
381
+ cache_put(CIVITAI_RESOLVE_CACHE, raw, normalized)
382
+ return normalized
383
+ cache_put(CIVITAI_RESOLVE_NEGATIVE_CACHE, raw, "html_no_download_url")
384
+ return raw
385
+ except Exception as e:
386
+ print(f"Failed to resolve Civitai model page URL: {sanitize_url_for_log(raw)} {type(e).__name__}: {e}")
387
+ return raw
388
+
389
+
390
+ def normalize_civitai_input_url(url: str, api_key: str = ""):
391
+ raw = str(url or "").strip()
392
+ if not raw or not is_civitai_url(raw):
393
+ return raw
394
+ normalized = resolve_civitai_model_page_to_download_url(raw, api_key=api_key)
395
+ if normalized != raw:
396
+ print(f"Normalized Civitai URL: {sanitize_url_for_log(raw)} -> {sanitize_url_for_log(normalized)}")
397
+ return normalized
398
+
399
+
400
+ def append_civitai_token(url: str, api_key: str = ""):
401
+ raw = str(url or "").strip()
402
+ if not raw or not api_key:
403
+ return raw
404
+ parts = get_civitai_url_parts(raw)
405
+ pairs = [(k, v) for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True) if k.lower() != "token"]
406
+ pairs.append(("token", api_key))
407
+ query = urllib.parse.urlencode(pairs)
408
+ return urllib.parse.urlunsplit((parts.scheme or "https", parts.netloc, parts.path, query, parts.fragment))
409
+
410
+
411
+ def get_civitai_request_context(url: str, api_key: str = ""):
412
+ raw_url = str(url or "").strip()
413
+ normalized_url = normalize_civitai_input_url(raw_url, api_key=api_key)
414
+ model_version_id = extract_civitai_model_version_id(normalized_url) or extract_civitai_model_version_id(raw_url)
415
+ return {
416
+ "raw_url": raw_url,
417
+ "normalized_url": normalized_url,
418
+ "model_version_id": model_version_id,
419
+ "filters": get_civitai_query_filters(raw_url),
420
+ }
421
+
422
+
423
+ def resolve_civitai_download_url(url: str, civitai_api_key: str = "", max_tries: int = 3):
424
+ raw = normalize_civitai_download_api_url(str(url or "").strip())
425
+ if not raw:
426
+ return raw
427
+ headers = get_civitai_headers(civitai_api_key)
428
+ headers["Referer"] = CIVITAI_REFERER
429
+ dl_url = append_civitai_token(raw, civitai_api_key)
430
+ last_error = None
431
+ for attempt in range(1, max_tries + 1):
432
+ response = None
433
+ try:
434
+ response = create_retry_session(total=3, backoff_factor=1.0).get(
435
+ dl_url,
436
+ headers=headers,
437
+ allow_redirects=False,
438
+ stream=True,
439
+ timeout=CIVITAI_RESOLVE_TIMEOUT,
440
+ )
441
+ status = int(response.status_code)
442
+ location = str(response.headers.get("Location") or "").strip()
443
+ resolved_url = str(location or response.url or dl_url).strip()
444
+ resolved_host = get_civitai_url_parts(resolved_url).netloc
445
+ print(
446
+ f"[civitai] resolve signed url attempt={attempt}/{max_tries} status={status} "
447
+ f"host={resolved_host or '-'} url={sanitize_url_for_log(raw)}"
448
+ )
449
+ if status in (301, 302, 303, 307, 308) and location:
450
+ return resolved_url
451
+ if response.ok and resolved_url and not is_civitai_host(resolved_host):
452
+ return resolved_url
453
+ last_error = RuntimeError(f"status={status}")
454
+ except Exception as e:
455
+ last_error = e
456
+ print(
457
+ f"[civitai] resolve signed url failed attempt={attempt}/{max_tries} "
458
+ f"url={sanitize_url_for_log(raw)} error={type(e).__name__}: {e}"
459
+ )
460
+ finally:
461
+ try:
462
+ if response is not None:
463
+ response.close()
464
+ except Exception:
465
+ pass
466
+ if attempt < max_tries:
467
+ time.sleep(min(3.0, 0.8 * attempt))
468
+ if last_error is not None:
469
+ raise last_error
470
+ raise RuntimeError("Failed to resolve Civitai signed download URL")
471
+
472
+
473
+ def pick_civitai_file_from_version_json(json_data, source_url: str = ""):
474
+ files = json_data.get("files", []) if isinstance(json_data, dict) else []
475
+ if not isinstance(files, list) or not files:
476
+ return {}
477
+ version_id = str((json_data or {}).get("id") or "")
478
+ filters = get_civitai_query_filters(source_url)
479
+ candidates = []
480
+ fallback = []
481
+ for idx, file_info in enumerate(files):
482
+ if not isinstance(file_info, dict):
483
+ continue
484
+ mismatch = False
485
+ matched_filter_count = 0
486
+ for key, expected in filters.items():
487
+ actual = file_info.get(key)
488
+ expected_norm = normalize_civitai_filter_value(key, expected)
489
+ actual_norm = normalize_civitai_filter_value(key, actual)
490
+ if actual_norm:
491
+ if actual_norm != expected_norm:
492
+ mismatch = True
493
+ break
494
+ matched_filter_count += 1
495
+ download_url = str(file_info.get("downloadUrl") or "")
496
+ score = 0
497
+ if matched_filter_count:
498
+ score += matched_filter_count * 3
499
+ if version_id and version_id in download_url:
500
+ score += 4
501
+ if download_url:
502
+ score += 2
503
+ if file_info.get("name"):
504
+ score += 1
505
+ hashes = file_info.get("hashes") if isinstance(file_info.get("hashes"), dict) else {}
506
+ if str(hashes.get("SHA256") or "").strip():
507
+ score += 1
508
+ target = fallback if mismatch else candidates
509
+ target.append((score, idx, file_info))
510
+ pool = candidates if candidates else fallback
511
+ if not pool:
512
+ return {}
513
+ pool.sort(key=lambda item: (item[0], item[1]), reverse=True)
514
+ return dict(pool[0][2])
515
+
516
+
517
+ def request_json_data(url):
518
+ context = get_civitai_request_context(url, api_key=CIVITAI_API_KEY)
519
+ raw_url = context["raw_url"]
520
+ normalized_url = context["normalized_url"]
521
+ model_version_id = context["model_version_id"]
522
+ if not model_version_id:
523
+ print(f"Civitai metadata lookup skipped: modelVersionId not found for {sanitize_url_for_log(raw_url)}")
524
+ cache_put(CIVITAI_RESOLVE_NEGATIVE_CACHE, raw_url, "missing_model_version_id")
525
+ return None
526
+
527
+ cached_json = CIVITAI_VERSION_JSON_CACHE.get(model_version_id)
528
+ if cached_json:
529
+ return copy.deepcopy(cached_json)
530
+ if model_version_id in CIVITAI_VERSION_NEGATIVE_CACHE:
531
+ return None
532
+
533
+ endpoint_url = f"https://civitai.com/api/v1/model-versions/{model_version_id}"
534
+ headers = get_civitai_headers(CIVITAI_API_KEY)
535
+ session = create_retry_session()
536
+
537
+ try:
538
+ result = session.get(endpoint_url, headers=headers, stream=True, timeout=CIVITAI_METADATA_TIMEOUT)
539
+ if result.status_code == 404:
540
+ print(f"Civitai metadata lookup status=404: {endpoint_url}")
541
+ cache_put(CIVITAI_VERSION_NEGATIVE_CACHE, model_version_id, "status=404")
542
+ return None
543
  result.raise_for_status()
544
  json_data = result.json()
545
+ if not json_data:
546
+ print(f"Civitai metadata lookup returned empty JSON: {endpoint_url}")
547
+ cache_put(CIVITAI_VERSION_NEGATIVE_CACHE, model_version_id, "empty_json")
548
+ return None
549
+ cache_put(CIVITAI_VERSION_JSON_CACHE, model_version_id, copy.deepcopy(json_data))
550
+ if normalized_url and normalized_url != raw_url:
551
+ cache_put(CIVITAI_RESOLVE_CACHE, raw_url, normalized_url)
552
+ return json_data
553
  except Exception as e:
554
+ print(f"Civitai metadata lookup failed: {endpoint_url} {type(e).__name__}: {e}")
555
  return None
556
 
557
 
558
  class ModelInformation:
559
+ def __init__(self, json_data, source_url: str = ""):
560
+ selected_file = pick_civitai_file_from_version_json(json_data, source_url=source_url)
561
  self.model_version_id = json_data.get("id", "")
562
  self.model_id = json_data.get("modelId", "")
563
+ self.download_url = selected_file.get("downloadUrl", "") or json_data.get("downloadUrl", "")
564
  self.model_url = f"https://civitai.com/models/{self.model_id}?modelVersionId={self.model_version_id}"
565
+ self.filename_url = selected_file.get("name", "") or ""
 
 
 
566
  self.description = json_data.get("description", "")
567
+ if self.description is None:
568
+ self.description = ""
569
  self.model_name = json_data.get("model", {}).get("name", "")
570
  self.model_type = json_data.get("model", {}).get("type", "")
571
  self.nsfw = json_data.get("model", {}).get("nsfw", False)
 
573
  self.images = [img.get("url", "") for img in json_data.get("images", [])]
574
  self.example_prompt = json_data.get("trainedWords", [""])[0] if json_data.get("trainedWords") else ""
575
  self.original_json = copy.deepcopy(json_data)
576
+ self.selected_file = copy.deepcopy(selected_file)
577
 
578
 
579
  def retrieve_model_info(url):
580
  json_data = request_json_data(url)
581
  if not json_data:
582
  return None
583
+ model_descriptor = ModelInformation(json_data, source_url=url)
584
+ filters = get_civitai_query_filters(url)
585
+ if filters:
586
+ selected_summary = describe_civitai_file_for_log(model_descriptor.selected_file)
587
+ if selected_summary:
588
+ print(f"Civitai selected file: filters={filters} {selected_summary}")
589
+ else:
590
+ print(f"Civitai selected file: filters={filters} using model-level downloadUrl")
591
  return model_descriptor
592
 
593
 
594
+ def list_downloaded_candidate_files(directory):
595
+ try:
596
+ return {
597
+ str(path.resolve())
598
+ for path in Path(directory).iterdir()
599
+ if path.is_file() and not path.name.endswith(".aria2")
600
+ }
601
+ except Exception:
602
+ return set()
603
+
604
+
605
+ def sanitize_civitai_log_text(text: str):
606
+ output = str(text or "")
607
+ if not output:
608
+ return output
609
+ output = re.sub(r"([?&]token=)[^&\s\"']+", r"\1***", output, flags=re.IGNORECASE)
610
+ output = re.sub(r"([?&]Authorization=)[^&\s\"']+", r"\1***", output, flags=re.IGNORECASE)
611
+ return output
612
+
613
+
614
+ def build_civitai_aria2_args(directory, download_url: str, filename: str = ""):
615
+ args = [
616
+ "aria2c",
617
+ "--console-log-level=error",
618
+ "--summary-interval=10",
619
+ "--user-agent", USER_AGENT,
620
+ "--referer", CIVITAI_REFERER,
621
+ "-x", str(CIVITAI_ARIA2_CONNECTIONS),
622
+ "-k", str(CIVITAI_ARIA2_MIN_SPLIT_SIZE),
623
+ "-s", str(CIVITAI_ARIA2_SPLIT),
624
+ "-d", str(directory),
625
+ ]
626
+ if CIVITAI_ARIA2_RESUME:
627
+ args.append("-c")
628
+ if filename:
629
+ args.extend(["-o", str(filename)])
630
+ args.append(str(download_url))
631
+ return args
632
+
633
+
634
+ def run_civitai_aria2(directory, download_url: str, filename: str = ""):
635
+ args = build_civitai_aria2_args(directory, download_url, filename=filename)
636
+ result = subprocess.run(args, capture_output=True, text=True)
637
+ output = "\n".join([part for part in [result.stdout, result.stderr] if part]).strip()
638
+ return result.returncode, output
639
+
640
+
641
+ def classify_civitai_download_failure(output_text: str):
642
+ text = str(output_text or "")
643
+ lower = text.lower()
644
+ if "status=403" in lower and "b2.civitai.com" in lower:
645
+ return "b2_403"
646
+ if "status=403" in lower and "civitai.com/api/download/models/" in lower:
647
+ return "api_403"
648
+ if "status=403" in lower:
649
+ return "http_403"
650
+ if "timed out" in lower or "timeout" in lower:
651
+ return "timeout"
652
+ return "other"
653
+
654
+
655
+ def cleanup_civitai_download_artifacts(directory, filename: str = ""):
656
+ removed = []
657
+ if not filename:
658
+ return removed
659
+ target = Path(directory) / filename
660
+ for candidate in [target, Path(str(target) + ".aria2")]:
661
+ try:
662
+ if candidate.exists() and candidate.is_file():
663
+ candidate.unlink()
664
+ removed.append(str(candidate))
665
+ except Exception as e:
666
+ print(f"[civitai] cleanup failed path={candidate} {type(e).__name__}: {e}")
667
+ return removed
668
+
669
+
670
+ def guess_downloaded_file_path(directory, before_files, expected_filename=""):
671
+ expected_path = os.path.join(directory, expected_filename) if expected_filename else ""
672
+ if expected_path and os.path.exists(expected_path):
673
+ return expected_path
674
+
675
+ after_files = list_downloaded_candidate_files(directory)
676
+ new_files = sorted(list(after_files - set(before_files)))
677
+ if len(new_files) == 1:
678
+ return new_files[0]
679
+
680
+ if expected_filename:
681
+ expected_name = str(expected_filename).strip()
682
+ stem = Path(expected_name).stem
683
+ suffix = Path(expected_name).suffix.lower()
684
+ matched = []
685
+ for path_str in new_files:
686
+ path_obj = Path(path_str)
687
+ if suffix and path_obj.suffix.lower() != suffix:
688
+ continue
689
+ if stem and (path_obj.stem == stem or path_obj.name == expected_name):
690
+ matched.append(path_str)
691
+ if len(matched) == 1:
692
+ return matched[0]
693
+
694
+ return None
695
+
696
+
697
  def download_things(directory, url, hf_token="", civitai_api_key="", romanize=False):
698
  hf_token = get_token()
699
  url = url.strip()
700
  downloaded_file_path = None
701
 
702
  if "drive.google.com" in url:
703
+ before_files = list_downloaded_candidate_files(directory)
704
  original_dir = os.getcwd()
705
  os.chdir(directory)
706
  os.system(f"gdown --fuzzy {url}")
707
  os.chdir(original_dir)
708
+ downloaded_file_path = guess_downloaded_file_path(directory, before_files)
709
  elif "huggingface.co" in url:
710
  url = url.replace("?download=true", "")
 
711
  if "/blob/" in url:
712
  url = url.replace("/blob/", "/resolve/")
713
 
714
  filename = unidecode(url.split('/')[-1]) if romanize else url.split('/')[-1]
 
715
  download_hf_file(directory, url, filename, hf_token)
 
716
  downloaded_file_path = os.path.join(directory, filename)
717
+ elif is_civitai_url(url):
 
 
718
  if not civitai_api_key:
719
+ print("You need an API key to download Civitai models.")
720
+
721
+ civitai_context = get_civitai_request_context(url, api_key=civitai_api_key)
722
+ normalized_url = civitai_context["normalized_url"]
723
+ if normalized_url != url:
724
+ print(f"Civitai download URL normalized: {sanitize_url_for_log(url)} -> {sanitize_url_for_log(normalized_url)}")
725
+ model_profile = retrieve_model_info(normalized_url)
726
+ if model_profile and model_profile.download_url:
727
  url = model_profile.download_url
728
+ filename = model_profile.filename_url or ""
729
+ if filename and romanize:
730
+ filename = unidecode(filename)
731
  else:
732
+ url = normalize_civitai_download_api_url(normalized_url)
733
+ if not is_civitai_download_api_path(get_civitai_url_parts(url).path):
734
+ print(f"Civitai download URL unresolved: {sanitize_url_for_log(normalized_url)}")
735
+ return None
736
  filename = ""
737
 
738
+ signed_url = ""
739
+ try:
740
+ signed_url = resolve_civitai_download_url(url, civitai_api_key, max_tries=2)
741
+ except Exception as e:
742
+ print(f"[civitai] failed to resolve signed download url: {sanitize_url_for_log(url)} {type(e).__name__}: {e}")
743
+ return None
 
 
 
 
 
 
 
 
 
744
 
745
+ signed_host = get_civitai_url_parts(signed_url).netloc
746
+ print(f"Filename: {filename}")
747
+ print(f"[civitai] resolved signed host={signed_host or '-'} url={sanitize_url_for_log(url)}")
748
+
749
+ before_files = list_downloaded_candidate_files(directory)
750
+ download_status, download_output = run_civitai_aria2(directory, signed_url, filename=filename)
751
+ if download_status != 0:
752
+ failure_kind = classify_civitai_download_failure(download_output)
753
+ print(
754
+ f"[civitai] download failed kind={failure_kind} status={download_status} "
755
+ f"filename={filename or '-'} url={sanitize_url_for_log(url)}"
756
+ )
757
+ if download_output:
758
+ print(sanitize_civitai_log_text(download_output))
759
+
760
+ if failure_kind == "b2_403":
761
+ retry_count = 0
762
+ while retry_count < CIVITAI_ARIA2_FRESH_RETRY_LIMIT and download_status != 0:
763
+ retry_count += 1
764
+ removed = cleanup_civitai_download_artifacts(directory, filename=filename)
765
+ stale_hint = "yes" if removed or filename else "unknown"
766
+ print(
767
+ f"[civitai] retrying fresh api/download request after b2_403 "
768
+ f"attempt={retry_count}/{CIVITAI_ARIA2_FRESH_RETRY_LIMIT} stale_resume={stale_hint} "
769
+ f"filename={filename or '-'}"
770
+ )
771
+ if removed:
772
+ print(f"[civitai] removed stale partials: {removed}")
773
+ try:
774
+ signed_url = resolve_civitai_download_url(url, civitai_api_key, max_tries=2)
775
+ signed_host = get_civitai_url_parts(signed_url).netloc
776
+ print(f"[civitai] resolved retry signed host={signed_host or '-'} url={sanitize_url_for_log(url)}")
777
+ except Exception as e:
778
+ print(f"[civitai] retry resolve failed url={sanitize_url_for_log(url)} error={type(e).__name__}: {e}")
779
+ break
780
+ download_status, download_output = run_civitai_aria2(directory, signed_url, filename=filename)
781
+ if download_status == 0:
782
+ print(f"[civitai] download recovered after fresh retry: {filename or sanitize_url_for_log(url)}")
783
+ break
784
+ retry_kind = classify_civitai_download_failure(download_output)
785
+ print(
786
+ f"[civitai] retry failed kind={retry_kind} status={download_status} "
787
+ f"filename={filename or '-'} url={sanitize_url_for_log(url)}"
788
+ )
789
+ if download_output:
790
+ print(sanitize_civitai_log_text(download_output))
791
+
792
+ if download_status != 0:
793
+ print(f"Civitai download command exited with status {download_status}: {sanitize_url_for_log(url)}")
794
+
795
+ downloaded_file_path = guess_downloaded_file_path(directory, before_files, expected_filename=filename)
796
+ if not downloaded_file_path:
797
+ print(f"Civitai downloaded file path unresolved: {sanitize_url_for_log(url)}")
798
  else:
799
+ before_files = list_downloaded_candidate_files(directory)
800
+ os.system(f"aria2c --console-log-level=error --summary-interval=10 -c -x 16 -k 1M -s 16 -d \"{directory}\" \"{url}\"")
801
+ downloaded_file_path = guess_downloaded_file_path(directory, before_files)
802
 
803
+ if downloaded_file_path and os.path.exists(downloaded_file_path):
804
+ print(f"Downloaded file path: {downloaded_file_path}")
805
  return downloaded_file_path
806
 
807
 
 
818
  else:
819
  print(f"Start downloading: {url}")
820
  before = get_local_model_list(temp_dir)
821
+ downloaded_path = ""
822
  try:
823
+ downloaded_path = download_things(temp_dir, url.strip(), HF_TOKEN, civitai_key) or ""
824
  except Exception:
825
  print(f"Download failed: {url}")
826
  return ""
827
  after = get_local_model_list(temp_dir)
828
+ fallback_files = list_sub(after, before)
829
+ new_file = downloaded_path if downloaded_path and Path(downloaded_path).exists() else (fallback_files[0] if fallback_files else "")
830
  if not new_file:
831
  print(f"Download failed: {url}")
832
  return ""
 
1131
  private_lora_model_list = get_private_lora_model_lists()
1132
 
1133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1134
 
1135
  def get_lora_model_list():
1136
  loras = list_uniq(get_private_lora_model_lists() + DIFFUSERS_FORMAT_LORAS + get_local_model_list(DIRECTORY_LORAS))
 
1182
  loras_dict[key] = items
1183
 
1184
 
1185
+ def finalize_downloaded_lora_path(file_path: str, source_url: str = ""):
1186
+ global loras_url_to_path_dict
1187
+ if not file_path:
1188
+ return ""
1189
+ path = Path(file_path)
1190
+ if not path.exists():
1191
+ return ""
1192
+ new_path = Path(f'{path.parent.name}/{escape_lora_basename(path.stem)}{path.suffix}')
1193
+ try:
1194
+ if path.resolve() != new_path.resolve():
1195
+ if new_path.exists():
1196
+ new_path = new_path.resolve()
1197
+ else:
1198
+ new_path = path.resolve().rename(new_path.resolve())
1199
+ else:
1200
+ new_path = path.resolve()
1201
+ except Exception as e:
1202
+ print(f"Failed to normalize downloaded lora path: {file_path} {e}")
1203
+ new_path = path.resolve()
1204
+
1205
+ final_path = str(new_path)
1206
+ if source_url:
1207
+ loras_url_to_path_dict[source_url] = final_path
1208
+ if is_civitai_url(source_url):
1209
+ normalized_url = get_civitai_request_context(source_url, api_key=CIVITAI_API_KEY).get("normalized_url", "")
1210
+ if normalized_url:
1211
+ loras_url_to_path_dict[normalized_url] = final_path
1212
+ update_lora_dict(final_path)
1213
+ return final_path
1214
+
1215
+
1216
  def download_lora(dl_urls: str):
1217
  global loras_url_to_path_dict
1218
  dl_path = ""
1219
+ for url in [url.strip() for url in dl_urls.split(',') if url.strip()]:
1220
+ cached_path = loras_url_to_path_dict.get(url, "")
1221
+ if cached_path and Path(cached_path).exists():
1222
+ dl_path = cached_path
1223
+ continue
1224
+
1225
+ if is_civitai_url(url):
1226
+ normalized_url = get_civitai_request_context(url, api_key=CIVITAI_API_KEY).get("normalized_url", "")
1227
+ cached_path = loras_url_to_path_dict.get(normalized_url, "") if normalized_url else ""
1228
+ if cached_path and Path(cached_path).exists():
1229
+ loras_url_to_path_dict[url] = cached_path
1230
+ dl_path = cached_path
1231
+ continue
1232
+
1233
+ downloaded_path = download_things(DIRECTORY_LORAS, url, HF_TOKEN, CIVITAI_API_KEY)
1234
+ final_path = finalize_downloaded_lora_path(downloaded_path or "", source_url=url)
1235
+ if final_path:
1236
+ dl_path = final_path
 
1237
  return dl_path
1238
 
1239
 
 
1296
 
1297
  def get_valid_lora_path(query: str):
1298
  path = None
1299
+ if not query or query == "None":
1300
+ return None
1301
+ if to_lora_key(query) in loras_dict.keys():
1302
+ return query
1303
+ if query in loras_url_to_path_dict.keys():
1304
+ path = loras_url_to_path_dict[query]
1305
+ else:
1306
+ path = to_lora_path(query.strip().split('/')[-1])
1307
+ if path and Path(path).exists():
1308
  return path
1309
  else:
1310
  return None
 
1353
  wt = result[0][1]
1354
  path = to_lora_path(key)
1355
  if not key in loras_dict.keys() or not Path(path).exists():
1356
+ path = get_valid_lora_name(path, model_name)
1357
  if not path or path == "None": continue
1358
  if path in lora_paths or key in lora_paths:
1359
  continue
 
1593
  def get_civitai_info(path):
1594
  global civitai_not_exists_list, loras_url_to_path_dict
1595
  default = ["", "", "", "", ""]
1596
+ if path in set(civitai_not_exists_list):
1597
+ return default
1598
+ if not Path(path).exists():
1599
+ return None
1600
+
1601
+ headers = get_civitai_headers(CIVITAI_API_KEY)
1602
  base_url = 'https://civitai.com/api/v1/model-versions/by-hash/'
1603
+ session = create_retry_session()
1604
+
 
 
1605
  import hashlib
1606
+ sha256_hash = hashlib.sha256()
1607
  with open(path, 'rb') as file:
1608
+ for chunk in iter(lambda: file.read(1024 * 1024), b''):
1609
+ sha256_hash.update(chunk)
1610
+ hash_sha256 = sha256_hash.hexdigest()
1611
  url = base_url + hash_sha256
1612
  try:
1613
+ r = session.get(url, headers=headers, stream=True, timeout=CIVITAI_METADATA_TIMEOUT)
1614
  except Exception as e:
1615
+ print(f"Civitai by-hash lookup failed: {path} {type(e).__name__}: {e}")
1616
  return default
1617
+ if not r.ok:
1618
+ print(f"Civitai by-hash lookup status={r.status_code}: {path}")
1619
+ if r.status_code == 404:
 
1620
  civitai_not_exists_list.append(path)
1621
  return default
1622
+ return None
1623
+ try:
1624
+ json_data = r.json()
1625
+ except Exception as e:
1626
+ print(f"Civitai by-hash JSON parse failed: {path} {type(e).__name__}: {e}")
1627
+ return default
1628
+ if 'baseModel' not in json_data:
1629
+ civitai_not_exists_list.append(path)
1630
+ return default
1631
+
1632
+ selected_file = pick_civitai_file_from_version_json(json_data, source_url=json_data.get('downloadUrl', ''))
1633
+ items = []
1634
+ items.append(" / ".join(json_data.get('trainedWords', [])))
1635
+ items.append(json_data.get('baseModel', ''))
1636
+ items.append(json_data.get('model', {}).get('name', ''))
1637
+ items.append(f"https://civitai.com/models/{json_data.get('modelId', '')}")
1638
+ images = json_data.get('images', []) if isinstance(json_data.get('images'), list) else []
1639
+ items.append(images[0].get('url', '') if images else '')
1640
+ download_url = selected_file.get('downloadUrl', '') or json_data.get('downloadUrl', '')
1641
+ if download_url:
1642
+ loras_url_to_path_dict[path] = normalize_civitai_download_api_url(download_url)
1643
+ return items
1644
 
1645
 
1646
  def search_lora_on_civitai(query: str, allow_model: list[str] = ["Pony", "SDXL 1.0"], limit: int = 100,
1647
  sort: str = "Highest Rated", period: str = "AllTime", tag: str = "", user: str = "", page: int = 1):
1648
+ headers = get_civitai_headers(CIVITAI_API_KEY)
 
 
1649
  base_url = 'https://civitai.com/api/v1/models'
1650
  params = {'types': ['LORA'], 'sort': sort, 'period': period, 'limit': limit, 'page': int(page), 'nsfw': 'true'}
1651
+ if query:
1652
+ params["query"] = query
1653
+ if tag:
1654
+ params["tag"] = tag
1655
+ if user:
1656
+ params["username"] = user
1657
+ session = create_retry_session()
1658
  try:
1659
+ r = session.get(base_url, params=params, headers=headers, stream=True, timeout=CIVITAI_SEARCH_TIMEOUT)
1660
  except Exception as e:
1661
+ print(f"Civitai search failed: query={query!r} page={page} {type(e).__name__}: {e}")
1662
  return None
1663
+ if not r.ok:
1664
+ print(f"Civitai search status={r.status_code}: query={query!r} page={page}")
1665
+ return None
1666
+ json = r.json()
1667
+ if 'items' not in json:
1668
+ print(f"Civitai search returned no items key: query={query!r} page={page}")
1669
+ return None
1670
+ items = []
1671
+ for j in json['items']:
1672
+ model_versions = j.get('modelVersions') if isinstance(j, dict) else []
1673
+ if not isinstance(model_versions, list):
1674
+ continue
1675
+ for model in model_versions:
1676
+ if not isinstance(model, dict):
1677
+ continue
1678
+ base_model = model.get('baseModel', '')
1679
+ if len(allow_model) != 0 and base_model not in set(allow_model):
1680
+ continue
1681
+ item = {}
1682
+ item['name'] = j['name'] if isinstance(j, dict) and 'name' in j else ""
1683
+ item['creator'] = j['creator']['username'] if isinstance(j, dict) and 'creator' in j and isinstance(j['creator'], dict) and 'username' in j['creator'] else ""
1684
+ item['tags'] = j['tags'] if isinstance(j, dict) and 'tags' in j else []
1685
+ item['model_name'] = model.get('name', '')
1686
+ item['base_model'] = base_model
1687
+ item['description'] = model.get('description', '')
1688
+ item['dl_url'] = model.get('downloadUrl', '')
1689
+ item['md'] = ""
1690
+ if 'images' in model.keys() and len(model["images"]) != 0:
1691
+ item['img_url'] = model["images"][0]["url"]
1692
+ item['md'] += f'<img src="{model["images"][0]["url"]}#float" alt="thumbnail" width="150" height="240"><br>'
1693
+ else:
1694
+ item['img_url'] = "/home/user/app/null.png"
1695
+ item['md'] += f'''Model URL: [https://civitai.com/models/{j["id"]}](https://civitai.com/models/{j["id"]})<br>Model Name: {item["name"]}<br>
1696
+ Creator: {item["creator"]}<br>Tags: {", ".join(item["tags"])}<br>Base Model: {item["base_model"]}<br>Description: {item["description"]}'''
1697
+ items.append(item)
1698
+ return items
1699
 
1700
 
1701
  def search_civitai_lora(query, base_model=[], sort=CIVITAI_SORT[0], period=CIVITAI_PERIOD[0], tag="", user="", gallery=[]):