Spaces:
Running on Zero
Running on Zero
Rework custom LoRA to a single URL box + Download & Verify button: paste an HF repo link or direct .safetensors link, download/verify/load on button press, apply on Generate
Browse files
app.py
CHANGED
|
@@ -1137,6 +1137,67 @@ def _ensure_custom_lora_loaded(repo, file):
|
|
| 1137 |
print(f"[gen] custom LoRA {repo}/{file} parsed ({len(_loaded)} modules)", flush=True)
|
| 1138 |
|
| 1139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
def _apply_lora(key, scale, store, merged):
|
| 1141 |
"""Merge a loaded LoRA into transformer weights on GPU. Lazy CUDA transfer of A/B factors."""
|
| 1142 |
if key in merged or key not in store:
|
|
@@ -1409,9 +1470,9 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
|
|
| 1409 |
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
|
| 1410 |
|
| 1411 |
|
| 1412 |
-
def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=25, seed=-1, upsample=False, turbo=False, nsfw_lora_keys=(),
|
| 1413 |
"""One request. `upsample` is last among the pre-existing positional args, so a positional API client that
|
| 1414 |
-
predates it is unaffected;
|
| 1415 |
if LOAD_ERROR:
|
| 1416 |
raise gr.Error(LOAD_ERROR)
|
| 1417 |
if PIPE is None:
|
|
@@ -1431,27 +1492,22 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
|
|
| 1431 |
# LoRAs the user selected via the catalog dropdown; the AIO motion adapter is auto-loaded whenever any is on.
|
| 1432 |
nsfw_keys = tuple(k for k in (nsfw_lora_keys or ()) if k in NSFW_LORA_CONFIGS)
|
| 1433 |
|
| 1434 |
-
# User-supplied
|
| 1435 |
custom = []
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
continue # fully empty row
|
| 1443 |
-
if not _repo or not _file:
|
| 1444 |
-
raise gr.Error("Each custom LoRA row needs both an HF repo and a filename.")
|
| 1445 |
try:
|
| 1446 |
-
_scale = float(
|
| 1447 |
except (TypeError, ValueError):
|
| 1448 |
-
raise gr.Error(
|
| 1449 |
if not 0.01 <= _scale <= 4.0:
|
| 1450 |
-
raise gr.Error(
|
| 1451 |
-
_trigger = str(
|
| 1452 |
-
custom
|
| 1453 |
-
_seen = set()
|
| 1454 |
-
custom = [c for c in custom if not (c[0], c[1]) in _seen and not _seen.add((c[0], c[1]))]
|
| 1455 |
|
| 1456 |
if nsfw_keys or custom:
|
| 1457 |
nsfw_keys += ("nsfw_aio",)
|
|
@@ -1461,7 +1517,6 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
|
|
| 1461 |
# The trigger token must be in the prompt text for the conditioner to encode the LoRA's concept.
|
| 1462 |
prompt = f"{prompt}, {NSFW_LORA_CONFIGS[_key]['trigger']}"
|
| 1463 |
for _repo, _file, _scale, _trigger in custom:
|
| 1464 |
-
_ensure_custom_lora_loaded(_repo, _file)
|
| 1465 |
if _trigger:
|
| 1466 |
prompt = f"{prompt}, {_trigger}"
|
| 1467 |
|
|
@@ -1860,20 +1915,20 @@ with gr.Blocks(title="minimax h3 turbo + lora's") as demo:
|
|
| 1860 |
info="Searchable — type to filter. Selected LoRAs show as removable tags; their trigger tokens are auto-appended to the prompt. The HMNSFW AIO motion adapter auto-loads whenever any LoRA is selected.",
|
| 1861 |
)
|
| 1862 |
lora_desc = gr.Markdown(visible=False)
|
| 1863 |
-
with gr.Accordion("Custom
|
| 1864 |
-
gr.Markdown("
|
| 1865 |
-
"
|
| 1866 |
-
"`Blowjob v3.0 - MinimaxH3 - bl0w_j0b.safetensors`. Scale 0.5–1.5 is typical; "
|
| 1867 |
-
"trigger is optional and auto-appended to the prompt if given. "
|
| 1868 |
"ComfyUI and kohya formats are auto-detected.")
|
| 1869 |
-
|
| 1870 |
-
|
| 1871 |
-
|
| 1872 |
-
value=[["", "", 1.0, ""]],
|
| 1873 |
-
type="array",
|
| 1874 |
-
interactive=True,
|
| 1875 |
-
row_count=(1, "dynamic"),
|
| 1876 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1877 |
with gr.Row():
|
| 1878 |
image = gr.Image(label="First frame (optional)", type="filepath")
|
| 1879 |
last_image = gr.Image(label="Last frame (optional)", type="filepath")
|
|
@@ -1900,11 +1955,16 @@ with gr.Blocks(title="minimax h3 turbo + lora's") as demo:
|
|
| 1900 |
|
| 1901 |
run.click(
|
| 1902 |
generate,
|
| 1903 |
-
[prompt, image, last_image, canvas, duration, steps, seed, upsample, turbo, nsfw_lora_keys,
|
|
|
|
| 1904 |
[video, report, upsampled, upsampled_panel, download_btn, lora_label],
|
| 1905 |
api_name="generate",
|
| 1906 |
)
|
| 1907 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1908 |
nsfw_lora_keys.change(_lora_descriptions, [nsfw_lora_keys], [lora_desc])
|
| 1909 |
|
| 1910 |
last_frame_btn.click(
|
|
|
|
| 1137 |
print(f"[gen] custom LoRA {repo}/{file} parsed ({len(_loaded)} modules)", flush=True)
|
| 1138 |
|
| 1139 |
|
| 1140 |
+
def _parse_hf_link(url):
|
| 1141 |
+
"""Parse a Hugging Face repo or file link into (repo, file). `file` is None for a bare repo link."""
|
| 1142 |
+
url = (url or "").strip().split("?")[0].split("#")[0]
|
| 1143 |
+
if not url:
|
| 1144 |
+
raise gr.Error("Paste a Hugging Face link first.")
|
| 1145 |
+
if not url.startswith("http"):
|
| 1146 |
+
parts = [p for p in url.split("/") if p]
|
| 1147 |
+
if len(parts) >= 2:
|
| 1148 |
+
return parts[0] + "/" + parts[1], "/".join(parts[2:]) or None
|
| 1149 |
+
raise gr.Error(f"Couldn't parse `{url}` as a Hugging Face repo link.")
|
| 1150 |
+
if "huggingface.co" not in url:
|
| 1151 |
+
raise gr.Error("Only Hugging Face links are supported (huggingface.co).")
|
| 1152 |
+
parts = [p for p in url.split("huggingface.co", 1)[1].strip("/").split("/") if p]
|
| 1153 |
+
if len(parts) < 2:
|
| 1154 |
+
raise gr.Error(f"Couldn't parse `{url}` as a Hugging Face repo link.")
|
| 1155 |
+
repo = parts[0] + "/" + parts[1]
|
| 1156 |
+
if len(parts) == 2:
|
| 1157 |
+
return repo, None
|
| 1158 |
+
if parts[2] in ("resolve", "tree", "blob"):
|
| 1159 |
+
if len(parts) < 5:
|
| 1160 |
+
raise gr.Error(f"Couldn't parse a filename from `{url}`.")
|
| 1161 |
+
if parts[3] != "main":
|
| 1162 |
+
raise gr.Error("Only `main`-branch links are supported — use a `resolve/main/...` link.")
|
| 1163 |
+
return repo, "/".join(parts[4:])
|
| 1164 |
+
return repo, "/".join(parts[2:])
|
| 1165 |
+
|
| 1166 |
+
|
| 1167 |
+
def _resolve_repo_file(repo):
|
| 1168 |
+
"""Pick the single .safetensors file of a repo link; error if there isn't exactly one."""
|
| 1169 |
+
from huggingface_hub import list_repo_files
|
| 1170 |
+
try:
|
| 1171 |
+
files = [f for f in list_repo_files(repo) if f.endswith(".safetensors")]
|
| 1172 |
+
except Exception as _e:
|
| 1173 |
+
raise gr.Error(f"Couldn't list files in `{repo}`: {_e}")
|
| 1174 |
+
if len(files) == 1:
|
| 1175 |
+
return files[0]
|
| 1176 |
+
if not files:
|
| 1177 |
+
raise gr.Error(f"No .safetensors files found in `{repo}`.")
|
| 1178 |
+
raise gr.Error(f"`{repo}` has multiple .safetensors files — paste a direct file link instead: "
|
| 1179 |
+
+ ", ".join(files[:5]) + ("…" if len(files) > 5 else ""))
|
| 1180 |
+
|
| 1181 |
+
|
| 1182 |
+
def _download_custom_lora(url, scale=1.0, trigger=""):
|
| 1183 |
+
"""Download + verify a user-supplied Hugging Face LoRA and report status. Loads it for the next Generate."""
|
| 1184 |
+
try:
|
| 1185 |
+
repo, file = _parse_hf_link(url)
|
| 1186 |
+
if file is None:
|
| 1187 |
+
file = _resolve_repo_file(repo)
|
| 1188 |
+
_ensure_custom_lora_loaded(repo, file)
|
| 1189 |
+
except gr.Error as _e:
|
| 1190 |
+
return gr.update(value=f"**❌ {_e}**", visible=True)
|
| 1191 |
+
except Exception as _e:
|
| 1192 |
+
return gr.update(value=f"**❌ Unexpected error loading the LoRA: {_e}**", visible=True)
|
| 1193 |
+
n = len(_CUSTOM_LoRA[(repo, file)])
|
| 1194 |
+
note = f" (scale {scale:g}" + (f", trigger `{trigger}`)" if trigger else ")")
|
| 1195 |
+
return gr.update(
|
| 1196 |
+
value=f"**✅ Loaded `{repo}/{file}`** — {n} modules, will apply on Generate{note}.",
|
| 1197 |
+
visible=True,
|
| 1198 |
+
)
|
| 1199 |
+
|
| 1200 |
+
|
| 1201 |
def _apply_lora(key, scale, store, merged):
|
| 1202 |
"""Merge a loaded LoRA into transformer weights on GPU. Lazy CUDA transfer of A/B factors."""
|
| 1203 |
if key in merged or key not in store:
|
|
|
|
| 1470 |
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
|
| 1471 |
|
| 1472 |
|
| 1473 |
+
def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=25, seed=-1, upsample=False, turbo=False, nsfw_lora_keys=(), custom_lora_url="", custom_lora_scale=1.0, custom_lora_trigger="", progress=gr.Progress(track_tqdm=True)):
|
| 1474 |
"""One request. `upsample` is last among the pre-existing positional args, so a positional API client that
|
| 1475 |
+
predates it is unaffected; the custom-LoRA args come after it."""
|
| 1476 |
if LOAD_ERROR:
|
| 1477 |
raise gr.Error(LOAD_ERROR)
|
| 1478 |
if PIPE is None:
|
|
|
|
| 1492 |
# LoRAs the user selected via the catalog dropdown; the AIO motion adapter is auto-loaded whenever any is on.
|
| 1493 |
nsfw_keys = tuple(k for k in (nsfw_lora_keys or ()) if k in NSFW_LORA_CONFIGS)
|
| 1494 |
|
| 1495 |
+
# User-supplied LoRA from the custom URL box: parse the link and require it was downloaded & verified.
|
| 1496 |
custom = []
|
| 1497 |
+
if custom_lora_url and str(custom_lora_url).strip():
|
| 1498 |
+
_repo, _file = _parse_hf_link(custom_lora_url)
|
| 1499 |
+
if _file is None:
|
| 1500 |
+
_file = _resolve_repo_file(_repo)
|
| 1501 |
+
if (_repo, _file) not in _CUSTOM_LoRA:
|
| 1502 |
+
raise gr.Error(f"`{_repo}/{_file}` isn't downloaded yet — press 'Download & Verify' first.")
|
|
|
|
|
|
|
|
|
|
| 1503 |
try:
|
| 1504 |
+
_scale = float(custom_lora_scale) if custom_lora_scale not in (None, "") else 1.0
|
| 1505 |
except (TypeError, ValueError):
|
| 1506 |
+
raise gr.Error("Custom LoRA scale must be a number.")
|
| 1507 |
if not 0.01 <= _scale <= 4.0:
|
| 1508 |
+
raise gr.Error("Custom LoRA scale must be between 0.01 and 4.")
|
| 1509 |
+
_trigger = str(custom_lora_trigger or "").strip()
|
| 1510 |
+
custom = [(_repo, _file, _scale, _trigger)]
|
|
|
|
|
|
|
| 1511 |
|
| 1512 |
if nsfw_keys or custom:
|
| 1513 |
nsfw_keys += ("nsfw_aio",)
|
|
|
|
| 1517 |
# The trigger token must be in the prompt text for the conditioner to encode the LoRA's concept.
|
| 1518 |
prompt = f"{prompt}, {NSFW_LORA_CONFIGS[_key]['trigger']}"
|
| 1519 |
for _repo, _file, _scale, _trigger in custom:
|
|
|
|
| 1520 |
if _trigger:
|
| 1521 |
prompt = f"{prompt}, {_trigger}"
|
| 1522 |
|
|
|
|
| 1915 |
info="Searchable — type to filter. Selected LoRAs show as removable tags; their trigger tokens are auto-appended to the prompt. The HMNSFW AIO motion adapter auto-loads whenever any LoRA is selected.",
|
| 1916 |
)
|
| 1917 |
lora_desc = gr.Markdown(visible=False)
|
| 1918 |
+
with gr.Accordion("Custom LoRA (Hugging Face)", open=False):
|
| 1919 |
+
gr.Markdown("Paste a Hugging Face link — a repo page or a direct `.safetensors` file link — then press "
|
| 1920 |
+
"**Download & Verify**. The verified LoRA is applied on Generate. "
|
|
|
|
|
|
|
| 1921 |
"ComfyUI and kohya formats are auto-detected.")
|
| 1922 |
+
custom_lora_url = gr.Textbox(
|
| 1923 |
+
label="HF repo or .safetensors link",
|
| 1924 |
+
placeholder="https://huggingface.co/Owner/Repo or https://huggingface.co/Owner/Repo/resolve/main/file.safetensors",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1925 |
)
|
| 1926 |
+
with gr.Row():
|
| 1927 |
+
custom_lora_scale = gr.Number(label="Scale", value=1.0, minimum=0.01, maximum=4.0, step=0.1)
|
| 1928 |
+
custom_lora_trigger = gr.Textbox(label="Trigger (optional)",
|
| 1929 |
+
placeholder="Auto-appended to the prompt if given")
|
| 1930 |
+
custom_lora_btn = gr.Button("Download & Verify", size="sm")
|
| 1931 |
+
custom_lora_status = gr.Markdown(visible=False)
|
| 1932 |
with gr.Row():
|
| 1933 |
image = gr.Image(label="First frame (optional)", type="filepath")
|
| 1934 |
last_image = gr.Image(label="Last frame (optional)", type="filepath")
|
|
|
|
| 1955 |
|
| 1956 |
run.click(
|
| 1957 |
generate,
|
| 1958 |
+
[prompt, image, last_image, canvas, duration, steps, seed, upsample, turbo, nsfw_lora_keys,
|
| 1959 |
+
custom_lora_url, custom_lora_scale, custom_lora_trigger],
|
| 1960 |
[video, report, upsampled, upsampled_panel, download_btn, lora_label],
|
| 1961 |
api_name="generate",
|
| 1962 |
)
|
| 1963 |
|
| 1964 |
+
custom_lora_btn.click(_download_custom_lora,
|
| 1965 |
+
[custom_lora_url, custom_lora_scale, custom_lora_trigger],
|
| 1966 |
+
[custom_lora_status])
|
| 1967 |
+
|
| 1968 |
nsfw_lora_keys.change(_lora_descriptions, [nsfw_lora_keys], [lora_desc])
|
| 1969 |
|
| 1970 |
last_frame_btn.click(
|