raubatz Oz commited on
Commit
12b5e99
·
1 Parent(s): 9d8cd76

Fix nested HF LoRA weight paths for custom imports

Browse files

Parse user/repo/sub/.../file.safetensors into hub repo + nested weight path; allow weight field subfolder/file and user/repo/sub + bare file.

Co-Authored-By: Oz <oz-agent@warp.dev>

Files changed (2) hide show
  1. app.py +5 -3
  2. lora_registry.py +152 -26
app.py CHANGED
@@ -912,12 +912,14 @@ with gr.Blocks() as demo:
912
  with gr.Row():
913
  lora_repo_id = gr.Textbox(
914
  label="HF repo ID or local path",
915
- placeholder="username/repo-name or /loras-flux/my.safetensors",
 
916
  )
917
  with gr.Row():
918
  lora_weight_name = gr.Textbox(
919
- label="Weight filename (optional if path is a .safetensors file)",
920
- placeholder="pytorch_lora_weights.safetensors",
 
921
  )
922
  lora_adapter_name = gr.Textbox(label="Adapter name (optional)", placeholder="my-lora")
923
  with gr.Row():
 
912
  with gr.Row():
913
  lora_repo_id = gr.Textbox(
914
  label="HF repo ID or local path",
915
+ placeholder="user/repo or user/repo/sub/model.safetensors or /loras-flux/my.safetensors",
916
+ info="Nested HF paths OK: user/repo/folder/model.safetensors",
917
  )
918
  with gr.Row():
919
  lora_weight_name = gr.Textbox(
920
+ label="Weight path inside repo (optional)",
921
+ placeholder="subfolder/model.safetensors",
922
+ info="Use for nested files if not included in the repo field.",
923
  )
924
  lora_adapter_name = gr.Textbox(label="Adapter name (optional)", placeholder="my-lora")
925
  with gr.Row():
lora_registry.py CHANGED
@@ -832,6 +832,57 @@ def _looks_like_local_path(value: str) -> bool:
832
  return bool(value) and value.startswith("/")
833
 
834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
  def _resolve_local_lora(path_str: str, weight_name: str | None):
836
  path = Path(os.path.expanduser(path_str)).resolve()
837
  requested = weight_name.strip() if weight_name and weight_name.strip() else None
@@ -839,69 +890,144 @@ def _resolve_local_lora(path_str: str, weight_name: str | None):
839
  if path.is_file():
840
  if path.suffix.lower() not in _WEIGHT_EXTS:
841
  raise ValueError(f"Not a LoRA weight file: {path.name}")
 
842
  return str(path.parent), path.name, path.stem
843
 
844
  if not path.is_dir():
845
  raise FileNotFoundError(f"Local path not found: {path}")
846
 
847
  if requested:
848
- candidate = path / requested
 
 
 
 
 
 
 
849
  if not candidate.is_file():
850
  available = sorted(
851
- p.name for p in path.iterdir()
 
852
  if p.is_file() and p.suffix.lower() in _WEIGHT_EXTS
853
- )
854
  raise FileNotFoundError(
855
- f"'{requested}' not in {path}. Available: {', '.join(available) or 'None'}"
856
  )
857
- return str(path), requested, Path(requested).stem
 
 
858
 
859
  for name in _DEFAULT_WEIGHT_CANDIDATES:
860
  if (path / name).is_file():
861
  return str(path), name, path.name
862
 
863
- available = sorted(
 
864
  p.name for p in path.iterdir()
865
  if p.is_file() and p.suffix.lower() in _WEIGHT_EXTS
866
  )
867
- if len(available) == 1:
868
- return str(path), available[0], Path(available[0]).stem
869
- if not available:
 
 
 
 
 
 
 
 
 
 
 
 
870
  raise FileNotFoundError(f"No .safetensors/.bin weights found in {path}")
871
  raise FileNotFoundError(
872
- f"Multiple weights in {path}; set Weight filename. Available: {', '.join(available)}"
873
  )
874
 
875
 
876
  def _resolve_hf_lora(repo_id: str, weight_name: str | None):
877
  from huggingface_hub import model_info
878
 
879
- info = model_info(repo_id)
880
- actual_weight = weight_name.strip() if weight_name and weight_name.strip() else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  if not actual_weight:
 
 
882
  for name in _DEFAULT_WEIGHT_CANDIDATES:
883
- if any(f.filename == name for f in info.siblings):
884
- actual_weight = name
 
885
  break
886
- if not actual_weight:
887
- available = [
888
- f.filename for f in info.siblings
889
- if f.filename.endswith(_WEIGHT_EXTS)
890
- ]
891
- raise FileNotFoundError(
892
- f"No weight found. Available: {', '.join(available) or 'None'}"
893
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
 
895
  sha = None
896
- for sib in info.siblings:
897
- if sib.filename == actual_weight:
898
  lfs = getattr(sib, "lfs", None) or {}
899
  if isinstance(lfs, dict):
900
  sha = lfs.get("sha256") or lfs.get("oid")
901
  break
902
 
903
- display = repo_id.split("/")[-1] if "/" in repo_id else repo_id
904
- return repo_id, actual_weight, display, (str(sha).lower() if sha else None)
905
 
906
 
907
  def session_custom_lora_titles(dynamic_loras) -> list[str]:
 
832
  return bool(value) and value.startswith("/")
833
 
834
 
835
+ def _is_weight_filename(name: str | None) -> bool:
836
+ if not name:
837
+ return False
838
+ lower = str(name).strip().lower()
839
+ return any(lower.endswith(ext) for ext in _WEIGHT_EXTS)
840
+
841
+
842
+ def _split_hf_repo_and_weight(repo_id: str, weight_name: str | None) -> tuple[str, str | None]:
843
+ """Split owner/repo[/nested/weight.safetensors] into hub repo id + weight path.
844
+
845
+ Supports nested weights inside the repo, e.g.:
846
+ user/repo/sub/dir/model.safetensors -> repo=user/repo, weight=sub/dir/model.safetensors
847
+ user/repo + weight=sub/dir/model.safetensors (unchanged)
848
+ """
849
+ repo = (repo_id or "").strip().strip("/")
850
+ weight = weight_name.strip() if weight_name and str(weight_name).strip() else None
851
+
852
+ # If weight already given: hub repo is owner/name; optional extra path
853
+ # segments are a subfolder prefix (user/repo/sub + file.safetensors).
854
+ if weight:
855
+ weight = weight.lstrip("/")
856
+ parts = [p for p in repo.split("/") if p]
857
+ if len(parts) >= 2:
858
+ hub = f"{parts[0]}/{parts[1]}"
859
+ extra = parts[2:]
860
+ # user/repo/subfolder + model.safetensors -> subfolder/model.safetensors
861
+ if extra and not _is_weight_filename(extra[-1]):
862
+ prefix = "/".join(extra)
863
+ if not (weight == prefix or weight.startswith(prefix + "/")):
864
+ weight = f"{prefix}/{weight}"
865
+ return hub, weight
866
+ return repo, weight
867
+
868
+ parts = [p for p in repo.split("/") if p]
869
+ if len(parts) <= 2:
870
+ return repo, None
871
+
872
+ # owner/repo/<rest...>
873
+ owner, name, *rest = parts
874
+ hub_repo = f"{owner}/{name}"
875
+ rest_path = "/".join(rest)
876
+
877
+ # user/repo/file.safetensors OR user/repo/sub/file.safetensors
878
+ if _is_weight_filename(rest[-1]):
879
+ return hub_repo, rest_path
880
+
881
+ # user/repo/subfolder (prefix inside repo; weight still unknown)
882
+ # Keep as repo + None so auto-detect can filter siblings under this prefix.
883
+ return hub_repo, None if not rest_path else f"{rest_path}/" # trailing slash = prefix marker
884
+
885
+
886
  def _resolve_local_lora(path_str: str, weight_name: str | None):
887
  path = Path(os.path.expanduser(path_str)).resolve()
888
  requested = weight_name.strip() if weight_name and weight_name.strip() else None
 
890
  if path.is_file():
891
  if path.suffix.lower() not in _WEIGHT_EXTS:
892
  raise ValueError(f"Not a LoRA weight file: {path.name}")
893
+ # Keep nested filename only for local load_lora_weights(dir, weight_name=file)
894
  return str(path.parent), path.name, path.stem
895
 
896
  if not path.is_dir():
897
  raise FileNotFoundError(f"Local path not found: {path}")
898
 
899
  if requested:
900
+ # Allow nested relative weight paths: sub/dir/model.safetensors
901
+ candidate = (path / requested).resolve()
902
+ try:
903
+ candidate.relative_to(path)
904
+ except ValueError as e:
905
+ raise FileNotFoundError(
906
+ f"Weight path escapes directory {path}: {requested}"
907
+ ) from e
908
  if not candidate.is_file():
909
  available = sorted(
910
+ str(p.relative_to(path))
911
+ for p in path.rglob("*")
912
  if p.is_file() and p.suffix.lower() in _WEIGHT_EXTS
913
+ )[:20]
914
  raise FileNotFoundError(
915
+ f"'{requested}' not under {path}. Available: {', '.join(available) or 'None'}"
916
  )
917
+ # diffusers local: repo=dir containing file tree root we pass, weights=relpath
918
+ rel = str(candidate.relative_to(path)).replace("\\", "/")
919
+ return str(path), rel, Path(rel).stem
920
 
921
  for name in _DEFAULT_WEIGHT_CANDIDATES:
922
  if (path / name).is_file():
923
  return str(path), name, path.name
924
 
925
+ # Prefer top-level weights; fall back to a single nested weight if unique.
926
+ top = sorted(
927
  p.name for p in path.iterdir()
928
  if p.is_file() and p.suffix.lower() in _WEIGHT_EXTS
929
  )
930
+ if len(top) == 1:
931
+ return str(path), top[0], Path(top[0]).stem
932
+ if top:
933
+ raise FileNotFoundError(
934
+ f"Multiple weights in {path}; set Weight filename. Available: {', '.join(top)}"
935
+ )
936
+
937
+ nested = sorted(
938
+ str(p.relative_to(path)).replace("\\", "/")
939
+ for p in path.rglob("*")
940
+ if p.is_file() and p.suffix.lower() in _WEIGHT_EXTS
941
+ )
942
+ if len(nested) == 1:
943
+ return str(path), nested[0], Path(nested[0]).stem
944
+ if not nested:
945
  raise FileNotFoundError(f"No .safetensors/.bin weights found in {path}")
946
  raise FileNotFoundError(
947
+ f"Multiple nested weights in {path}; set Weight path. Available: {', '.join(nested[:20])}"
948
  )
949
 
950
 
951
  def _resolve_hf_lora(repo_id: str, weight_name: str | None):
952
  from huggingface_hub import model_info
953
 
954
+ hub_repo, weight_or_prefix = _split_hf_repo_and_weight(repo_id, weight_name)
955
+ # Trailing slash marks "directory prefix inside repo" from user/repo/subfolder
956
+ prefix = None
957
+ actual_weight = weight_or_prefix
958
+ if actual_weight and actual_weight.endswith("/") and not _is_weight_filename(actual_weight):
959
+ prefix = actual_weight.lstrip("/")
960
+ actual_weight = None
961
+ elif actual_weight:
962
+ actual_weight = actual_weight.lstrip("/")
963
+
964
+ info = model_info(hub_repo)
965
+ siblings = list(info.siblings or [])
966
+
967
+ def _weight_siblings(pref: str | None = None):
968
+ out = []
969
+ for f in siblings:
970
+ name = getattr(f, "filename", None) or ""
971
+ if not name.endswith(_WEIGHT_EXTS):
972
+ continue
973
+ if pref and not name.startswith(pref):
974
+ continue
975
+ out.append(name)
976
+ return out
977
+
978
  if not actual_weight:
979
+ # Auto-pick under optional subfolder prefix.
980
+ search_prefix = prefix or ""
981
  for name in _DEFAULT_WEIGHT_CANDIDATES:
982
+ candidate = f"{search_prefix}{name}" if search_prefix else name
983
+ if any(getattr(f, "filename", None) == candidate for f in siblings):
984
+ actual_weight = candidate
985
  break
986
+ if not actual_weight:
987
+ available = _weight_siblings(search_prefix or None)
988
+ # If prefix was a folder and defaults missing, unique weight under prefix
989
+ if len(available) == 1:
990
+ actual_weight = available[0]
991
+ elif not available and not search_prefix:
992
+ available = _weight_siblings(None)
993
+ if len(available) == 1:
994
+ actual_weight = available[0]
995
+ if not actual_weight:
996
+ shown = available[:30] if available else _weight_siblings(None)[:30]
997
+ where = f" under '{search_prefix.rstrip('/')}'" if search_prefix else ""
998
+ raise FileNotFoundError(
999
+ f"No weight found in {hub_repo}{where}. "
1000
+ f"Available: {', '.join(shown) or 'None'}"
1001
+ )
1002
+
1003
+ # Validate nested path exists in repo file list when possible
1004
+ sibling_names = {getattr(f, "filename", None) for f in siblings}
1005
+ if actual_weight not in sibling_names:
1006
+ # allow if list incomplete; still try exact match after strip
1007
+ alt = actual_weight.lstrip("./")
1008
+ if alt in sibling_names:
1009
+ actual_weight = alt
1010
+ else:
1011
+ available = _weight_siblings(None)
1012
+ # helpful: show nested matches by basename
1013
+ base = Path(actual_weight).name
1014
+ nested_hits = [a for a in available if a == actual_weight or a.endswith("/" + base)]
1015
+ hint = nested_hits[:10] if nested_hits else available[:20]
1016
+ raise FileNotFoundError(
1017
+ f"Weight '{actual_weight}' not in {hub_repo}. "
1018
+ f"Try nested path like 'subfolder/{base}'. Available: {', '.join(hint) or 'None'}"
1019
+ )
1020
 
1021
  sha = None
1022
+ for sib in siblings:
1023
+ if getattr(sib, "filename", None) == actual_weight:
1024
  lfs = getattr(sib, "lfs", None) or {}
1025
  if isinstance(lfs, dict):
1026
  sha = lfs.get("sha256") or lfs.get("oid")
1027
  break
1028
 
1029
+ display = Path(actual_weight).stem if actual_weight else hub_repo.split("/")[-1]
1030
+ return hub_repo, actual_weight, display, (str(sha).lower() if sha else None)
1031
 
1032
 
1033
  def session_custom_lora_titles(dynamic_loras) -> list[str]: