Spaces:
Running on Zero
Running on Zero
Upload app.py
Browse files
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 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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–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
|
| 1223 |
-
switches a slot off without clearing it. Adapters have to be trained
|
| 1224 |
-
ComfyUI-trained adapters are remapped to diffusers' module names on the fly,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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–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
|