observantdistressed commited on
Commit
cded1e5
·
verified ·
1 Parent(s): f7f09ad

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
Files changed (1) hide show
  1. app.py +93 -33
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=(), custom_loras=(), progress=gr.Progress(track_tqdm=True)):
1413
  """One request. `upsample` is last among the pre-existing positional args, so a positional API client that
1414
- predates it is unaffected; `custom_loras` comes after it."""
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 LoRAs from the custom table: rows are [repo, file, scale, trigger].
1435
  custom = []
1436
- for _row in (custom_loras or ()):
1437
- if not isinstance(_row, (list, tuple)) or not _row:
1438
- continue
1439
- _repo = str(_row[0] or "").strip()
1440
- _file = str(_row[1] or "").strip()
1441
- if not _repo and not _file:
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(_row[2]) if _row[2] not in (None, "") else 1.0
1447
  except (TypeError, ValueError):
1448
- raise gr.Error(f"Custom LoRA scale for `{_repo}/{_file}` must be a number.")
1449
  if not 0.01 <= _scale <= 4.0:
1450
- raise gr.Error(f"Custom LoRA scale for `{_repo}/{_file}` must be between 0.01 and 4.")
1451
- _trigger = str(_row[3] or "").strip() if len(_row) > 3 else ""
1452
- custom.append((_repo, _file, _scale, _trigger))
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 LoRAs (Hugging Face)", open=False):
1864
- gr.Markdown("Load your own MiniMax-H3 LoRA from any public Hugging Face repo — one row per LoRA. "
1865
- "Repo like `EllaPriest45/MinimaxH3_Actions`, file like "
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
- custom_lora_df = gr.Dataframe(
1870
- headers=["HF repo", "File", "Scale", "Trigger (optional)"],
1871
- datatype=["str", "str", "number", "str"],
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, custom_lora_df],
 
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(