observantdistressed commited on
Commit
0d1dbfc
·
verified ·
1 Parent(s): 62299e2

fix: cached LoRAs fully wired — dropdown discovers /data/loras/*.safetensors, loads direct from disk

Browse files

- _community_lora_choices() scans bucket cache dir for .safetensors not
in registry, shows them with cache: prefix
- _load_lora_from_cache() loads safetensors by path, detects format
(ComfyUI/kohya/LoKR), converts, stores in _CUSTOM_LoRA
- _download_custom_lora() detects cache: prefix, skips download
- _generate_impl() handles cache: keys for the generate pipeline
- ~100 previously-invisible cached LoRAs now searchable in dropdown

Files changed (1) hide show
  1. app.py +116 -27
app.py CHANGED
@@ -1532,6 +1532,24 @@ def _download_custom_lora(url, scale=1.0, trigger="", description="", tags=""):
1532
 
1533
  On success the LoRA is also added to the community registry so other
1534
  users can discover it via the searchable dropdown."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  try:
1536
  repo, file = _parse_hf_link(url)
1537
  if file is None:
@@ -1600,8 +1618,9 @@ def _community_lora_choices():
1600
  """Return (label, value) pairs for the community LoRA dropdown.
1601
 
1602
  Sources: the JSON registry **plus** any ``.safetensors`` files already cached
1603
- in ``/data/loras/`` that aren't in the registry yet (from previous sessions
1604
- where the download happened before the registry existed)."""
 
1605
  import glob as _glob, os
1606
  registry = _load_community_registry()
1607
  known_urls = {e.get("url", "") for e in registry}
@@ -1617,22 +1636,70 @@ def _community_lora_choices():
1617
  label += f" [{', '.join(tags[:3])}]"
1618
  choices.append((label, entry.get("url", "")))
1619
  # Auto-discover cached .safetensors files not yet in the registry.
1620
- for path in _glob.glob(os.path.join(_LORA_CACHE_DIR, "*.safetensors")):
1621
  fname = os.path.basename(path)
1622
- # Filename encoding: repo__file.safetensors (non-alnum → _)
1623
- # Try to reverse: strip .safetensors, split on __
 
 
 
1624
  stem = fname[:-len(".safetensors")]
1625
- parts = stem.split("__", 1)
1626
- if len(parts) == 2:
1627
- repo = parts[0].replace("_", "/") # best-effort reverse
1628
- file = parts[1]
1629
- url = f"https://huggingface.co/{repo}/resolve/main/{file}"
1630
- if url not in known_urls and (repo, file) not in known_keys:
1631
- name = repo.split("/")[-1]
1632
- choices.append((f"{name} (cached)", url))
1633
  return choices
1634
 
1635
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1636
  def _submit_community_lora(url, scale, trigger, description, tags):
1637
  """Add a LoRA to the community registry and return updated dropdown choices + status."""
1638
  import json
@@ -1670,9 +1737,15 @@ def _submit_community_lora(url, scale, trigger, description, tags):
1670
 
1671
 
1672
  def _load_selected_community_lora(evt: gr.SelectData):
1673
- """When a community LoRA is selected from the dropdown, return its URL / scale / trigger."""
 
 
 
1674
  registry = _load_community_registry()
1675
  url = evt.value
 
 
 
1676
  for entry in registry:
1677
  e_url = entry.get("url", "")
1678
  if e_url == url or f"https://huggingface.co/{entry.get('repo', '')}/resolve/main/{entry.get('file', '')}" == url:
@@ -2040,19 +2113,35 @@ def _generate_impl(prompt, image_path=None, last_image_path=None, canvas=DEFAULT
2040
  # User-supplied LoRA from the custom URL box: parse the link and require it was downloaded & verified.
2041
  custom = []
2042
  if custom_lora_url and str(custom_lora_url).strip():
2043
- _repo, _file = _parse_hf_link(custom_lora_url)
2044
- if _file is None:
2045
- _file = _resolve_repo_file(_repo)
2046
- if (_repo, _file) not in _CUSTOM_LoRA:
2047
- raise gr.Error(f"`{_repo}/{_file}` isn't downloaded yet — press 'Download & Verify' first.")
2048
- try:
2049
- _scale = float(custom_lora_scale) if custom_lora_scale not in (None, "") else 1.0
2050
- except (TypeError, ValueError):
2051
- raise gr.Error("Custom LoRA scale must be a number.")
2052
- if not 0.01 <= _scale <= 4.0:
2053
- raise gr.Error("Custom LoRA scale must be between 0.01 and 4.")
2054
- _trigger = str(custom_lora_trigger or "").strip()
2055
- custom = [(_repo, _file, _scale, _trigger)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2056
 
2057
  if nsfw_keys or custom:
2058
  nsfw_keys += ("nsfw_aio",)
 
1532
 
1533
  On success the LoRA is also added to the community registry so other
1534
  users can discover it via the searchable dropdown."""
1535
+ # Handle cached LoRAs — load directly from local path, skip download.
1536
+ if url.startswith("cache:"):
1537
+ cache_path = url[len("cache:"):]
1538
+ try:
1539
+ key = _load_lora_from_cache(cache_path)
1540
+ except gr.Error as _e:
1541
+ return gr.update(value=f"**❌ {_e}**", visible=True)
1542
+ except Exception as _e:
1543
+ return gr.update(value=f"**❌ Unexpected error loading cached LoRA: {_e}**", visible=True)
1544
+ n = len(_CUSTOM_LoRA[key])
1545
+ fname = os.path.basename(cache_path)
1546
+ note = f" (scale {scale:g}" + (f", trigger `{trigger}`)" if trigger else ")")
1547
+ choices = _community_lora_choices()
1548
+ _log(f"cached LoRA {fname} loaded ({n} modules)")
1549
+ return (
1550
+ gr.update(value=f"**✅ Loaded `{fname}`** — {n} modules, will apply on Generate{note}.", visible=True),
1551
+ gr.update(choices=choices, value=url),
1552
+ )
1553
  try:
1554
  repo, file = _parse_hf_link(url)
1555
  if file is None:
 
1618
  """Return (label, value) pairs for the community LoRA dropdown.
1619
 
1620
  Sources: the JSON registry **plus** any ``.safetensors`` files already cached
1621
+ in ``/data/loras/`` that aren't in the registry yet. Cached files get a
1622
+ ``cache:`` prefixed value so the download/load path can load them directly
1623
+ without going through the URL → download flow."""
1624
  import glob as _glob, os
1625
  registry = _load_community_registry()
1626
  known_urls = {e.get("url", "") for e in registry}
 
1636
  label += f" [{', '.join(tags[:3])}]"
1637
  choices.append((label, entry.get("url", "")))
1638
  # Auto-discover cached .safetensors files not yet in the registry.
1639
+ for path in sorted(_glob.glob(os.path.join(_LORA_CACHE_DIR, "*.safetensors"))):
1640
  fname = os.path.basename(path)
1641
+ # Check if this file is already represented in the registry by matching
1642
+ # any URL that contains this filename.
1643
+ if any(fname in url for url in known_urls):
1644
+ continue
1645
+ # Extract a readable name from the filename (strip .safetensors, replace _ with space)
1646
  stem = fname[:-len(".safetensors")]
1647
+ label = stem.replace("_", " ")
1648
+ choices.append((f"📦 {label}", f"cache:{path}"))
 
 
 
 
 
 
1649
  return choices
1650
 
1651
 
1652
+ def _load_lora_from_cache(path):
1653
+ """Load a cached .safetensors LoRA directly by file path. Returns (repo, file) key
1654
+ and populates ``_CUSTOM_LoRA`` so it can be merged on generate."""
1655
+ import torch
1656
+ from safetensors.torch import load_file as _load_sd
1657
+ _t0 = time.time()
1658
+ fname = os.path.basename(path)
1659
+ # Use the filename as a synthetic (repo, file) key.
1660
+ key = ("__cache__", fname)
1661
+ if key in _CUSTOM_LoRA:
1662
+ return key
1663
+ try:
1664
+ _raw = _load_sd(path)
1665
+ except Exception as _e:
1666
+ raise gr.Error(f"Couldn't load cached LoRA `{fname}`: {_e}")
1667
+ # Detect format and convert.
1668
+ _has_comfy = any(".lora_A." in _k for _k in _raw)
1669
+ _has_kohya = any(".lora_down.weight" in _k or _k.startswith("lora_unet_") for _k in _raw)
1670
+ _has_lokr = any(".lokr_w1" in _k or ".lokr_w1_a" in _k for _k in _raw)
1671
+ if _has_comfy:
1672
+ _sd = _convert_comfyui_h3_lora(_raw)
1673
+ elif _has_lokr:
1674
+ _sd = _convert_lokr_h3_lora(_raw)
1675
+ elif _has_kohya:
1676
+ _sd = _convert_kohya_h3_lora(_raw)
1677
+ else:
1678
+ raise gr.Error(f"`{fname}` doesn't look like a MiniMax-H3 LoRA.")
1679
+ _pairs: dict[str, dict[str, torch.Tensor]] = {}
1680
+ for _k, _v in _sd.items():
1681
+ if ".lora_A." in _k:
1682
+ _pairs.setdefault(_k.split(".lora_A.")[0], {})["A"] = _v
1683
+ elif ".lora_B." in _k:
1684
+ _pairs.setdefault(_k.split(".lora_B.")[0], {})["B"] = _v
1685
+ _loaded = []
1686
+ for _base, _pair in _pairs.items():
1687
+ if "A" not in _pair or "B" not in _pair:
1688
+ continue
1689
+ _parts = _base.split(".")
1690
+ _obj = PIPE.transformer
1691
+ for _p in _parts:
1692
+ _obj = _obj[int(_p)] if _p.isdigit() else getattr(_obj, _p)
1693
+ _loaded.append((_obj, _pair["A"].to(_obj.weight.dtype), _pair["B"].to(_obj.weight.dtype)))
1694
+ if not _loaded:
1695
+ raise gr.Error(f"`{fname}` converted to no usable modules.")
1696
+ del _sd, _pairs, _raw
1697
+ import gc as _gc; _gc.collect()
1698
+ _CUSTOM_LoRA[key] = _loaded
1699
+ _log(f"cached LoRA {fname} loaded ({len(_loaded)} modules in {time.time() - _t0:.1f}s)")
1700
+ return key
1701
+
1702
+
1703
  def _submit_community_lora(url, scale, trigger, description, tags):
1704
  """Add a LoRA to the community registry and return updated dropdown choices + status."""
1705
  import json
 
1737
 
1738
 
1739
  def _load_selected_community_lora(evt: gr.SelectData):
1740
+ """When a community LoRA is selected from the dropdown, return its URL / scale / trigger.
1741
+
1742
+ For cached LoRAs (value starts with ``cache:``) the URL field is set to the
1743
+ cache path so the Download & Apply button can detect it."""
1744
  registry = _load_community_registry()
1745
  url = evt.value
1746
+ # Cached LoRAs use cache:path — pass through so _download_custom_lora picks them up.
1747
+ if url.startswith("cache:"):
1748
+ return url, 1.0, ""
1749
  for entry in registry:
1750
  e_url = entry.get("url", "")
1751
  if e_url == url or f"https://huggingface.co/{entry.get('repo', '')}/resolve/main/{entry.get('file', '')}" == url:
 
2113
  # User-supplied LoRA from the custom URL box: parse the link and require it was downloaded & verified.
2114
  custom = []
2115
  if custom_lora_url and str(custom_lora_url).strip():
2116
+ _url = str(custom_lora_url).strip()
2117
+ # Handle cache: prefix — LoRA was loaded directly from bucket cache.
2118
+ if _url.startswith("cache:"):
2119
+ _cache_path = _url[len("cache:"):]
2120
+ _key = ("__cache__", os.path.basename(_cache_path))
2121
+ if _key not in _CUSTOM_LoRA:
2122
+ raise gr.Error(f"Cached LoRA isn't loaded yet — press 'Download & Verify' first.")
2123
+ try:
2124
+ _scale = float(custom_lora_scale) if custom_lora_scale not in (None, "") else 1.0
2125
+ except (TypeError, ValueError):
2126
+ raise gr.Error("Custom LoRA scale must be a number.")
2127
+ if not 0.01 <= _scale <= 4.0:
2128
+ raise gr.Error("Custom LoRA scale must be between 0.01 and 4.")
2129
+ _trigger = str(custom_lora_trigger or "").strip()
2130
+ custom = [(_key[0], _key[1], _scale, _trigger)]
2131
+ else:
2132
+ _repo, _file = _parse_hf_link(_url)
2133
+ if _file is None:
2134
+ _file = _resolve_repo_file(_repo)
2135
+ if (_repo, _file) not in _CUSTOM_LoRA:
2136
+ raise gr.Error(f"`{_repo}/{_file}` isn't downloaded yet — press 'Download & Verify' first.")
2137
+ try:
2138
+ _scale = float(custom_lora_scale) if custom_lora_scale not in (None, "") else 1.0
2139
+ except (TypeError, ValueError):
2140
+ raise gr.Error("Custom LoRA scale must be a number.")
2141
+ if not 0.01 <= _scale <= 4.0:
2142
+ raise gr.Error("Custom LoRA scale must be between 0.01 and 4.")
2143
+ _trigger = str(custom_lora_trigger or "").strip()
2144
+ custom = [(_repo, _file, _scale, _trigger)]
2145
 
2146
  if nsfw_keys or custom:
2147
  nsfw_keys += ("nsfw_aio",)