amisima commited on
Commit
d7f7589
·
verified ·
1 Parent(s): d37720d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -6
app.py CHANGED
@@ -349,6 +349,85 @@ def _hub_url_parts(url: str) -> tuple[str, str]:
349
  return "/".join(parts[:2]), "/".join(parts[4:])
350
 
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  def resolve_lora(reference: str) -> str:
353
  """Turn what the user typed into a local `.safetensors` path.
354
 
@@ -363,8 +442,13 @@ def resolve_lora(reference: str) -> str:
363
  if os.path.exists(reference):
364
  return reference
365
  if reference.startswith(("http://", "https://")):
366
- repo_id, filename = _hub_url_parts(reference)
367
- return hf_hub_download(repo_id, filename)
 
 
 
 
 
368
 
369
  parts = [part for part in reference.split("/") if part]
370
  if len(parts) > 2 and parts[-1].endswith(".safetensors"):
@@ -1213,15 +1297,20 @@ HERO = """
1213
  <a href="https://huggingface.co/spaces/multimodalart/minimax-h3" target="_blank" rel="noopener">text / image to video</a></p>
1214
  <div class="pills">
1215
  <span>33B</span><span>joint video + audio</span><span>Turbo lora: 4&ndash;8 steps</span>
1216
- <span>ComfyUI lora accepted</span><span>up to 9 references</span><span>named profiles</span><span>GPU cost estimate</span><span>clip stitching with audio</span>
1217
  </div>
1218
  </div>
1219
  """
1220
 
1221
  LORA_HELP = """Each slot takes a Hugging Face repo (`owner/repo`), a file inside one
1222
- (`owner/repo/name.safetensors`), a file URL, or a local path or just drop the files below. A strength of `0`
1223
- switches a slot off without clearing it. Adapters have to be trained against the `transformer_ref/` partition.
1224
- ComfyUI-trained adapters are remapped to diffusers' module names on the fly, so no separate conversion step is needed.
 
 
 
 
 
1225
  """
1226
 
1227
  TURBO_HELP = """A few-step distillation that renders joint video + soundtrack in **4–8 steps** instead of the usual
 
349
  return "/".join(parts[:2]), "/".join(parts[4:])
350
 
351
 
352
+ CIVITAI_HOSTS = ("civitai.com", "civitai.red", "civitai.green", "civitai.work")
353
+
354
+
355
+ def _download_direct_lora(url: str) -> str:
356
+ """Fetch a `.safetensors` from a plain URL - CivitAI in particular - and return the local path.
357
+
358
+ The download happens on the Space's own machine, not in the visitor's browser, so a CivitAI
359
+ session in a browser tab has nothing to do with it: a gated model answers a server with an HTML
360
+ login page instead of weights. `CIVITAI_TOKEN` (Settings -> Variables and secrets) is appended
361
+ automatically when it is set, and the header of whatever comes back is checked so a login page
362
+ fails with a sentence that says what to do rather than a parse error deep inside safetensors.
363
+ """
364
+ import hashlib
365
+ from urllib.parse import urlparse, unquote
366
+
367
+ import requests
368
+
369
+ host = (urlparse(url).hostname or "").lower()
370
+ request_url = url
371
+ token = os.environ.get("CIVITAI_TOKEN", "").strip()
372
+ if token and any(host.endswith(known) for known in CIVITAI_HOSTS) and "token=" not in url:
373
+ request_url = url + ("&" if "?" in url else "?") + f"token={token}"
374
+
375
+ cache_dir = os.path.join(tempfile.gettempdir(), "url-loras")
376
+ os.makedirs(cache_dir, exist_ok=True)
377
+ cached = os.path.join(cache_dir, hashlib.sha256(url.encode()).hexdigest()[:16] + ".safetensors")
378
+ if os.path.exists(cached) and os.path.getsize(cached) > 1_000_000:
379
+ return cached
380
+
381
+ try:
382
+ response = requests.get(request_url, stream=True, timeout=120,
383
+ headers={"User-Agent": "Mozilla/5.0"})
384
+ response.raise_for_status()
385
+ except Exception as error:
386
+ raise gr.Error(f"Could not download `{url}`: {error}")
387
+
388
+ content_type = (response.headers.get("content-type") or "").lower()
389
+ if "text/html" in content_type:
390
+ raise gr.Error(
391
+ "That link answered with a web page instead of a file. The model is gated, so the "
392
+ "Space needs its own key: add CIVITAI_TOKEN under Settings -> Variables and secrets."
393
+ )
394
+
395
+ disposition = response.headers.get("content-disposition", "")
396
+ name = unquote(re.findall(r'filename\*?=(?:UTF-8\'\'|")?([^";]+)', disposition)[0]) \
397
+ if "filename" in disposition else os.path.basename(urlparse(url).path)
398
+ if name and not name.lower().endswith(".safetensors") and "." in name:
399
+ print(f"[lora] {name} is not a .safetensors; trying it anyway")
400
+
401
+ written = 0
402
+ with open(cached, "wb") as handle:
403
+ for chunk in response.iter_content(chunk_size=1 << 20):
404
+ if chunk:
405
+ handle.write(chunk)
406
+ written += len(chunk)
407
+
408
+ if written < 1_000_000:
409
+ os.remove(cached)
410
+ raise gr.Error(
411
+ "That link returned only a few kilobytes - almost always a login or error page rather "
412
+ "than weights. Check the link, or add CIVITAI_TOKEN to the Space."
413
+ )
414
+
415
+ # safetensors starts with an 8-byte little-endian header length followed by that much JSON.
416
+ with open(cached, "rb") as handle:
417
+ header_len = int.from_bytes(handle.read(8), "little")
418
+ if not (0 < header_len < 100_000_000):
419
+ os.remove(cached)
420
+ raise gr.Error("The downloaded file is not a `.safetensors` (bad header).")
421
+ try:
422
+ json.loads(handle.read(header_len).decode("utf-8"))
423
+ except Exception:
424
+ os.remove(cached)
425
+ raise gr.Error("The downloaded file is not a `.safetensors` (unreadable header).")
426
+
427
+ print(f"[lora] downloaded {written / 1e6:.0f} MB from {host} -> {os.path.basename(cached)}")
428
+ return cached
429
+
430
+
431
  def resolve_lora(reference: str) -> str:
432
  """Turn what the user typed into a local `.safetensors` path.
433
 
 
442
  if os.path.exists(reference):
443
  return reference
444
  if reference.startswith(("http://", "https://")):
445
+ from urllib.parse import urlparse
446
+
447
+ if (urlparse(reference).hostname or "").lower().endswith("huggingface.co"):
448
+ repo_id, filename = _hub_url_parts(reference)
449
+ return hf_hub_download(repo_id, filename)
450
+ # Anything else - CivitAI and any other direct link - is fetched as a plain file.
451
+ return _download_direct_lora(reference)
452
 
453
  parts = [part for part in reference.split("/") if part]
454
  if len(parts) > 2 and parts[-1].endswith(".safetensors"):
 
1297
  <a href="https://huggingface.co/spaces/multimodalart/minimax-h3" target="_blank" rel="noopener">text / image to video</a></p>
1298
  <div class="pills">
1299
  <span>33B</span><span>joint video + audio</span><span>Turbo lora: 4&ndash;8 steps</span>
1300
+ <span>ComfyUI lora accepted</span><span>up to 9 references</span><span>named profiles</span><span>CivitAI links</span><span>GPU cost estimate</span><span>clip stitching with audio</span>
1301
  </div>
1302
  </div>
1303
  """
1304
 
1305
  LORA_HELP = """Each slot takes a Hugging Face repo (`owner/repo`), a file inside one
1306
+ (`owner/repo/name.safetensors`), a **CivitAI download link**, any other direct `.safetensors` URL, or a local path
1307
+ or just drop the files below. A strength of `0` switches a slot off without clearing it. Adapters have to be trained
1308
+ against the `transformer_ref/` partition. ComfyUI-trained adapters are remapped to diffusers' module names on the fly,
1309
+ so no separate conversion step is needed.
1310
+
1311
+ CivitAI links are fetched by the Space itself, not by your browser, so being signed in there does not help a gated
1312
+ model — set `CIVITAI_TOKEN` under *Settings → Variables and secrets* and it is appended automatically. Downloads are
1313
+ cached, so a second run with the same link costs nothing.
1314
  """
1315
 
1316
  TURBO_HELP = """A few-step distillation that renders joint video + soundtrack in **4–8 steps** instead of the usual