Download custom_nodes/dolphin_nodes/lora_profile.py from bjooo/tutorials: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/lora_profile.py
- Command line
-
hf download hf://bjooo/tutorials/custom_nodes/dolphin_nodes/lora_profile.py
-
curl -L -o lora_profile.py https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/lora_profile.py
11.8 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| ๋ก๋ผ ํ์ผ ํ๋๋ฅผ "LLM์๊ฒ ์ค๋ช ํ ์ ์๋ ํ๋กํ"๋ก ๋ณํํ๋ค. | |
| ๊ธฐ์กด `get_triggers()`(dolphin_vision_director.py)๋ ํธ๋ฆฌ๊ฑฐ '๋จ์ด'๋ง ๋ฝ์์ | |
| ์บ๋ฆญํฐ ์ด๋ฆ ์์ ์ฝค๋ง๋ก ๋์ดํ๋ ์ฉ๋์๋ค. ๊ทธ ๋ฐฉ์์ ๋ฌธ๋ฒ์ ๋งฅ๋ฝ์ด ์์ด์ | |
| `bounce`/`swing` ๊ฐ์ ๋ฒ์ฉ ๋์ ๋์ฌ๊ฐ ์ ์ ๋ชจ์ ์ผ๋ก ์๋ ๋ฌธ์ ๊ฐ ์์๊ณ , | |
| ๋์์ผ๋ก ๊ทธ๋ฐ ๋จ์ด๋ฅผ ์์ ์ ์ธํ๋ ํํฐ(_filter_generic_motion_verbs)๋ฅผ | |
| ๋ฃ์ด๋ ์ํ๋ค - ๋ก๋ผ๊ฐ ์๋ํ ํจ๊ณผ๊น์ง ๊ฐ์ด ์ฌ๋ผ์ง๋ ์ฐํ์ฑ ์ด๋ค. | |
| ์ด ๋ชจ๋์ ๋ค๋ฅธ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ๋ค: ํธ๋ฆฌ๊ฑฐ๋ฅผ '๋์ค์ ๋ง๋ถ์ผ ํ ํฐ'์ด ์๋๋ผ | |
| 'ํ๋กฌํํธ๋ฅผ ์ฐ๊ธฐ ์ ์ LLM์๊ฒ ์ค ์ฌ๋ฃ'๋ก ๋ณธ๋ค. LoRA Manager๊ฐ ํ์ผ ์์ | |
| ๋ง๋ค์ด๋๋ `<์ด๋ฆ>.metadata.json` ์ฌ์ด๋์นด์ ์ด๋ฏธ ๋ค์์ด ๋ค ๋ค์ด์๋ค. | |
| - model_name / civitai.model.name : ์ด ๋ก๋ผ์ ์ด๋ฆ | |
| - modelDescription : ์์ฐ์ด ์ค๋ช (HTML) | |
| - tags / civitai.model.tags : ๋ถ๋ฅ ํ๊ทธ | |
| - civitai.trainedWords : ํ์ต ํธ๋ฆฌ๊ฑฐ. ๋จ์ด์ธ ๊ฒฝ์ฐ๋ ์์ง๋ง | |
| ์๋น์๋ ํ์ต ์บก์ ๋ฌธ์ฅ ๊ทธ๋๋ก๋ค | |
| (์: "She pulls down her pants revealing a penis.") | |
| - base_model : Wan 2.1/2.2, i2v/t2v ๊ตฌ๋ถ | |
| ๋ก๋ผ๋ ํ์ต ์บก์ ๋ถํฌ์ ๋ฎ์ ํ๋กฌํํธ์์ ๊ฐ์ฅ ์ ๋ฐํํ๋ฏ๋ก, ์ด ์ฌ๋ฃ๋ฅผ | |
| LLM์ ๋จผ์ ์ฃผ๋ฉด ํธ๋ฆฌ๊ฑฐ๋ฅผ ๋ฌธ์ฅ ์์ ๋ฌธ๋ฒ์ ์ผ๋ก ๋ น์ธ ํ๋กฌํํธ๋ฅผ ์ธ ์ ์๋ค. | |
| trainedWords 3๋ถ๋ฅ (`classify_trained_words` ์ฐธ๊ณ ): | |
| hard : ๊ณต๋ฐฑ ์๊ณ ์ซ์๋ ๋๋ฌธ์๊ฐ ์์ธ ํ์ต ์๋ณ์. LLM์ด "๊ณ ์ณ์ฃผ๋ฉด" | |
| ๋ก๋ผ๊ฐ ์ ๊ฑธ๋ฆฌ๋ฏ๋ก ์๋ฌธ ๊ทธ๋๋ก ๋์์ผ ํ๋ค (์: `4n4lfi5tin6`, | |
| `PENISLORA`). ์ถ๋ ฅ ๊ฒ์ฆ ๋์. | |
| soft : ๊ณต๋ฐฑ ์๋ ํ๋ฒํ ์๋ฌธ์ ์๋จ์ด (์: `bounce`, `penis`). | |
| ๊ฐ์ ํ์ง ์๋๋ค - ๊ฐ์ ์ฝ์ ์ด ๋ฐ๋ก ์์ ๋ฒ๊ทธ์ ์์ธ์ด์๋ค. | |
| ๋ฌธ๋งฅ์ ๋ง์ ๋ ๋ชธ ๋ถ์์ ๋ถ์ฌ ์ฐ๋ผ๊ณ '๊ถ๊ณ '๋ง ํ๋ค. | |
| caption: ๊ณต๋ฐฑ์ด ์๋ ๊ตฌ/๋ฌธ์ฅ. ํ์ต ์บก์ ์์๋ก๋ง ์ค๋ค(๋ฌธ์ฒด ์ฐธ๊ณ ์ฉ). | |
| """ | |
| import json | |
| import os | |
| import re | |
| _HTML_TAG_RE = re.compile(r"<[^>]+>") | |
| _WS_RE = re.compile(r"\s+") | |
| # ์ธ์ ๋ด ํ๋กํ ์บ์. ํค๋ ๋ก๋ผ ํ์ผ์ (๊ฒฝ๋ก, size, mtime). | |
| _PROFILE_CACHE = {} | |
| # LLM ์ปจํ ์คํธ๋ฅผ ์ก์๋จน์ง ์๋๋ก ์ค๋ช ์ ์๋ผ์ ์ค๋ค. ๋ก๋ผ ์ค๋ช ๋ฌธ์ ๋ณดํต | |
| # ์๋ถ๋ถ์ "๋ฌด์์ ํ๋ ๋ก๋ผ์ธ์ง"๊ฐ ์๊ณ ๋ค๋ ํ์ต ์ค์ /ํ์ ๋งํฌ๋ค. | |
| MAX_DESC_CHARS = 400 | |
| MAX_CAPTION_EXAMPLES = 2 | |
| def _strip_html(s): | |
| if not s: | |
| return "" | |
| s = re.sub(r"<br\s*/?>", " ", s, flags=re.IGNORECASE) | |
| s = re.sub(r"</p>", " ", s, flags=re.IGNORECASE) | |
| s = _HTML_TAG_RE.sub("", s) | |
| s = (s.replace(" ", " ").replace("&", "&") | |
| .replace("<", "<").replace(">", ">").replace(""", '"')) | |
| return _WS_RE.sub(" ", s).strip() | |
| def _truncate(s, limit): | |
| if not s or len(s) <= limit: | |
| return s | |
| cut = s[:limit] | |
| # ๋ฌธ์ฅ ์ค๊ฐ์์ ์๋ฅด์ง ์๋๋ก ๋ง์ง๋ง ๋ฌธ์ฅ๋ถํธ๊น์ง๋ง. | |
| for sep in (". ", "! ", "? "): | |
| idx = cut.rfind(sep) | |
| if idx > limit * 0.5: | |
| return cut[:idx + 1] | |
| return cut.rstrip() + "..." | |
| def is_hard_token(word): | |
| """LLM์ด ์ฒ ์๋ฅผ ๋ฐ๊พธ๋ฉด ์ ๋๋ ํ์ต ์๋ณ์์ธ๊ฐ. | |
| ๊ณต๋ฐฑ์ด ์๊ณ (=๋จ์ผ ํ ํฐ) ์ซ์๋ ๋๋ฌธ์๊ฐ ์์ฌ์์ผ๋ฉด ํ์ต ์๋ณ์๋ก ๋ณธ๋ค. | |
| `4n4lfi5tin6`, `PENISLORA`, `az420` -> True | |
| `bounce`, `penis`, `swing` -> False (ํ๋ฒํ ์๋ฌธ์ ์๋จ์ด) | |
| """ | |
| w = word.strip() | |
| if not w or _WS_RE.search(w): | |
| return False | |
| return any(c.isdigit() for c in w) or any(c.isupper() for c in w) | |
| def classify_trained_words(words): | |
| """trainedWords ๋ฆฌ์คํธ๋ฅผ (hard, soft, captions)๋ก ๋๋๋ค.""" | |
| hard, soft, captions = [], [], [] | |
| for raw in words or []: | |
| w = str(raw).strip().strip(",") | |
| if not w: | |
| continue | |
| if _WS_RE.search(w): | |
| captions.append(w) | |
| elif is_hard_token(w): | |
| hard.append(w) | |
| else: | |
| soft.append(w) | |
| # ์์ ์ ์ง ์ค๋ณต ์ ๊ฑฐ | |
| return (list(dict.fromkeys(hard)), list(dict.fromkeys(soft)), | |
| list(dict.fromkeys(captions))) | |
| def _sidecar_path(lora_path): | |
| """LoRA Manager๊ฐ ๋ง๋๋ `<ํ์ฅ์ ๋ ์ด๋ฆ>.metadata.json`.""" | |
| return os.path.splitext(lora_path)[0] + ".metadata.json" | |
| def _read_sidecar(lora_path): | |
| p = _sidecar_path(lora_path) | |
| if not os.path.exists(p): | |
| return None | |
| try: | |
| with open(p, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except (OSError, ValueError, json.JSONDecodeError) as e: | |
| print(f"โ ๏ธ [LoraProfile] ์ฌ์ด๋์นด ํ์ฑ ์คํจ({os.path.basename(p)}): " | |
| f"{type(e).__name__}: {e}") | |
| return None | |
| def _resolve_lora_path(lora_name): | |
| try: | |
| import folder_paths | |
| return folder_paths.get_full_path("loras", lora_name) | |
| except Exception: | |
| return lora_name if os.path.exists(lora_name) else None | |
| def build_profile_from_sidecar(data, lora_name, lora_path): | |
| """์ฌ์ด๋์นด dict -> ํ๋กํ dict. ์ฌ์ด๋์นด๊ฐ ์์ผ๋ฉด ํธ์ถํ์ง ์๋๋ค.""" | |
| civitai = data.get("civitai") or {} | |
| model = civitai.get("model") or {} | |
| name = (data.get("model_name") or model.get("name") | |
| or os.path.splitext(os.path.basename(lora_name))[0]) | |
| desc = _strip_html(data.get("modelDescription") or model.get("description") or "") | |
| tags = data.get("tags") or model.get("tags") or [] | |
| if not isinstance(tags, list): | |
| tags = [] | |
| trained = civitai.get("trainedWords") or [] | |
| if not isinstance(trained, list): | |
| trained = [trained] | |
| hard, soft, captions = classify_trained_words(trained) | |
| return { | |
| "lora_name": lora_name, | |
| "file": os.path.basename(lora_name), | |
| "display_name": str(name).strip(), | |
| "description": _truncate(desc, MAX_DESC_CHARS), | |
| "tags": [str(t).strip() for t in tags if str(t).strip()], | |
| "base_model": str(data.get("base_model") or civitai.get("baseModel") or "").strip(), | |
| "hard_tokens": hard, | |
| "soft_words": soft, | |
| "captions": captions[:MAX_CAPTION_EXAMPLES], | |
| "source": "sidecar", | |
| } | |
| def load_profile(lora_name, fallback_triggers=None): | |
| """๋ก๋ผ ์ด๋ฆ(ComfyUI ์๋๊ฒฝ๋ก) -> ํ๋กํ dict. ์คํจ ์ None. | |
| ์ฌ์ด๋์นด๊ฐ ์์ผ๋ฉด `fallback_triggers`(๊ธฐ์กด get_triggers ๊ฒฝ๋ก์ ๊ฒฐ๊ณผ)๋ก | |
| ์ต์ ํ๋กํ์ด๋ผ๋ ๋ง๋ ๋ค - ๊ฐ์ธ ํ์ต ๋ก๋ผ์ฒ๋ผ civitai์ ์๋ ๊ฒฝ์ฐ. | |
| `fallback_triggers`๋ ๋ฆฌ์คํธ ๋๋ "ํธ์ถํ๋ฉด ๋ฆฌ์คํธ๋ฅผ ์ฃผ๋ ํจ์"๋ฅผ ๋ฐ๋๋ค. | |
| ๊ธฐ์กด ์กฐํ ๊ฒฝ๋ก๋ sha256 ํด์(์ GB ํ์ผ) + ๋คํธ์ํฌ ํธ์ถ๊น์ง ํ ์ ์์ด์, | |
| ์ฌ์ด๋์นด๊ฐ ์๋ 99.5%์ ๊ฒฝ์ฐ์ ์์ ํธ์ถ๋์ง ์๋๋ก ์ง์ฐ ํ๊ฐํ๋ค. | |
| """ | |
| if not lora_name or lora_name == "None": | |
| return None | |
| path = _resolve_lora_path(lora_name) | |
| cache_key = lora_name | |
| if path and os.path.exists(path): | |
| try: | |
| st = os.stat(path) | |
| cache_key = f"{lora_name}::{st.st_size}::{int(st.st_mtime)}" | |
| except OSError: | |
| pass | |
| if cache_key in _PROFILE_CACHE: | |
| return _PROFILE_CACHE[cache_key] | |
| profile = None | |
| if path: | |
| data = _read_sidecar(path) | |
| if data: | |
| profile = build_profile_from_sidecar(data, lora_name, path) | |
| if profile is None: | |
| # ์ฌ์ด๋์นด ์์ - ๊ธฐ์กด ํธ๋ฆฌ๊ฑฐ ์กฐํ ๊ฒฐ๊ณผ๋ก ์ต์ ํ๋กํ. | |
| fb = fallback_triggers() if callable(fallback_triggers) else fallback_triggers | |
| hard, soft, captions = classify_trained_words(fb or []) | |
| if not (hard or soft or captions): | |
| return None | |
| profile = { | |
| "lora_name": lora_name, | |
| "file": os.path.basename(lora_name), | |
| "display_name": os.path.splitext(os.path.basename(lora_name))[0], | |
| "description": "", | |
| "tags": [], | |
| "base_model": "", | |
| "hard_tokens": hard, | |
| "soft_words": soft, | |
| "captions": captions[:MAX_CAPTION_EXAMPLES], | |
| "source": "triggers-only", | |
| } | |
| _PROFILE_CACHE[cache_key] = profile | |
| return profile | |
| def format_profiles_for_llm(profiles, indent=" "): | |
| """ํ๋กํ ๋ชฉ๋ก -> LLM ํ๋กฌํํธ์ ๋ฃ์ ํ ์คํธ ๋ธ๋ก. | |
| ํ ๋ก๋ผ๋น 4์ค ์ด๋ด๋ก ์์ถํ๋ค. ๋ก๋ผ๊ฐ 4๊ฐ๊น์ง ๊ฑธ๋ฆด ์ ์๊ณ ํด๋ฆฝ๋ 4๊ฐ๋ผ | |
| ์ฅํฉํ๋ฉด ์์คํ ํ๋กฌํํธ์ ๋ค๋ฅธ ๊ท์น์ด ๋ฌปํ๋ค. | |
| """ | |
| if not profiles: | |
| return "" | |
| lines = [] | |
| for p in profiles: | |
| head = f"{indent}- {p['display_name']}" | |
| if p.get("tags"): | |
| head += f" [tags: {', '.join(p['tags'][:5])}]" | |
| lines.append(head) | |
| if p.get("description"): | |
| lines.append(f"{indent} what it does: {p['description']}") | |
| if p.get("hard_tokens"): | |
| lines.append(f"{indent} MUST APPEAR VERBATIM: " | |
| + ", ".join(f'"{t}"' for t in p["hard_tokens"])) | |
| if p.get("soft_words"): | |
| lines.append(f"{indent} vocabulary it was trained on (use only where it " | |
| f"genuinely fits, anchored to the body part): " | |
| + ", ".join(p["soft_words"])) | |
| for cap in p.get("captions", []): | |
| lines.append(f'{indent} training caption example: "{cap}"') | |
| return "\n".join(lines) | |
| def collect_hard_tokens(profiles): | |
| out = [] | |
| for p in profiles or []: | |
| out.extend(p.get("hard_tokens") or []) | |
| return list(dict.fromkeys(out)) | |
| def missing_hard_tokens(text, profiles): | |
| """์์ฑ๋ ํ๋กฌํํธ์ ๋น ์ง hard token ๋ชฉ๋ก. | |
| LLM์ `4n4lfi5tin6` ๊ฐ์ ํ ํฐ์ "์ ์ ์ฒ ์๋ก ๊ณ ์ณ์ฃผ๋" ๊ฒฝํฅ์ด ์์ด์ | |
| (๊ทธ ์๊ฐ ๋ก๋ผ๊ฐ ์ ๊ฑธ๋ฆฐ๋ค) ๋์๋ฌธ์ ๋ฌด์ํ๊ณ ์กด์ฌ๋ง ํ์ธํ๋ค. | |
| """ | |
| low = (text or "").lower() | |
| return [t for t in collect_hard_tokens(profiles) if t.lower() not in low] | |
| if __name__ == "__main__": | |
| # ์ค์ ๋ก๋ผ ํด๋์ ๋๊ณ ๋๋ ค๋ณด๋ ์์ฒด ์ ๊ฒ. | |
| import glob | |
| import sys | |
| # ComfyUI ์ฝ์์ UTF-8์ด์ง๋ง ๋งจ์์ผ๋ก ๋๋ฆด ๋ cp949๋ผ ์ด๋ชจ์ง print๊ฐ ์ฃฝ๋๋ค. | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| except (AttributeError, OSError): | |
| pass | |
| root = sys.argv[1] if len(sys.argv) > 1 else "." | |
| files = sorted(glob.glob(os.path.join(root, "*.safetensors"))) | |
| print(f"safetensors {len(files)}๊ฐ ๊ฒ์ฌ\n") | |
| stats = {"sidecar": 0, "none": 0, "hard": 0, "soft": 0, "caption": 0, "desc": 0} | |
| samples = [] | |
| for f in files: | |
| data = _read_sidecar(f) | |
| if not data: | |
| stats["none"] += 1 | |
| continue | |
| p = build_profile_from_sidecar(data, os.path.basename(f), f) | |
| stats["sidecar"] += 1 | |
| if p["hard_tokens"]: | |
| stats["hard"] += 1 | |
| if p["soft_words"]: | |
| stats["soft"] += 1 | |
| if p["captions"]: | |
| stats["caption"] += 1 | |
| if p["description"]: | |
| stats["desc"] += 1 | |
| if p["hard_tokens"] and len(samples) < 5: | |
| samples.append(p) | |
| print(f"์ฌ์ด๋์นด ์์: {stats['sidecar']} / ์์: {stats['none']}") | |
| print(f" ์ค๋ช ์์: {stats['desc']}") | |
| print(f" hard token ์์: {stats['hard']}") | |
| print(f" soft word ์์: {stats['soft']}") | |
| print(f" caption ์์ ์์: {stats['caption']}\n") | |
| for p in samples: | |
| print("=" * 60) | |
| print(format_profiles_for_llm([p])) | |