From b02dd7a1a357acf80544e46e413615be8aa07af4 Mon Sep 17 00:00:00 2001 From: Gin3601 Date: Tue, 19 May 2026 16:39:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=A0=B8=E5=BF=83=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/nodes.cpython-312.pyc | Bin 3262 -> 8269 bytes conditioning/nodes.py | 174 +++++--- image/__pycache__/nodes.cpython-312.pyc | Bin 3093 -> 3604 bytes image/nodes.py | 20 +- latent/__pycache__/nodes.cpython-312.pyc | Bin 4402 -> 4943 bytes latent/nodes.py | 69 +-- .../__pycache__/backend_base.cpython-312.pyc | Bin 11753 -> 13399 bytes .../__pycache__/backend_flux.cpython-312.pyc | Bin 2379 -> 2369 bytes .../__pycache__/backend_sdxl.cpython-312.pyc | Bin 3280 -> 3270 bytes .../__pycache__/nodes.cpython-312.pyc | Bin 5231 -> 15541 bytes model_loader/nodes.py | 414 ++++++++++++++--- sampling/__pycache__/nodes.cpython-312.pyc | Bin 8264 -> 8485 bytes sampling/nodes.py | 28 +- .../test_conditioning_compat.cpython-312.pyc | Bin 0 -> 6066 bytes ...est_loader_prompt_refactor.cpython-312.pyc | Bin 0 -> 23130 bytes .../test_upscale_switcher.cpython-312.pyc | Bin 0 -> 4621 bytes tests/test_loader_prompt_refactor.py | 418 ++++++++++++++++++ upscale/__pycache__/nodes.cpython-312.pyc | Bin 15216 -> 16150 bytes utils/__pycache__/model_info.cpython-312.pyc | Bin 0 -> 8578 bytes utils/model_info.py | 234 ++++++++++ 20 files changed, 1176 insertions(+), 181 deletions(-) create mode 100644 tests/__pycache__/test_conditioning_compat.cpython-312.pyc create mode 100644 tests/__pycache__/test_loader_prompt_refactor.cpython-312.pyc create mode 100644 tests/__pycache__/test_upscale_switcher.cpython-312.pyc create mode 100644 tests/test_loader_prompt_refactor.py create mode 100644 utils/__pycache__/model_info.cpython-312.pyc create mode 100644 utils/model_info.py diff --git a/conditioning/__pycache__/nodes.cpython-312.pyc b/conditioning/__pycache__/nodes.cpython-312.pyc index 2de01a73f28888f53d9151777fb6bc0f43500d46..54d6a43dd6a50ad13f031a70c227b516f7d8cfb9 100644 GIT binary patch literal 8269 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 XEW-jrRJ{|_9#Gd$`j@G?z|a2yNS3!y diff --git a/conditioning/nodes.py b/conditioning/nodes.py index 60c2717..4dff4ee 100644 --- a/conditioning/nodes.py +++ b/conditioning/nodes.py @@ -7,31 +7,38 @@ CATEGORY = "LLS/Conditioning" """ from __future__ import annotations -# ---------- 防御性导入 ---------- -# (本节点直接调用 clip 对象方法,无需额外导入 comfy.sd) +from ..utils.model_info import is_flux_family, is_sdxl_family, parse_model_info + _CLIP_SKIP_CHOICES = [None] + list(range(-1, -25, -1)) -def _encode_conditioning(clip, prompt: str): - """ - 兼容新旧 ComfyUI 的文本编码入口。 +def _normalize_clip_skip(value) -> int: + if value in (None, "", "None"): + return -1 + try: + normalized = int(value) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"[LLS] Invalid clip_skip value: {value!r}") from exc + if normalized < -24 or normalized > -1: + raise RuntimeError(f"[LLS] clip_skip out of supported range [-24, -1]: {normalized}") + return normalized - 新版本优先使用 `encode_from_tokens_scheduled()`; - 若运行环境较旧,仅存在 `encode_from_tokens()`,则手动包装成 - ComfyUI 约定的 CONDITIONING 结构。 - """ - tokens = clip.tokenize(prompt) +def _encode_tokens(clip, tokens, add_dict=None): scheduled = getattr(clip, "encode_from_tokens_scheduled", None) if callable(scheduled): - return scheduled(tokens) + try: + if add_dict: + return scheduled(tokens, add_dict=add_dict) + return scheduled(tokens) + except TypeError: + return scheduled(tokens) plain_encode = getattr(clip, "encode_from_tokens", None) if not callable(plain_encode): raise AttributeError( - "CLIP object does not expose encode_from_tokens_scheduled() " - "or encode_from_tokens()." + "CLIP object does not expose encode_from_tokens_scheduled() or encode_from_tokens()." ) try: @@ -51,30 +58,65 @@ def _encode_conditioning(clip, prompt: str): return [[cond, pooled_dict]] -def _normalize_clip_skip(value) -> int: - """ - 兼容旧工作流中的 null / None,以及字符串形式的 clip_skip。 - """ - if value in (None, "", "None"): - return -1 - try: - normalized = int(value) - except (TypeError, ValueError) as exc: - raise RuntimeError(f"[LLS] Invalid clip_skip value: {value!r}") from exc - - if normalized < -24 or normalized > -1: - raise RuntimeError(f"[LLS] clip_skip out of supported range [-24, -1]: {normalized}") - return normalized +def _encode_standard_prompt(clip, prompt: str): + tokens = clip.tokenize(prompt) + return _encode_tokens(clip, tokens) -# ---------- 节点类 ---------- +def _build_sdxl_tokens(clip, prompt: str): + tokens = clip.tokenize(prompt) + local_tokens = clip.tokenize(prompt) + + if "l" in local_tokens: + tokens["l"] = local_tokens["l"] + if "g" not in tokens and "g" in local_tokens: + tokens["g"] = local_tokens["g"] + + if "l" in tokens and "g" in tokens and len(tokens["l"]) != len(tokens["g"]): + empty = clip.tokenize("") + while len(tokens["l"]) < len(tokens["g"]): + tokens["l"] += empty.get("l", []) + while len(tokens["g"]) < len(tokens["l"]): + tokens["g"] += empty.get("g", []) + + return tokens + + +def _encode_sdxl_prompt(clip, prompt: str, width: int, height: int): + tokens = _build_sdxl_tokens(clip, prompt) + add_dict = { + "width": width, + "height": height, + "crop_w": 0, + "crop_h": 0, + "target_width": width, + "target_height": height, + } + return _encode_tokens(clip, tokens, add_dict=add_dict) + + +def _build_flux_tokens(clip, prompt: str): + tokens = clip.tokenize(prompt) + t5_tokens = clip.tokenize(prompt) + if "t5xxl" in t5_tokens: + tokens["t5xxl"] = t5_tokens["t5xxl"] + return tokens + + +def _encode_flux_prompt(clip, prompt: str, guidance): + tokens = _build_flux_tokens(clip, prompt) + add_dict = {} + if guidance is not None: + add_dict["guidance"] = guidance + return _encode_tokens(clip, tokens, add_dict=add_dict or None) + class LLSSimplePromptEncode: """ 简化版提示词编码节点。 - 优先复用 ComfyUI 原生 clip.encode_from_tokens_scheduled(), - 旧版环境下自动回退到 encode_from_tokens(), - 以兼容 SD1.5 / SDXL 的常见 CLIP 编码路径。 + + - 默认仍兼容原有 SD 风格 CLIP 编码。 + - 若连接 model_info,则按 family 自动切换到 SDXL / FLUX 编码路径。 """ CATEGORY = "LLS/Conditioning" @@ -82,67 +124,79 @@ class LLSSimplePromptEncode: RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "STRING") RETURN_NAMES = ("positive", "negative", "prompt_info") DESCRIPTION = ( - "Encode positive and negative prompts into CONDITIONING tensors. " - "Prefers ComfyUI native encode_from_tokens_scheduled and falls back to " - "encode_from_tokens for older runtimes." + "Encode prompts into CONDITIONING tensors. Uses model_info when available to " + "dispatch between SD15, SDXL, and FLUX text-encoding paths." ) @classmethod def INPUT_TYPES(cls): return { "required": { - "clip": ("CLIP",), + "text_encoder": ("CLIP",), "positive_prompt": ("STRING", {"default": "", "multiline": True}), "negative_prompt": ("STRING", {"default": "", "multiline": True}), "clip_skip": (_CLIP_SKIP_CHOICES, {"default": -1}), - } + }, + "optional": { + "model_info": ("STRING", {"default": ""}), + }, } - def encode(self, clip, positive_prompt: str, negative_prompt: str, clip_skip): - if clip is None: + def encode( + self, + text_encoder, + positive_prompt: str, + negative_prompt: str, + clip_skip, + model_info: str | None = None, + ): + if text_encoder is None: raise RuntimeError( - "[LLS] CLIP is None. Make sure a checkpoint is loaded before connecting to this node." + "[LLS] text_encoder is None. Connect the loader output or load a compatible text encoder first." ) + info = parse_model_info(model_info) + family = info["family"] clip_skip = _normalize_clip_skip(clip_skip) - # 应用 clip_skip(ComfyUI CLIP 对象支持 clip_layer) if clip_skip != -1: try: - clip = clip.clone() - clip.clip_layer(clip_skip) + text_encoder = text_encoder.clone() + text_encoder.clip_layer(clip_skip) except Exception: - # clip_layer 不可用时静默忽略,不阻断流程 pass - # 编码 positive — 复用 ComfyUI 原生编码入口 try: - positive = _encode_conditioning(clip, positive_prompt) + if is_flux_family(family): + guidance = info.get("guidance") if info.get("guidance_embed") else None + positive = _encode_flux_prompt(text_encoder, positive_prompt, guidance) + negative = _encode_flux_prompt(text_encoder, "", guidance) + negative_note = "negative_ignored_for_flux" + elif is_sdxl_family(family): + width = int(info.get("base_width", 1024)) + height = int(info.get("base_height", 1024)) + positive = _encode_sdxl_prompt(text_encoder, positive_prompt, width, height) + negative = _encode_sdxl_prompt(text_encoder, negative_prompt, width, height) + negative_note = "negative_used" + else: + positive = _encode_standard_prompt(text_encoder, positive_prompt) + negative = _encode_standard_prompt(text_encoder, negative_prompt) + negative_note = "negative_used" except Exception as exc: - raise RuntimeError( - f"[LLS] Failed to encode positive prompt: {exc}" - ) from exc - - # 编码 negative — 复用 ComfyUI 原生编码入口 - try: - negative = _encode_conditioning(clip, negative_prompt) - except Exception as exc: - raise RuntimeError( - f"[LLS] Failed to encode negative prompt: {exc}" - ) from exc + raise RuntimeError(f"[LLS] Failed to encode prompts for family {family}: {exc}") from exc pos_preview = positive_prompt[:60].replace("\n", " ") neg_preview = negative_prompt[:40].replace("\n", " ") + if is_flux_family(family): + neg_preview = "" prompt_info = ( - f"clip_skip={clip_skip} " - f"| pos=\"{pos_preview}...\" | neg=\"{neg_preview}...\"" + f"family={family} | clip_skip={clip_skip} | " + f"pos=\"{pos_preview}...\" | neg=\"{neg_preview}...\" | {negative_note}" ) return (positive, negative, prompt_info) -# ---------- 注册表 ---------- - NODE_CLASS_MAPPINGS: dict[str, type] = { "LLSSimplePromptEncode": LLSSimplePromptEncode, } diff --git a/image/__pycache__/nodes.cpython-312.pyc b/image/__pycache__/nodes.cpython-312.pyc index 4fa9c6fee667bcb671f3eb61ec5f4a160b420eac..5aa8a51bc75312d7a0d4578a0f8257fa5a5f60b5 100644 GIT binary patch delta 1621 zcmZ8hUuauZ7(eI!|C8hN1LIxJ&6e)=rrvu? z+jwh5`q0OXqi$nU`#2av#1}yyicnB|SVA9KE)xa{x+hcXP(;*o(x!8shu{6a@BF@V zzVrRLA9TLo6Z$R~Q~+5!$-vaMdM(t6-XPj_a1J1l0|+9-2YJFEEV$E4gzL>WAoezcIK-1^#1bqrC5WWk%vq3@$8kIR)HA;F_%>T5ZoY_& zqCRW7J`GW_jkjh?wt;-9JmZ$imTuys?CHrQVLF2Z-UST_^bBmplG{8R4>c4JN!*yf zldhh*ldSa)H&`Yf+E%5o(g2dI><|wyUf}3wZ-vOW8S~HhYc|Jn9*2`$D$)%3?%tE_ zHex-?rA9azc+8aC@_qdExVvyyTjbqf%kI*IG?&NY&$tBS@jWia@>5}4=i208oa}jf zni$&EaFS1r@NV0lBk#i*o=hg^6i7hd<y4AX3v=Sz2Ybq}5Gn_G+4&pVje~H%Jm$UN)quGi+ zYZ{g>+J-||hD}@27AVlF`)tW_eapy_4I z7krM6p0BoIAd_kLlg%SOyzIj{pKpe3(=Kyp_x3%p>@H%mW&EbpD`Y6;OS~*?Ed8h@ zSCtJdv%=l)=wC$}9hnvNeplkgrBANhyiz-Sa_vHG;$l5p*yu$YUB-&`K$h$r&ss~q3Bg~ zP1eUIH;$jH zjht_=F!&m5ut097v)pm@?V6UVNvS`68^GVnLz!$lSZ|lJL+pA&%N}9ZkMKlJ!|^oJ zOln1OT$|KyylfdbtW;9n&1_KUq|ig5nIVZv6g2#)ax58K delta 1113 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 diff --git a/latent/nodes.py b/latent/nodes.py index c8ac511..bae5bf0 100644 --- a/latent/nodes.py +++ b/latent/nodes.py @@ -7,8 +7,6 @@ CATEGORY = "LLS/Latent" """ from __future__ import annotations -# ---------- 防御性导入 ---------- - try: import torch except Exception as exc: @@ -22,16 +20,14 @@ try: except Exception: model_management = None +from ..utils.model_info import get_latent_spec, parse_model_info -# ---------- 工具函数 ---------- _SIZE_PRESETS = [ "Custom", - # SD1.5 推荐尺寸 "512x512", "512x768", "768x512", - # SDXL 推荐尺寸 "1024x1024", "832x1216", "1216x832", @@ -42,17 +38,16 @@ _SIZE_PRESETS = [ ] -def _round_to_8(value: int) -> int: - """将整数修正为最近的 8 的倍数。""" - return max(8, round(value / 8) * 8) +def _round_to_multiple(value: int, multiple: int) -> int: + return max(multiple, ((value + multiple - 1) // multiple) * multiple) -# ---------- 节点类 ---------- - class LLSSimpleEmptyLatent: """ 生成空白 Latent,作为 txt2img 推理的起点。 - 支持常用分辨率预设,自动修正宿高为 8 的倍数。 + + 默认保持 SD 风格 4 通道 / 8x 下采样; + 若提供 model_info,则可自动切换到 FLUX 的 128 通道 / 16x 下采样。 """ CATEGORY = "LLS/Latent" @@ -61,9 +56,7 @@ class LLSSimpleEmptyLatent: RETURN_NAMES = ("latent", "width", "height", "latent_info") DESCRIPTION = ( "Create an empty latent image as the starting point for txt2img. " - "Select a preset or enter custom width/height. " - "Width and height are automatically rounded to multiples of 8. " - "SD1.5 recommended: 512x512; SDXL recommended: 1024x1024 or 896x1152." + "When model_info is connected, latent channels and downscale ratio adapt to the model family." ) @classmethod @@ -74,30 +67,39 @@ class LLSSimpleEmptyLatent: "width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}), "height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), - } + }, + "optional": { + "model_info": ("STRING", {"default": ""}), + }, } - def create_empty_latent(self, size_preset: str, width: int, height: int, batch_size: int): + def create_empty_latent( + self, + size_preset: str, + width: int, + height: int, + batch_size: int, + model_info: str | None = None, + ): if torch is None: raise RuntimeError( - "[LLS] PyTorch is not available. " - "Make sure this node runs inside a ComfyUI environment." + "[LLS] PyTorch is not available. Make sure this node runs inside a ComfyUI environment." ) from _TORCH_ERR + info = parse_model_info(model_info) + latent_spec = get_latent_spec(info) + latent_channels = latent_spec["latent_channels"] + downscale_ratio = latent_spec["downscale_ratio"] notes = [] - # 如果选择了预设,覆盖 width / height if size_preset != "Custom": try: - w_str, h_str = size_preset.split("x") - width = int(w_str) - height = int(h_str) + width, height = [int(part) for part in size_preset.split("x", 1)] except Exception: notes.append(f"preset parse failed, using custom {width}x{height}") - # 修正为 8 的倍数 - corrected_w = _round_to_8(width) - corrected_h = _round_to_8(height) + corrected_w = _round_to_multiple(width, downscale_ratio) + corrected_h = _round_to_multiple(height, downscale_ratio) if corrected_w != width or corrected_h != height: notes.append(f"size corrected {width}x{height} -> {corrected_w}x{corrected_h}") width, height = corrected_w, corrected_h @@ -114,25 +116,26 @@ class LLSSimpleEmptyLatent: except Exception: dtype = torch.float32 - # 生成空白 Latent(与 ComfyUI EmptyLatentImage 格式一致) - # Latent 尺寸是图像尺寸的 1/8 latent = torch.zeros( - [batch_size, 4, height // 8, width // 8], + [batch_size, latent_channels, height // downscale_ratio, width // downscale_ratio], device=device, dtype=dtype, ) note_str = " | ".join(notes) if notes else "ok" latent_info = ( - f"size={width}x{height} | batch={batch_size} " - f"| preset={size_preset} | {note_str}" + f"family={info['family']} | size={width}x{height} | batch={batch_size} | " + f"channels={latent_channels} | downscale={downscale_ratio} | preset={size_preset} | {note_str}" ) - return ({"samples": latent, "downscale_ratio_spacial": 8}, width, height, latent_info) + return ( + {"samples": latent, "downscale_ratio_spacial": downscale_ratio}, + width, + height, + latent_info, + ) -# ---------- 注册表 ---------- - NODE_CLASS_MAPPINGS: dict[str, type] = { "LLSSimpleEmptyLatent": LLSSimpleEmptyLatent, } diff --git a/lls_universal/__pycache__/backend_base.cpython-312.pyc b/lls_universal/__pycache__/backend_base.cpython-312.pyc index 387c47adaa3893beb56d26d1186688342c2f7233..9621284c5522621e51ea0dee383d9f0c014d6c72 100644 GIT binary patch delta 2802 zcmaJ>duUtN89zt&p{uv`uq;b{$Zua;bsUm3amj)sy99T^%WyD6PHj}5>qLnxX-8Mi zqgSX551bKJyBAh5y49uW;w@guP`4VkwYHN%p)lmx%#P;3#=;(djCo56Au!lEdRZl- z5}fb*e&_f7&Ue0Z&-usL_C@pOCX*hJ-=5ug$s;b9UGU~YN8dOChzsiiIvvP+`hXsB z0Yfn|2h1e06r-wXYrqN#KVT~&_Gw3;I*&LJkF?>MX;;8SVgc#GwbSl^d*PP{QXUOB z1(10bAdAG@VQ38i1nRDFEkH810&EVLh@@UJ1q|n?xM|FEbF^Tuz$@S?a}HdAU*ehx z*HY$6U&*!h6SA#L#$QRUCR}Hkn<{WKw;+blr`9li(5ZftakV)SGt1s7Yy<>&i9X43DBe8m7O{g9jh&3Z8G7@V+4ah`n z6>21%WCK>@MHXVoLG1sL3sn)!zK^IsWsE*@R<+1VCLE{<*@&%1&8P)AX1Rcqc&(_K zcs1ATVq4CkE^-d_D*JPIOzq;*@VI)9dkglfqx^etP<_gOAATeAruHd>Bbhnf`w+gG z@fq%EAj<5SKGncP=5Eyy7Jeu5SH~HKf0y(pKCAxI)0`Q1{RYAhGj;CIA^bS=chA2V z_(^7`G0DI`WJX&^=D$^E`v)nc0pM*Q!(wBB!d!)M4Fn-@r-&sPx%mA6pg;r>D1A>! zR_4liY>AKc8lbR>Mq!pUcZfRujesv{Fl7x9B)JL^t|St#m9>Rb01&HyOM-gFRz1Z1 zkSQ^~4Rt^^lsK1+C2!v^@Sj$CQI8=NOQPwJBfMXwl;;T=s|^s*F*V}&SPAMw5xoFpOgV(D9n5SHTJfzv~0deokdx>Tp=jp9Q5e150clQUpR zPT)v>jkBUAXDY_AXf!M#{A03$2=Q%Vut0rHj-8A~P|j2!OhglMEFt41TFJqRN+zYy zjD%9&L?oVw#iCf2P`(Urp$u;!uSYcJSh_ttMFeufWH1~Kz8047F}h%YV%kt79+OA| zbn(!INRSjO;UPMq9}R{R(#se}aZVo#;^4F-OE{j>hvFnf4n`)WT#Azuv9Oe5V$oQR zPsC7AmP8|NqUu&E!WTPviw3&*8HwXM`1($UE=`*tOTBg&&v~ z{hRiVIpLARlOD`A_pi2YI!?{$b`AFVp)Eth1AAS1bm`)j{b<&B^ifSiTF$ohtsLI0 z>7O(1T58h9Elb<3tzpOJN;|e|#f@4q>+0GAu)CKG*51bqld_xl0An)m=^0)9uB&m| zC2qLHCEup&=-kkr0KDQ|Hrsgk_l@H#^`D8`uZ(ZJGM?=l&kA)942=)m?Ta%jXFrp- z+-Glm{m;(k#TQo`Th0@6r*5f}Rwk+aKWBVdrg6Y>6jPvaL)J{kD%G5@~%L|4- zHMT8Y$)*|(%o`pNcK0oL%i_DsuUy{f9eS?jY}Z(}>ilCyL(A2Was^Q3c{6n*m7dwO zw9m0Sf_+$67uAdbX-1YiZ32txunH1FQR;m$H`j{{X0~-*MG_cW93x z-m|#PM>@XNT0Q6i_dV9Z1I+y%NXAK)z@;C3a>;`XAD>o#-QEd-8f8)1`dnih9q8kZ571$sNimVFV6M{nJgUiS<=Ek9L?V>aWFp7JWn5U|ULldbY6aod z7pa?vdAtutOK2f&{kb%CSZwDD1*z9X_t-~eMcSWd)%1#uk!5|}laXt$dKE9Nry!Ou zQ^;TGLJgmk7t8ndJ`GptkY~Eo-->ab9tZwZ?dz)J4-q#$kQwWG3A&!x+PJnahM|TV zgxEzn9x{ma>h11+xUBxA`$tW`BLJi+|0M(Ycp!WTn*Rn`9)hNa!1+gDR^RnqG{qP= z2-AnY0Mt<(FI+pn$e!#0w^=_6KIHvw@G;Gsoq4~P8(^6ABdq@fvwnj5{{s=ez=r?; delta 1263 zcmaKrUuau(6vuz({(kr7@4YwwlO}DGrM2#MZJMN4JLk4qn2pRu%_!zh?wXCwb(kHi-w##d5_y?$txC9Fq?^>fn-DXx zEccM2r-;xnTI*8vd!kZ5mP}W!daW*KqV?J zQlk>%N}KLNy@Na26gQ7^XO)>Ap5QL@Ui5DCD(~bT^cvpfMX&QNuA=wxZd@)i{XEHa z^Z^&y`|&9Ep%&az#5mKFG44k-#8W(gKFs&=G!Io}GqM~sdg>to@sRkK-G&#$OY%Cr zD7NL#;Ha2ZuE25Oc3gxB>x$zEK*74?x(aaG8u$Dp!5K@^evn|%s`(Bvs9H}#uTteQ zuB2QQ|0dGbyRka}Us;*>U4U<`r-{EP+_C;ho~N*FP4{5sBeB2lnzMivYh(q)r&>t- z(&=XB878^G@Gx7UXQ3PK^dJoJaxx>mc^-@EVc`$&1H2OsF!U-j(QkstTh zGC#j{PtV@hPu_E$6whT2!G@U196UOSM=%`d;6T1naSG`ls%jEYpY4A;fy*-m8`E6PW#@3LbMcpQ-A z0+eEQy964eQ>eqoL}B<+_cxg0--DE&qIpCCV27lCBRxB0-wufgb!1w7g+c+=M;;S9 z5Fd;zpZtbWn)*G%qg~0}%WX=E?Hf$h(eBm<`BdW?*3a8~|iYXQ*M!Vw`-DNtmB$H6utN z14AuK4MP?)P)vlUh9!luhB=dQ4by7o$$ad3{Cx4LdCB=HsqrQG*{ONOlOx%q4RwKf z8W?T}icXiEC_AI- delta 191 zcmX>obXth_G%qg~0}wR*=FSS*$h(eBm=(xlW?*3a><45_XQ*M!Vw`-DNtmB$H6utN z14AtfP>dNQ#?6w#Si_vjxQ1yp(_{g5Jz1I5yyX0p)cCZb{M`7G{Or`c;`rj^jMS9U zoYa)bzU~DeoAV5Nq%-} rUh(8zoE?nvlbyL<3aecdR$EZAB5X&-1!1+z!tNJ%+&9~DPi6!Fo02QI delta 156 zcmX>mc|nr*G%qg~0}wR*=FZaF$orp-kz+D1yPNRo6vi6XGKQk#DNJDca1G<+Mplu{ zRqWo3%#2x_lP@v}3$xa+uVGuw1k%sI5X?}+ki`uY7n#h$Ia^jHH7_|oB{e>+C_gv8 zBtJVfuQ@;c5AM%l^cTrUMpE()7$2s@B*LD=N7u>S=f|INnSlNkZu Cm@uIL diff --git a/model_loader/__pycache__/nodes.cpython-312.pyc b/model_loader/__pycache__/nodes.cpython-312.pyc index 6a50fe85aa8fbbdfc097f80d855fcd0ff9eebedc..90ab0d148c729db33a5e390c71c73291bb8e7372 100644 GIT binary patch literal 15541 zcmcJ0dvqJudFKo;cn%1F4-kBF_!jgaMM|P9*@{e4lth^#RgtnK_yI#0kc0vf@(d`6 zD45Wy?9i@Vv%6kfu9H|vlQkW?4%6K<^s7fc?m6{2-Q5liwt{A{=Xf`iW&dTw)Sjfu zIoH$>?PgWo`7+88zZjW#94_!-{lH|5@}6E?pc>KOg3jWSSSC#-s-R9 zZT>3W?yu%q-Z5tI*Njno(GO{VEyPZU>-aX_b&2uU^P6}##2oMAiy>}+xCG)xh)W@E z;y3eU5I6H%cn`!a{8qjk;#Phe?}fOHZ|5r@-o$tCl@R;*?R*u)n{Cvc|M*V68p^h; zFX6lR8pz+u@8D}8-p23b>mY82_j-ss4Adct=Pprv!yB;P`1rT;4dLw(+TY1HhP(Kt zdmxW*;F}?52j2oYt(Y@yY@)*EW=e4SEwF}6*y1;x!gaSiWO?>)mH0e;p1NQ>MV+S$ z>e`_$tJPik+PXzh*QwS0hxK)RyQZFl!Po}|hPe*zXp9d>2Vy}!EZFz0f9yA&dG)hD zdjH0&uYU2~pMARUgB#!f;~URh`Rx7oSC{_e=376wIseob@6Fx#>DxEnc_CMG?x~wU zd-v1XcRzb=?&h0sa-G~~&s_ZCy{GN=-ky=ZLqo?;ar?MN_;LsNE~k9B(f&CMi1N`g z8=Rbs#e?xkY*HMQO_MM<@pD{bpH(h9I&`3KAaJnfX#c>eK=0w9{@%V}*?oE{66FIE zIE_GLax^BFhJur^$w(*|jU>W>(cnZRIxSZyaX<(^J{1wdVqk14!UrcqVc9hnjt8Q_ zcz7}%5YL7~ayj%87IF;(qe5&VFd3W(%g%@x7>!O{P`ku;k-rely(i3@f>ZIBFHyW{ zGRBedh}>vwYLfRQoS1bk7*^ja@I4;B5a+^^AsD%!)|Aa+Wa4ZzEL(&8IUFAEV`K*V zm9624(_x+u^N_j_4-1pQsLa4b!#+lE!wLz-7?eODn4S#BQ*2YjXM#wRW*8CPE=`q8w zZcE1Kq%i?~4MPt!{ALX2DWM!PUZ-Vu&xw(tz_Edz-oC>_0|)w!!?K!i=|q`58x4lS z<7A0}G>~R3p=dZb8RkVCBL|OA0|~h}a6S?r59sO(Es%d7{KUH;n5EKO%Rf5XFEc+W zdnPV9+h-3!aGfp5xVbA^Bv-p+ZO=GrB)0a7WtnZ0n6`iV(n7g9L@P9Y($dB}$Fwma zNNW$02U(*J1JL}Rg6P2u|3e!r!Y0y3V^TRnGsSxo9qt7CPt?Np_mX35Zsb2fLXzKT#zl}K@k&# zT7>QJ4RRpm5}*_4QBNd76_z5j`3$n1e1kxtLMw<%>M3^m;Kjjw=dUjU#v>i z-jk}>H*d;#H!XNpw%?cX_Db&FTV~2pyHeY+XiC*~uiSMc#ojM5_y6XWi7KlFI4$yA zcb5T}7I|)qxY)1xdNxxZY_|1u8$Z}ZLs)`TTC>J4v z4X6y(XCy017EZ2)WgY1|ITGj_>;<9XxPE-zi`f+Qc|0K~}+oK(1#5qs%!<}Q<3mwSig!NBFqeQR>#DstljEEqy^}vyeoSuq@`vf5-$VCP9QwAsm8Qqwo{)H033W(yYN`D$BU4uerLGT_9B~ zjig-nN!I&7sHjAtVlOH4ExUXRn-&kGTzfvY?g7fdqO$9*Qj|8zZi^o1^l48$^=f?& zYy5z+VrZu!+?bY*z^))I<>}~~z&r$Yqr-BM2K>}e7QT~v0Bv)7EA;5FR%6bhOV=A= z!94Rl4J%2n8=x{Oi7G|#=1Tb1j3JM@yd|k&JYm8N4}psE2sfz7@#n)r zPI%`clUy(2_=*0Ukl&$`47iw}=B^hA+65oX|F&7_I?>OCFMzfY7r7vp?|?%=xSbmg zN10r?BaM#h%1^@t{k-h*_EFQIlZzozi>oNK&U{;Rpn}>pu>r==(%uC=fg_E z38?fq{KV@JAmkOVcs74**_?4!r5&f&j0TrIOBrnTtd+8sT(i_HTWX}*?JJhfjH~wY z=!&Z)Q&N{MskmHz*(z}n$$bWzlschFkuz(i%DHQvmSs=NmC!ue=9c1oqYCCBbndwJUKy=#uFs`w77e^?Yj44WK{j42Gn zO&EhtH&ukJ0a#RE3CAIljo}NS0+!$ntnc?Uu#sq3cnqpP4?poV1hZ7uU^P8TXI$QE zuAR%SooPpT+FPBjsL60!uW|d9xqVB~6nFnEo5>9%YO>jXbIUf|J!FUw3B)zF%Ljys+AJD8oz#*;HnFzcy zOk86aJRu$4oMb?=%M%bo2#EO>qaz^8prRrnOAzkI0GU~$bwj$)kVL^vu1$CdDudMU z7&b)VO)evws8v)jMYsDV*`a`$7@HD8VTBgV*a!?UCQ>8>pd+FY=}?MQs6(lMlEN!k z=@JBpPNnlRA6uF;#hVtYQpG#x%xRZrKKS(X<<=|3FZ-@Mu+Z~^Q;L$EWsIf}opD!O zb8lOAZ(BH%a_^Zl0VFw!=bJAx5?doNH7H~&6a!U}VtTJ0xq8n>qBQ(~bo@c7`ee#^ zN(!8otfvXhgn(vH!noquOTA<3sWN_GEa~wWKk%3!y|K;c4r$P>lLoZy&dYlj4S5+N zFO~3s2b#V|C+S(egrbur3@As!0P*>HU5_V336io&{vR+l3AoL!wimuCM4i6b*`EA%krfkUoLX zx*!q|Mr2o@cj$OuU=Y-e;XvQ<UFGd)m4`@8Eo~ptW-JZXsWI^JD9e3B;Z9rGu4AB=a6I_0#=k_ ztFN)m%WU(NlPR`+;qhg*Q(`(5-UAS#J(@1_&j1?*y29_I4%F@m{2WZu;#W|?h7`o; zCzX4OawPLB)tRu$H>t_3wIC> zb_KoKOWaZ3HrLZGG*Z^I#|`Or+`-D{{la8435PE{S6bEVlTiEV^xQvGnMWJGd|TzA!_T--I+=4IFBgweh;3<3Hs+7Df$-ek1dp=)p5N?p6O{azS-`JvgNw543K)DhM} zWK!$Mb+#(S)?8y-m)X`UQw!mxR*7v*u{~ECme~Umb3oxBdfS7>LH-XcE(oQdn-H!* zPa7a@Bxz65Fzd<-sj9A_*Vs`=MZF%{m`NL*tpcn{MnG8=LCvI|;R!}-V-$Wz?Yy?6 z;|i>9L>3+PYPwl&oL2ofeYQaXg>|s&kyyF0>xZ1g2$3Jea2$0SkUTi`vajH{iCZ}^ zW)an0Bo+eDfrx|&Dmzcq`?EsqT!ar}m5Fw88JKukcXml-Fa(5Ac>?Uvt{>aEW4V?( z{P|ZO;H_u|!C4fTHsa$!_#j?lipuyB)7rSf781kylN(PQEFxfQ2#u3Lt0vumB9h~Z zwW&DRrU2)740}TpM-Eqcb*99?0%i>(k;=%)CKim>b1-67I+Z=@1{5H!>{35O3>ksW z^%-63amAxqD+&?XZoA}+b7 z@_+(bfV<_&8OgO>vTj!!WdJ$47x!NsO1Va)2Og7Lk4e_YJ^^coWmDSKtkwpb#D*1B zgvkLw7{NtF06WEc({}fK`Mfnt(UpE0LYp6JpmoBiS|=Eokosunn;{aScD}v&(^GJ! zH0d&LgSbdfgSC-obXbtDWg>P5v;K_1xhB>@Xk#I5s^F?sFJW4Br&YrukQQ?%m$&E; zLg~pU6zMQvUCrM)6P>I$ZS>g(g;H3Z#F*|-j)f<~0&#gzsTFO4ppd+Q)07B&Iy$XD6pl>o6P5!fq~VlWfC|vRU_FfFv@3@n)uz`OB0dN+kd__Vp@&Z8 z;yXUE?k3iuoo!UYo`N3!8GhnN5WtaksImZ<{x7$#unj~Ym@YiOHR`!dvtEg<*B|z% z%E_#!$W=ARjoF>ZmGDV zQbx5%qN()T@I}>B3Py&CJ1vfQ`rt>eZGwc(kOVw?!?OVAcbWnHT$qIVGK1_zb`rN4 zB9W2zV&Yb#aGn6+I@SG18g`s@S@`x^xc5igfSR6$wY-Y=tMRvQ_96- zvWd)1_+uy&o`pd1Xi;t&8&%gNzF>mja+IIke}FcdadK#t&`r)=K6hcxrA^RXae0&E zYLcu?X-C=on8eme%-2nrgWmL)tU63K@Gx~ACuSU!ttvB>tuf-A6O8(3yhj2xRHXwgvM_WyjoJiP^vChd|jGDAXRbrNBe4p~dbcDopvoalj8l z^TXaKo{T)>H}fX&sx$koVHXeTw%HSR|*uD<)+ z%}amv*cjVIr`@z&d)zVzCSU;KFW z{qKJI^C!M|?-kP5>ibWDN8Rc>&#o^06(o|I?3e%)NZ`g`bh0 z3H0U;5Vr+PV`%B*KA(O1^B=tZ>7TrIWA3kTLOAn;d%f$P>{8S*ZL!cDiOf-EaEh`K zP+K-D69iASFf5|%D3FCZF)Mc^gbsX(@8N=TsNZje@Z^9JawAj1>DX2x;1NZXoOrmS z;t1zc&i^*_;ERk#LcmIM^WY;QToHxF+qn^t2XRc>5HxsBxoiNj`8-rU3-!SRkVvMa zEnF}a%E%ykqj|r*z(UHx*a;f`4@kU(cliiaV05pe*wCaEqr)1I2Db{Ob_@W z3g`es91g)H(*_<N^+vm(EE$4oY>0GMjp@Mx;M@Q0hA=wVleecBfkJTH3R6 z@BwM>gHp@M%+|it)J-nE#L?r6Jm3Ifi)fcFGo^9#5F!tJgNBtbCf&)Bctbu{UGb!or>5v+5r*-m zJImuOcjhH+ybbp#-nKzmDR0LKCQEr1<2M~|m$tk#p5TEO6S3~(!y1wD?bFb3bk z;1UMkhd{2$xsnvh+$v)tf=Q`X1qIn0A7gcYFCe@MJ&_All#uRth;%Q!40RH|ujU!+ zgs2ArBE@GQn5Ax&gBq4o4c!5v5U!Rs!*yO!3%XpENo+MyEFa1haahy<_S%w`D-WlN zyWnz}^tOOEl?nr%}1{*|8LRLzKFt;{s;kapg` za&$b^7?G^?>t0uP9Z8o~rb}DXjl0t|JK+B<*4%B!4>_dru3Jv3c{{04o33n0*L8p| zL^G6fb4IC%gHmtnl?#jIQtMr*@;!5G#?yk8y&P69uTOLLq`jLnb$2fvOx5+KYkiAl zQthsE>)ls(O09=7O?_8KQ%y%Rm7R;NsmeX+@|r7-K%&*Q`L8;YrqX>&vMOP zEpe;fU2dPVXPc?w^7-VI;f2kTYnNo*MOf#c&rQfrHj3b@OK9*a?5skFBt@u^q{sm# zDMFVdMTnE62!&$G=a8)d#hMt9?E!Q^m;z`B$c{kZaj=CD7w>><2?Y392(qo}AJWLy zgC_=iNBV~b!8xlBz-CYpO|-Yco}=*hF)Z9S_a5&*Mk=!85^EwHACK|ETkv&4n8Mwd zx(9}y6yWigel>7`BGpOltoO_%^V$Xk`QkffC=^JZ-%X{S6DS(YRnR9!>1h$Nj< zO?}oyl5VQ8B@5Rb&_f9%Z6er{%b4lf|H*g(5GrdW#)I#b|BI=IcTeT)QGBgVd)w!yuX)>-z3mI+X`M>a?f!azZNGEK$X>XCGwC zb%!B)&1|Mi)=C(9)0&f^cdt2Z^Z>n9LDKGH}ahe%n~ZLtqF_Iak)Nxf+9ZKhr=>D^0x=rr{<7(Xm! pdTWdy){u0wsdu;WLm$)IW&E&CblSu1QOB&OsZOjIQVNUlhjV&qUg>Zna=bdIZlgmb!TXUZ1^hz(CR|(Z};unx9`3Ey|?Sp>ak z2@-u)!64cM;~E$i^a2YbcEJQAW;`-PEkYtwk0fiK4){`{u8;1;JLsO&?OT&;cMvY-aD#YcG77IePT# zPkufzay;|)E1A8=u6_FH)pM^-o<2T#V9(c|Br|6|%6xn)QPhMu(W=~Jc`p)iNu8CU+J1^Jv<#HP@WUph@xCqtc4~HX)UkOISaz_9U z778{Dg%B|%LFX`13gak23B(q{2Zm9I!r<1tstez^708qM`C$}C5*bH6P$yow2q}59 zBLoW9VMA^q0%R{x6cp^DJ7N`$&5?orp^dE^$jJG(`-36>Ga=EXngY*?f#(Jz!LTC7 z${WKGu4sVkk3_?Q%SEZ?{zwSa%n$mNXJxfyi>UDZ!H^jC4~TpyC@ZS5WoJMf#D@n} zFnC;+vCQ0^vH@%+KBU=cc^E!q_u!CP#$zTkoAKHj$pmZa;3s!Nl|bWqXG%XWZFi+? zOHzg{Y(8QsauAcueb723(t0NPK-pl*&=?^CkzxGvPmy5i^_`LVIA5dwZ_eX7k zV$rZ5E3w1f1D)524Tu1@i=#~jgocSY@iMs`9U=}B1lq^aXb8PW?I2u4$GZp&O}g(@ z$>(;cY|gZi9ik*-b{v92hSI{JPuL(s0{*b)ZJxy{S ztdPs0x_u4ZPN2_O?=dpD;z0Emrg@{R_dhb$_Y2k%!bPjJEbI)a6u&bRqn6>`&JZtI zVP3L8OSFs!;MS zL`->M|Km9hDA*f`clx-7l(a&Y6PY z{z$AF>zniE)fbU+`kR3)H`f_e2BV6c)6P;&IZHL&_``VN3@=Vrq+oc98$6fxM3so< z)S7$Gy0{GZ58O6lYOUr-0x=U83OLeF01g#nR^AB(QlNrFKPk|^AcKHbE~Y~@NaD6= zP!a`IFNppAXh@MYnZ*vh~1PUIA4CWp&6>F}`5s*n$UA^B+vt)nBf=f3)uYvqKl| zNvagW_#s48ogV(e@dF-BIJ56c3ejOhaCl~c|^IR30KYM+5#fhYj&OkmtaD!Kxf zL^17jSXD4@t?VLa1!FDSD$M0tfr=A_d}x>gYJFjTD`aGIOPqrINIzr7tr!jG6v0rC z`IPY_hD0dCOq>zSRnUXhl5efJ9X1T>hjnpX!d7%@I!?Ta1lwCwfzvJ`uKx}S`z#8> zcTkiRQz+O)v=S%cSR1!JmS#4GjIVl>JxnNih zLf=0PmST8&P>O^H#IWLyJ&|J`z^R;D7XELHC5$^UM~-_@C~1gA8Z^_%8A+h7SLZV-W0|){fP2_Nr$SIK=63WYqmM!NnMoOZr_$8&l*B!GIZTK+P zqq5?_Gol~>cFKUAY(xWG9wW6W-`B9)IZehF#2Jd!B-KCd=Wt|jHsP)JMC`G~|z{-7+XR*wR)=b5OI=V|O28q9YsvzDkO>p)Q*5oI0D z`f~!*%!#Lbag zF-1{M>n$BBU7XcZmc_RW$XuE-&HMIz9iFetnk~kP@yhzkm8(Z9SD$PD(?esG8@%no&*Lz0mJr|aa)py=9GEQrxMJxni;>n_%ly-O97AXIH1zZcf$mV~(d&=BLvpTXIeD)TUJJ z<}nAKGV^JZHMzQY>Zw%irZLCnlzH>j>c)|(vu$J5o}>+8u&wk!>w(orwvN=M8XroT zAI?(9=*${WN%;Zgi0{~vlxpiw% zWcZx#TuaLS!zqe%TCX_EvkYme_$EsM)i<}Qk-chWfu@FYfeVW+l%?$Jzhl82`2jHP z>GEa^y2vc`HlROPR(b2GON;^67KgW%x>U>HeuJ?^PhGm#(Lz#xBpK*;xMXzpq4 z?0|dR(zCIg#Ir^z!)V)12Trr;|h_DurW4{PzLgd6P?* z0}E_|8DmI2_){}WiL?Rw*dqN=SPK@BaDJ!(r-XHqgCG0|%floDNtKiZ=^;G%Fs?9^ zXaJ=tQtHOEHl8o~?*_Q~Fwtz~-!ZJoxxreg7iMv^kWWDc|11#1O;ShD|FR-tQP!X% z=KqbZ$g-$veu^&3(mJB*?{qDEmsKU&r>uHn$yBL?31}+g%^G(*C zpQ}vUoZt?n6)(1HQ list[str]: + +def _get_filename_list(category: str) -> list[str]: if folder_paths is None: - return ["(ComfyUI not available)"] + return [] try: - names = folder_paths.get_filename_list("checkpoints") - return names if names else ["(no checkpoints found)"] + return list(folder_paths.get_filename_list(category)) except Exception: - return ["(no checkpoints found)"] + return [] -def _detect_family(ckpt_name: str, model_family: str) -> str: - """根据 model_family 参数或文件名推断模型家族。""" - if model_family in ("SD1.5", "SDXL"): - return model_family - # Auto:通过文件名关键词简单判断 - name_lower = ckpt_name.lower() - if any(kw in name_lower for kw in ("sdxl", "_xl", "-xl", "xl_")): - return "SDXL" - return "SD1.5" +def _with_placeholder(names: list[str], placeholder: str) -> list[str]: + cleaned = [name for name in names if name] + if not cleaned: + return [AUTO_PLACEHOLDER, placeholder] + return [AUTO_PLACEHOLDER] + cleaned -# ---------- 节点类 ---------- +def _get_model_name_choices() -> list[str]: + checkpoints = _get_filename_list("checkpoints") + diffusion_models = [f"diffusion_models/{name}" for name in _get_filename_list("diffusion_models")] + names = list(dict.fromkeys(checkpoints + diffusion_models)) + return names if names else [NO_MODELS_PLACEHOLDER] + + +def _get_vae_choices() -> list[str]: + if comfy_core_nodes is not None: + vae_loader = getattr(comfy_core_nodes, "VAELoader", None) + if vae_loader is not None and hasattr(vae_loader, "vae_list"): + try: + names = list(vae_loader.vae_list(vae_loader)) + return _with_placeholder(names, NO_VAE_PLACEHOLDER) + except Exception: + pass + return _with_placeholder(_get_filename_list("vae"), NO_VAE_PLACEHOLDER) + + +def _get_text_encoder_choices() -> list[str]: + return _with_placeholder(_get_filename_list("text_encoders"), NO_TEXT_ENCODER_PLACEHOLDER) + + +def _normalize_choice(value: str | None, placeholder: str) -> str | None: + if value in (None, "", AUTO_PLACEHOLDER, placeholder): + return None + return value + + +def _get_full_path(category: str, name: str) -> str | None: + if folder_paths is None: + return None + try: + return folder_paths.get_full_path_or_raise(category, name) + except AttributeError: + return folder_paths.get_full_path(category, name) + except FileNotFoundError: + return None + + +def _resolve_model_path(model_name: str, family: str) -> tuple[str, str]: + if model_name == NO_MODELS_PLACEHOLDER: + raise RuntimeError( + "[LLS] No models were found in ComfyUI models/checkpoints or models/diffusion_models." + ) + + if model_name.startswith("diffusion_models/"): + if not is_flux_family(family): + raise RuntimeError( + f"[LLS] {family} expects a checkpoint file. Select a checkpoint from models/checkpoints/." + ) + short_name = model_name.split("/", 1)[1] + model_path = _get_full_path("diffusion_models", short_name) + if model_path: + return ("diffusion_models", model_path) + + checkpoint_path = _get_full_path("checkpoints", model_name) + if checkpoint_path: + return ("checkpoints", checkpoint_path) + + if is_flux_family(family): + diffusion_path = _get_full_path("diffusion_models", model_name) + if diffusion_path: + return ("diffusion_models", diffusion_path) + + raise RuntimeError( + f"[LLS] Model '{model_name}' was not found in the expected ComfyUI model directories." + ) + + +def _load_checkpoint_bundle(model_path: str): + try: + return comfy_sd.load_checkpoint_guess_config( + model_path, + output_vae=True, + output_clip=True, + embedding_directory=folder_paths.get_folder_paths("embeddings"), + ) + except Exception as exc: + raise RuntimeError(f"[LLS] Failed to load checkpoint '{model_path}': {exc}") from exc + + +def _load_model(model_source: str, model_path: str): + if model_source == "checkpoints": + loaded = _load_checkpoint_bundle(model_path) + return loaded[0], loaded[1], loaded[2] + + try: + model = comfy_sd.load_diffusion_model(model_path) + except Exception as exc: + raise RuntimeError(f"[LLS] Failed to load diffusion model '{model_path}': {exc}") from exc + return model, None, None + + +def _load_external_vae(vae_name: str): + if comfy_core_nodes is None: + raise RuntimeError( + "[LLS] ComfyUI core VAELoader is unavailable, so external VAE loading cannot be used." + ) from _CORE_NODES_ERR + + vae_loader_cls = getattr(comfy_core_nodes, "VAELoader", None) + if vae_loader_cls is None: + raise RuntimeError("[LLS] ComfyUI core VAELoader class was not found.") + return vae_loader_cls().load_vae(vae_name)[0] + + +def _load_external_text_encoder(family: str, name1: str | None, name2: str | None): + if comfy_core_nodes is None: + raise RuntimeError( + "[LLS] ComfyUI core CLIP loaders are unavailable, so external text encoder loading cannot be used." + ) from _CORE_NODES_ERR + + if family == "SD15": + if not name1: + raise RuntimeError( + "[LLS] SD15 external text encoder loading requires external_text_encoder_1." + ) + clip_loader_cls = getattr(comfy_core_nodes, "CLIPLoader", None) + if clip_loader_cls is None: + raise RuntimeError("[LLS] ComfyUI core CLIPLoader class was not found.") + return clip_loader_cls().load_clip(name1, type="stable_diffusion")[0] + + if is_sdxl_family(family): + if not name1 or not name2: + raise RuntimeError( + "[LLS] SDXL external text encoder loading requires clip_l and clip_g via " + "external_text_encoder_1/external_text_encoder_2." + ) + dual_loader_cls = getattr(comfy_core_nodes, "DualCLIPLoader", None) + if dual_loader_cls is None: + raise RuntimeError("[LLS] ComfyUI core DualCLIPLoader class was not found.") + return dual_loader_cls().load_clip(name1, name2, type="sdxl")[0] + + if not name1 or not name2: + raise RuntimeError( + "[LLS] FLUX text encoder loading requires clip_l and t5xxl via " + "external_text_encoder_1/external_text_encoder_2." + ) + dual_loader_cls = getattr(comfy_core_nodes, "DualCLIPLoader", None) + if dual_loader_cls is None: + raise RuntimeError("[LLS] ComfyUI core DualCLIPLoader class was not found.") + return dual_loader_cls().load_clip(name1, name2, type="flux")[0] + + +def _resolve_text_encoder( + family: str, + source: str, + embedded_text_encoder, + external_name_1: str | None, + external_name_2: str | None, +) -> tuple[object | None, str]: + if source == "manual": + return None, "manual" + + if source == "embedded": + if embedded_text_encoder is None: + raise RuntimeError( + f"[LLS] {family} was set to use embedded text encoders, but the selected model did not provide them." + ) + return embedded_text_encoder, "embedded" + + if source == "external": + return _load_external_text_encoder(family, external_name_1, external_name_2), "external" + + if embedded_text_encoder is not None: + return embedded_text_encoder, "embedded" + if external_name_1: + return _load_external_text_encoder(family, external_name_1, external_name_2), "external" + + if is_flux_family(family): + raise RuntimeError( + "[LLS] FLUX models need text encoders. Provide clip_l and t5xxl via " + "external_text_encoder_1/external_text_encoder_2, or use a checkpoint that embeds them." + ) + + raise RuntimeError( + f"[LLS] {family} did not expose an embedded text encoder. " + "Switch text_encoder_source to 'external' and choose the required encoder files." + ) + + +def _resolve_vae( + family: str, + source: str, + embedded_vae, + external_vae_name: str | None, +) -> tuple[object | None, str]: + if source == "none": + if is_flux_family(family): + raise RuntimeError( + "[LLS] FLUX generation requires a VAE/AE. Provide external_vae_name or use a model with an embedded VAE." + ) + return None, "none" + + if source == "embedded": + if embedded_vae is None: + raise RuntimeError( + f"[LLS] {family} was set to use an embedded VAE, but the selected model did not provide one." + ) + return embedded_vae, "embedded" + + if source == "external": + if not external_vae_name: + raise RuntimeError("[LLS] External VAE loading requires external_vae_name.") + return _load_external_vae(external_vae_name), "external" + + if embedded_vae is not None: + return embedded_vae, "embedded" + if external_vae_name: + return _load_external_vae(external_vae_name), "external" + if is_flux_family(family): + raise RuntimeError( + "[LLS] FLUX models usually need an external AE/VAE. Provide external_vae_name or use a model with an embedded VAE." + ) + return None, "none" + + +def _text_encoder_type_for_family(family: str) -> str: + if family == "SD15": + return "clip" + if is_sdxl_family(family): + return "sdxl_dual_clip" + return "flux_clip_l_t5xxl" + + +def _required_text_encoders_for_family(family: str) -> list[str]: + if family == "SD15": + return ["clip"] + if is_sdxl_family(family): + return ["clip_l", "clip_g"] + return ["clip_l", "t5xxl"] + + +def _required_vae_for_family(family: str) -> str: + return "required" if is_flux_family(family) else "optional" + class LLSSimpleCheckpointLoader: """ - 简化版 Checkpoint 加载器,支持 SD1.5 / SDXL 基础流程。 - 自动识别模型家族,输出 MODEL / CLIP / VAE 以及模型信息字符串。 - 内部复用 ComfyUI 原生 load_checkpoint_guess_config,已自动适配两种架构。 + 统一外观的模型加载节点。 + + 对外仍然输出 MODEL / CLIP / VAE / STRING 以兼容旧工作流; + 但第二个端口实际语义已经升级为 text_encoder,资源分派策略则由 + model_family + source 选项控制。 """ CATEGORY = "LLS/Model Loader" FUNCTION = "load_checkpoint" RETURN_TYPES = ("MODEL", "CLIP", "VAE", "STRING") - RETURN_NAMES = ("model", "clip", "vae", "model_info") - DESCRIPTION = "Load a SD1.5 or SDXL checkpoint. Outputs MODEL, CLIP, VAE and a model_info string." - - _FAMILY_CHOICES = ["Auto", "SD1.5", "SDXL"] + RETURN_NAMES = ("model", "text_encoder", "vae", "model_info") + DESCRIPTION = ( + "Load SD15, SDXL, SDXL Turbo, and FLUX families with family-specific resource " + "dispatch. The CLIP-typed output now represents the resolved text encoder bundle." + ) @classmethod def INPUT_TYPES(cls): return { "required": { - "ckpt_name": (_get_checkpoint_names(),), - "model_family": (cls._FAMILY_CHOICES, {"default": "Auto"}), + "ckpt_name": (_get_model_name_choices(),), + "model_family": (MODEL_FAMILY_CHOICES, {"default": "Auto"}), + "load_mode": (LOAD_MODE_CHOICES, {"default": "simple"}), + "vae_source": (VAE_SOURCE_CHOICES, {"default": "auto"}), + "text_encoder_source": (TEXT_ENCODER_SOURCE_CHOICES, {"default": "auto"}), + "external_vae_name": (_get_vae_choices(), {"default": AUTO_PLACEHOLDER}), + "external_text_encoder_1": (_get_text_encoder_choices(), {"default": AUTO_PLACEHOLDER}), + "external_text_encoder_2": (_get_text_encoder_choices(), {"default": AUTO_PLACEHOLDER}), } } - def load_checkpoint(self, ckpt_name: str, model_family: str): + def load_checkpoint( + self, + ckpt_name: str, + model_family: str, + load_mode: str, + vae_source: str, + text_encoder_source: str, + external_vae_name: str, + external_text_encoder_1: str, + external_text_encoder_2: str, + ): if folder_paths is None: raise RuntimeError( - "[LLS] folder_paths is not available. " - "Make sure this node runs inside a ComfyUI environment." + "[LLS] folder_paths is not available. Make sure this node runs inside ComfyUI." ) from _FOLDER_PATHS_ERR if comfy_sd is None: raise RuntimeError( - "[LLS] comfy.sd is not available. " - "Make sure this node runs inside a ComfyUI environment." + "[LLS] comfy.sd is not available. Make sure this node runs inside ComfyUI." ) from _COMFY_SD_ERR - # 解析模型路径(使用 ComfyUI 原生方法,自动抛出文件不存在异常) - try: - ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name) - except (AttributeError, TypeError): - # get_full_path_or_raise 在旧版 ComfyUI 中可能不存在,回退到 get_full_path - ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) - if not ckpt_path: - raise RuntimeError( - f"[LLS] Checkpoint '{ckpt_name}' not found in ComfyUI checkpoints directories. " - f"Please add the file to models/checkpoints/ and restart ComfyUI." - ) - except FileNotFoundError as exc: - raise RuntimeError( - f"[LLS] Checkpoint '{ckpt_name}' not found: {exc}" - ) from exc + family = canonicalize_family(model_family) + if model_family == "Auto": + family = infer_family_from_name(ckpt_name, family) - # 加载 checkpoint(复用 ComfyUI 原生加载逻辑,自动适配 SD1.5 / SDXL) - try: - out = comfy_sd.load_checkpoint_guess_config( - ckpt_path, - output_vae=True, - output_clip=True, - embedding_directory=folder_paths.get_folder_paths("embeddings"), - ) - except Exception as exc: - raise RuntimeError( - f"[LLS] Failed to load checkpoint '{ckpt_name}': {exc}" - ) from exc - - model, clip, vae = out[0], out[1], out[2] + model_source, model_path = _resolve_model_path(ckpt_name, family) + model, embedded_text_encoder, embedded_vae = _load_model(model_source, model_path) if model is None: - raise RuntimeError(f"[LLS] Checkpoint '{ckpt_name}' did not produce a valid MODEL.") - if clip is None: - raise RuntimeError(f"[LLS] Checkpoint '{ckpt_name}' did not produce a valid CLIP.") - if vae is None: - raise RuntimeError(f"[LLS] Checkpoint '{ckpt_name}' did not produce a valid VAE.") + raise RuntimeError(f"[LLS] The selected model '{ckpt_name}' did not produce a valid MODEL.") - detected_family = _detect_family(ckpt_name, model_family) + external_vae_name = _normalize_choice(external_vae_name, NO_VAE_PLACEHOLDER) + external_text_encoder_1 = _normalize_choice(external_text_encoder_1, NO_TEXT_ENCODER_PLACEHOLDER) + external_text_encoder_2 = _normalize_choice(external_text_encoder_2, NO_TEXT_ENCODER_PLACEHOLDER) - model_info = ( - f"ckpt={ckpt_name} | family={detected_family}" + text_encoder, resolved_text_encoder_source = _resolve_text_encoder( + family=family, + source=text_encoder_source, + embedded_text_encoder=embedded_text_encoder, + external_name_1=external_text_encoder_1, + external_name_2=external_text_encoder_2, + ) + vae, resolved_vae_source = _resolve_vae( + family=family, + source=vae_source, + embedded_vae=embedded_vae, + external_vae_name=external_vae_name, ) - return (model, clip, vae, model_info) + latent_spec = get_latent_spec({"family": family}) + model_info = build_model_info( + family=family, + model_name=ckpt_name, + model_source=model_source, + load_mode=load_mode, + text_encoder_source=resolved_text_encoder_source, + vae_source=resolved_vae_source, + text_encoder_type=_text_encoder_type_for_family(family), + has_embedded_vae=embedded_vae is not None, + has_embedded_text_encoder=embedded_text_encoder is not None, + required_text_encoders=_required_text_encoders_for_family(family), + required_vae=_required_vae_for_family(family), + is_turbo=(family == "SDXL_TURBO"), + guidance_embed=family_requires_guidance(family), + latent_channels=latent_spec["latent_channels"], + downscale_ratio=latent_spec["downscale_ratio"], + ) + return (model, text_encoder, vae, model_info) -# ---------- 注册表 ---------- NODE_CLASS_MAPPINGS: dict[str, type] = { "LLSSimpleCheckpointLoader": LLSSimpleCheckpointLoader, diff --git a/sampling/__pycache__/nodes.cpython-312.pyc b/sampling/__pycache__/nodes.cpython-312.pyc index 47101eae2b4287eda753f13ed6158f2e45878477..d2cfe58eefd17968b01b73080fbc33217f652d37 100644 GIT binary patch delta 2982 zcmbtWO>7&-6`tMYF3BaAT>gm?MTv~8T4HUhaf7B-9NQ9U$rdUowti5HZo)F2rOcE+ z@^DpI%Bswd0vLr0#N9*e+ChvWEnKKbg80z-W=NL??ID3%z*q!mixdci_CUuj3Iqk* z_m&cE192{$#eVbt=FRNPdvEp+L%&M*{ZUb50&O$led$k65b_x^+s)A=xVyJ6^d;)l zLB*1zEM~>Bl$AK*CX}N>$ys(~-5jApR^E~*Wnb2BOVlk%pd8GGY)N=a63IqjMB5e@ z%|_=4m43-(+o5+t-vPY~`WOvS_o^e?Nkbtj0}*$_#Lb6>&Aoxx&pELkp`KMS+r^V3 zgnCyARo3|C(zD%E(YlLVHbH$_lKP(oF;%Dvk|$^YBteuc2|I}9=_SmU^}?#$nl~%Y z;um>Ode8s3*)MX7jALjsm#%l!!zR} zlT)l01)<#GkQh$P|{e94S;majEghT!a0fd7HhX5>ZDX(i4 zJy$H}=QQ?h6p58;v8J(oXfuc~gfMK@eFI#-`JS&$q#&5Q{*kYbmjxi9ad%%Zzf`}= zQ+X@gJRS&1G3!}!rM%|%1AmRpfEDbtG(J2%GBIsQQ`3`U6Gu`!%bCZ5H|i>=!54S; zT_E1Vtl{Aibl{o|GpooIcq&mRJTB_KBG{ZvJ=D9!Dbz>(RHXqLyy7VE`qt_QS$p6m zgkE%U+iz0?`v((*?Fkeb0VfgK)^tLnS3Iq zS|Pts(wQ4MVQgZ0^FFkH;WYQ_&Bsqt0xR44a|VLCTtcVi*OR*2M8D{mfVW^?^W4pl;GjHn098N$TVuXs`GlWTFIAEf~Dq; z&kT=`P0!|zO^!^BOiwY)2Rm9*9E4!0R>Ln$vVQZQ;cuxofv({xzd{-=66-bhMC$wF zB+zy7?6uSflh+a-cMmoMzQcE?E1=2^5^&3QQcRpuV9<7J#mfigCz0ptI&O&F4DTAw z25m(#6e{2*t@!lTSVJ+y?aH^}H~g>0b-!&6VECy6?OPt4df*|Q_=wJBNGqx#QRzqh z#7|h89@;j2DQw<%N9wNDG*II8=(g724ApQNF2ile25$(4!|?ppy=LDqu+#wc80uvx z(S{RHFPnsjq7bh*LkS`ZYOAp0J6jv4Qj; z0CPBIzj-HGN-WDsBiZzPAf>PjY7QfeASBKC_VzlHkjB)pgknXv#6qc>*DZ9766Dao_=kPbO;es(j18>zw0uAoAwv3*G0We*}83PG38CEAY%Y z>9!~Ev8U_PaP*>daq*J>R=EG%$frT%7A`5bf;-KRI##{Ef97W4=81;LE6;J}WGqu( z7bEF?WPP7I6A?Bze?}EHR1s)qUpcD%ot9A3S@UCGhjlg2&Zg`nUqzXn=|4ltPIsHK zcfr=;RxHcl1&(Q=TG3Ko%azMj@@13jJkwvtVOYu8qjwc6pbX)Ccx&&Nzv$ds4~p_# zfk=S{4^$(~k)EB6&LK|R-SCo*ctf#CAMpnpew$Q@8fpY=GDv)(M#v_^BpPc(Y_g4n zqm8Idwv%AA(P5J@5=q>%{WkATT*b1V7R|l!m+Onbj8069rqusySMcVv94}U@CHvN7 zmw`|9unQ=bbGgETzQD9xjtL-OZ=h6N(2J$o;6SU6R_%bcL)-G6s+J2&1NIMx*4)nw zj3r@ zlf!0v_i_FUhk3sHr``g`?Y=`08z;=3#EtS>QrbzbO6e~08!7!1`OuMZlN*jqf~-5m zbWB+HI?@qgJz~>cV){v8y~mN>C9LnV>8HeuB&_dqWH<V=0aV(ea{vGU delta 2753 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 MQCK`|b!>M2KlGJW2LJ#7 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_upscale_switcher.cpython-312.pyc b/tests/__pycache__/test_upscale_switcher.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75829f1fd1e3700fb3742ebf67fa08a881351a92 GIT binary patch literal 4621 zcmb_fOKcnG5&rkZT|V_PWs-VCitJddRJpS2rg0oKdeCuTiBc^Ig@qssn!A!nuSjNh zWmy7+$bgGjKnfMe!9E)3#fE(Fv8SR1oLZppO}1PZFnZ|BE|;Wfx=kNH z!P);YGyCuSGvCbrhhWf0pk)sSvR^^(FF0@;SMD7A13EW}L?lWf3zYq&7ijy*EHL(y zU0~r!E9?rlz)^%IGMx>$imFeU-=&;Uwxv4j_IAsiY^ys{eM(Fe1`9rky-pVVH2H`~ z+;t*(c4>!aAz;t(Fek`tHdd8%d%H}TGbA@j-pl=jsI9P1HrwNJRG;onD6&qiQ*qxD zTuR0%i(Sd5FF(O=NLRTD;bDO44??hXljxPTYWXW;CF&mIe$M8?^^9U2)(Uc3TvYQb;$jA5TxN^CFg-nIF`BMg&1HQy z7xKgOx@Pgtv}Spfd`gm4%P(IAUCwNrwpg3hQ|7`W?CE*gjeFOQ*M5Pht!4D|vaG@f zSZ%0FEGVl>nVeWir7x$JWHk)3cmUdEAY0_I|Ip6J_DIP;Z1{&w;jPC);KrsQbUr?E z>>m58k8i)f&y&E31HuM8TNBTHBy@P^)9p`7!9gQ9cxUcG@Wj?xb71)T_!sA|o!@Id zX0ZJxE8w8Sb{K5Ot+Nl=KGWZ_HFW^5d(}nDaYqiiVdW-~?MQ|wrXZdei3Z9_3{Y;J zjdNDyf}GB)QYN=F?;O@_`AgHa4vl@=?Z0R1q~XAKp@*$VM5hCE2}kbx$vTNsNiZ@e z!ig#t2dAK z#l{{A<7U&_j=gG;s)BJ7ApYOjtH>^L^%mnG#$*M8KQJ{tk(ex7OYMVK#5vnW>QU%h zKATU>rB-B%h3J%aclUM%rYIBPcb*j7r$J?N#lH&L{FVi`xE9}vaQR#S?zqGI5wkamcA9LQZ_3)_dQ zqT|}+FmyCbjn~ODn&u+U1e)tR0MhGlBV!u=mF9-k0j@L->vbm1T5WTHUiGo;3Zu44 zI7DC;Qdnipjc5u|0c3b;h2EqnQcX%(p|Y7uCySU@*NT+H6sbm40i-k5O=qVFrj164 z=wDR~n^=v&&#eYP2Wga?KBpxP>j=qXQ{JY6O}$mwb<#(MiAJr_6uAidp~#p0SDA}s z4Jv`8ir@qR&S5^&tcbWIS27x&ysX%sprKEzvc9V3#49OfRkj3G{%kd)$`Te4kaRG- zEnborQ>%)u_P{9av-5@3)i7NSmE&3cF|ewLOBPp1LHe;g`AbopLc6@X) zAv)nSGdVh*I5#~xk+@*BPEO9&Ie_K=I5{&vC(d1*Nz7XQl%~n5o_rohbwh7K)(!cd z63#n8?fXc3UnzXb2%jp2M~(34gYfu`voHMiT8MaqB_U!6kz4JzeMWTP&RHXR^8Tn1 z{n7n(BRcxq_x>bIy=by|ZEY_#jE3KIj1lH*!i{Zx)9E4Keg)Q0H27rjk$!m7fagu2zsO>=fTY2vvTEeA_lF32ooq-D+ zdC~(5ao!eD@eZJ3WU0rH3?lhHk|89$K%k`Q!9J#I^*oYEAn}k5Y3c}0*uaH(N*zOj zUa$TH$%jb#kzi)Dco*qLaSF=~yLc)WO=<#p08Oz#h1%Vt${HZ3hIk5?R4xOCZ3rnS zskFSDSAeeuo5e*eImJs*qu0RHM&gY~^q;6X3ER*Fi11qYX&bmRRT`W$1}7i1P2CV4 z*TGRKFlYn@%|PGd=KfOih|xS!i%6~B&~t$Vns-ibpDy`34Sy#fm@Cq2i<~i<&*0Ue z3$G5or(O`*Z>_?oPx5^K|eEaXeYUyG=nGh0w@2@F<#$`0Te;GmyWdE)sBEg ze%JH3*e)|!Ip z6b_M~yJMz9xjM%_)0)zce eDDaLO8-HY?Cfl-g?u)5wQ$L^HX9yd!E&ETL67Q}6 literal 0 HcmV?d00001 diff --git a/tests/test_loader_prompt_refactor.py b/tests/test_loader_prompt_refactor.py new file mode 100644 index 0000000..7c40000 --- /dev/null +++ b/tests/test_loader_prompt_refactor.py @@ -0,0 +1,418 @@ +import importlib.util +import json +import pathlib +import sys +import types +import unittest +from unittest import mock + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def load_plugin_package(): + spec = importlib.util.spec_from_file_location( + "lls_node_test_refactor", + ROOT / "__init__.py", + submodule_search_locations=[str(ROOT)], + ) + module = importlib.util.module_from_spec(spec) + sys.modules["lls_node_test_refactor"] = module + spec.loader.exec_module(module) + return module + + +class FolderPathsStub: + def __init__(self): + self._files = { + "checkpoints": [ + "sd15.safetensors", + "sdxl_turbo.safetensors", + "flux-dev-checkpoint.safetensors", + ], + "diffusion_models": [ + "flux-schnell.safetensors", + ], + "vae": [ + "ae.safetensors", + ], + "text_encoders": [ + "clip_l.safetensors", + "clip_g.safetensors", + "t5xxl_fp16.safetensors", + ], + } + + def get_filename_list(self, category): + return list(self._files.get(category, [])) + + def get_full_path_or_raise(self, category, name): + if name in self._files.get(category, []): + return f"/fake/{category}/{name}" + raise FileNotFoundError(name) + + def get_full_path(self, category, name): + if name in self._files.get(category, []): + return f"/fake/{category}/{name}" + return None + + def get_folder_paths(self, category): + return [f"/fake/{category}"] + + +class ComfySDStub: + class CLIPType: + STABLE_DIFFUSION = "stable_diffusion" + FLUX = "flux" + + def load_checkpoint_guess_config( + self, + ckpt_path, + output_vae=True, + output_clip=True, + embedding_directory=None, + ): + if ckpt_path.endswith("sd15.safetensors"): + return ("MODEL::SD15", "CLIP::SD15", "VAE::SD15") + if ckpt_path.endswith("sdxl_turbo.safetensors"): + return ("MODEL::SDXL_TURBO", None, None) + if ckpt_path.endswith("flux-dev-checkpoint.safetensors"): + return ("MODEL::FLUX_DEV", None, None) + raise AssertionError(f"unexpected checkpoint path: {ckpt_path}") + + def load_diffusion_model(self, unet_path, model_options=None, disable_dynamic=False): + return f"MODEL::{pathlib.Path(unet_path).name}" + + def load_clip( + self, + ckpt_paths, + embedding_directory=None, + clip_type=None, + model_options=None, + disable_dynamic=False, + ): + joined = "+".join(pathlib.Path(path).name for path in ckpt_paths) + return f"CLIP::{clip_type}::{joined}" + + +class CoreNodesStub: + class VAELoader: + @staticmethod + def vae_list(_self): + return ["ae.safetensors"] + + def load_vae(self, vae_name): + return (f"VAE::{vae_name}",) + + class CLIPLoader: + def load_clip(self, clip_name, type="stable_diffusion", device="default"): + return (f"CLIP::{type}::{clip_name}",) + + class DualCLIPLoader: + def load_clip(self, clip_name1, clip_name2, type, device="default"): + return (f"CLIP::{type}::{clip_name1}+{clip_name2}",) + + +class RecordingClip: + def __init__(self, calls=None): + self.calls = [] if calls is None else calls + self.layer = None + + def clone(self): + cloned = RecordingClip(self.calls) + cloned.layer = self.layer + return cloned + + def clip_layer(self, layer): + self.layer = layer + + def tokenize(self, text): + return { + "text": text, + "g": [f"g::{text}"], + "l": [f"l::{text}"], + "t5xxl": [f"t5::{text}"], + } + + def encode_from_tokens_scheduled(self, tokens, add_dict=None): + self.calls.append( + { + "tokens": dict(tokens), + "add_dict": dict(add_dict or {}), + "layer": self.layer, + } + ) + return [[f"cond::{tokens.get('text', '')}", {"pooled_output": dict(add_dict or {})}]] + + +class FakeVAE: + def decode(self, latent_tensor): + batch, _channels, height, width = latent_tensor.shape + return FakeTensor((batch, height, width, 3)) + + +class FakeTensor: + def __init__(self, shape): + self.shape = tuple(shape) + self.dtype = "float32" + self.layout = "strided" + self.device = "cpu" + self.is_nested = False + + def size(self): + return self.shape + + def reshape(self, *shape): + return FakeTensor(shape) + + +class FakeTorch: + float32 = "float32" + + @staticmethod + def zeros(shape, device=None, dtype=None, layout=None): + return FakeTensor(shape) + + +class TestLoaderPromptRefactor(unittest.TestCase): + def test_loader_schema_exposes_new_family_and_source_controls(self): + plugin = load_plugin_package() + node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimpleCheckpointLoader"] + required = node_cls.INPUT_TYPES()["required"] + + self.assertEqual( + tuple(required.keys()), + ( + "ckpt_name", + "model_family", + "load_mode", + "vae_source", + "text_encoder_source", + "external_vae_name", + "external_text_encoder_1", + "external_text_encoder_2", + ), + ) + family_choices = required["model_family"][0] + self.assertIn("SD15", family_choices) + self.assertIn("SDXL_TURBO", family_choices) + self.assertIn("FLUX_SCHNELL", family_choices) + self.assertIn("FLUX_DEV", family_choices) + + def test_loader_returns_json_model_info_for_sd15_embedded_resources(self): + load_plugin_package() + from lls_node_test_refactor.model_loader import nodes as loader_nodes + + with mock.patch.object(loader_nodes, "folder_paths", FolderPathsStub()), mock.patch.object( + loader_nodes, + "comfy_sd", + ComfySDStub(), + ), mock.patch.object(loader_nodes, "comfy_core_nodes", CoreNodesStub()): + node = loader_nodes.LLSSimpleCheckpointLoader() + model, text_encoder, vae, model_info = node.load_checkpoint( + "sd15.safetensors", + "SD15", + "simple", + "auto", + "auto", + "(auto)", + "(auto)", + "(auto)", + ) + + info = json.loads(model_info) + self.assertEqual(model, "MODEL::SD15") + self.assertEqual(text_encoder, "CLIP::SD15") + self.assertEqual(vae, "VAE::SD15") + self.assertEqual(info["family"], "SD15") + self.assertEqual(info["text_encoder_type"], "clip") + self.assertTrue(info["has_embedded_vae"]) + self.assertEqual(info["required_text_encoders"], ["clip"]) + self.assertEqual(info["required_vae"], "optional") + self.assertFalse(info["is_turbo"]) + + def test_loader_builds_flux_resources_from_external_models(self): + load_plugin_package() + from lls_node_test_refactor.model_loader import nodes as loader_nodes + + with mock.patch.object(loader_nodes, "folder_paths", FolderPathsStub()), mock.patch.object( + loader_nodes, + "comfy_sd", + ComfySDStub(), + ), mock.patch.object(loader_nodes, "comfy_core_nodes", CoreNodesStub()): + node = loader_nodes.LLSSimpleCheckpointLoader() + model, text_encoder, vae, model_info = node.load_checkpoint( + "diffusion_models/flux-schnell.safetensors", + "FLUX_SCHNELL", + "advanced", + "external", + "external", + "ae.safetensors", + "clip_l.safetensors", + "t5xxl_fp16.safetensors", + ) + + info = json.loads(model_info) + self.assertEqual(model, "MODEL::flux-schnell.safetensors") + self.assertEqual(text_encoder, "CLIP::flux::clip_l.safetensors+t5xxl_fp16.safetensors") + self.assertEqual(vae, "VAE::ae.safetensors") + self.assertEqual(info["family"], "FLUX_SCHNELL") + self.assertEqual(info["text_encoder_type"], "flux_clip_l_t5xxl") + self.assertEqual(info["required_text_encoders"], ["clip_l", "t5xxl"]) + self.assertEqual(info["required_vae"], "required") + + def test_loader_raises_clear_error_when_flux_resources_are_missing(self): + load_plugin_package() + from lls_node_test_refactor.model_loader import nodes as loader_nodes + + with mock.patch.object(loader_nodes, "folder_paths", FolderPathsStub()), mock.patch.object( + loader_nodes, + "comfy_sd", + ComfySDStub(), + ), mock.patch.object(loader_nodes, "comfy_core_nodes", CoreNodesStub()): + node = loader_nodes.LLSSimpleCheckpointLoader() + with self.assertRaisesRegex(RuntimeError, "clip_l.*t5xxl"): + node.load_checkpoint( + "diffusion_models/flux-schnell.safetensors", + "FLUX_DEV", + "simple", + "external", + "external", + "ae.safetensors", + "(auto)", + "(auto)", + ) + + def test_prompt_encode_dispatches_sdxl_using_model_info_dimensions(self): + plugin = load_plugin_package() + node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimplePromptEncode"] + node = node_cls() + clip = RecordingClip() + model_info = json.dumps({"family": "SDXL", "base_width": 1216, "base_height": 832}) + + positive, negative, prompt_info = node.encode( + clip, + "a castle", + "low quality", + -1, + model_info=model_info, + ) + + self.assertEqual(len(clip.calls), 2) + self.assertIn("g", clip.calls[0]["tokens"]) + self.assertIn("l", clip.calls[0]["tokens"]) + self.assertEqual( + clip.calls[0]["add_dict"], + { + "width": 1216, + "height": 832, + "crop_w": 0, + "crop_h": 0, + "target_width": 1216, + "target_height": 832, + }, + ) + self.assertEqual(positive[0][0], "cond::a castle") + self.assertEqual(negative[0][0], "cond::low quality") + self.assertIn("family=SDXL", prompt_info) + + def test_prompt_encode_dispatches_flux_and_neutralizes_negative_prompt(self): + plugin = load_plugin_package() + node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimplePromptEncode"] + node = node_cls() + clip = RecordingClip() + model_info = json.dumps({"family": "FLUX_DEV", "guidance": 3.5, "guidance_embed": True}) + + positive, negative, prompt_info = node.encode( + clip, + "a robot in a forest", + "ugly, blurry", + -1, + model_info=model_info, + ) + + self.assertEqual(len(clip.calls), 2) + self.assertIn("t5xxl", clip.calls[0]["tokens"]) + self.assertEqual(clip.calls[0]["add_dict"]["guidance"], 3.5) + self.assertEqual(clip.calls[1]["tokens"]["text"], "") + self.assertEqual(positive[0][0], "cond::a robot in a forest") + self.assertEqual(negative[0][0], "cond::") + self.assertIn("negative_ignored_for_flux", prompt_info) + + def test_empty_latent_uses_flux_shape_when_model_info_requests_it(self): + load_plugin_package() + from lls_node_test_refactor.latent import nodes as latent_nodes + + node = latent_nodes.LLSSimpleEmptyLatent() + model_info = json.dumps({"family": "FLUX_SCHNELL", "latent_channels": 128, "downscale_ratio": 16}) + + with mock.patch.object(latent_nodes, "torch", FakeTorch()): + latent, width, height, latent_info = node.create_empty_latent( + "Custom", + 1000, + 1000, + 2, + model_info=model_info, + ) + + self.assertEqual((width, height), (1008, 1008)) + self.assertEqual(tuple(latent["samples"].shape), (2, 128, 63, 63)) + self.assertEqual(latent["downscale_ratio_spacial"], 16) + self.assertIn("family=FLUX_SCHNELL", latent_info) + + def test_ksampler_uses_turbo_preset_defaults_when_model_info_requests_it(self): + load_plugin_package() + from lls_node_test_refactor.sampling import nodes as sampling_nodes + + recorded = {} + + def fake_common_ksampler(**kwargs): + recorded.update(kwargs) + return kwargs["latent"] + + with mock.patch.object(sampling_nodes, "comfy_sample", object()), mock.patch.object( + sampling_nodes, + "comfy_samplers", + object(), + ), mock.patch.object( + sampling_nodes, + "_common_ksampler", + side_effect=fake_common_ksampler, + ): + node = sampling_nodes.LLSSimpleKSampler() + latent, sample_info = node.sample( + model="model", + positive="positive", + negative="negative", + latent_image={"samples": FakeTensor((1, 4, 64, 64))}, + quality_preset="Balanced", + seed=123, + steps=30, + cfg=7.0, + sampler_name="euler", + scheduler="normal", + denoise=1.0, + model_info=json.dumps({"family": "SDXL_TURBO", "is_turbo": True}), + ) + + self.assertEqual(recorded["steps"], 6) + self.assertEqual(recorded["cfg"], 1.5) + self.assertEqual(latent["samples"].shape, (1, 4, 64, 64)) + self.assertIn("family=SDXL_TURBO", sample_info) + + def test_vae_decode_uses_model_info_downscale_ratio_for_flux(self): + plugin = load_plugin_package() + node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimpleVAEDecode"] + node = node_cls() + + image, decode_info = node.decode( + {"samples": FakeTensor((1, 128, 64, 64))}, + FakeVAE(), + model_info=json.dumps({"family": "FLUX_DEV", "downscale_ratio": 16}), + ) + + self.assertEqual(tuple(image.shape), (1, 64, 64, 3)) + self.assertIn("decoded=1024x1024", decode_info) + + +if __name__ == "__main__": + unittest.main() diff --git a/upscale/__pycache__/nodes.cpython-312.pyc b/upscale/__pycache__/nodes.cpython-312.pyc index 6df1478f170577599cc90bcbb9b791c502794bf7..e210813a104292e4badbbde1a6c37b572b6707ca 100644 GIT binary patch delta 3149 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 RsY|jfwA#7^%S{33{{XafC>j6& diff --git a/utils/__pycache__/model_info.cpython-312.pyc b/utils/__pycache__/model_info.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00bd536d1439e15a14ba71e5806b03a9d83e0902 GIT binary patch literal 8578 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 H#18)lhDek8 literal 0 HcmV?d00001 diff --git a/utils/model_info.py b/utils/model_info.py new file mode 100644 index 0000000..b1fae1f --- /dev/null +++ b/utils/model_info.py @@ -0,0 +1,234 @@ +from __future__ import annotations + +import json +import re +from typing import Any + + +AUTO_FAMILY = "Auto" + +LEGACY_FAMILY_ALIASES: dict[str, str] = { + "SD1.5": "SD15", + "SD15": "SD15", + "SDXL": "SDXL", + "SDXL_TURBO": "SDXL_TURBO", + "FLUX": "FLUX_DEV", + "FLUX_SCHNELL": "FLUX_SCHNELL", + "FLUX_DEV": "FLUX_DEV", +} + +MODEL_FAMILY_CHOICES: list[str] = [ + AUTO_FAMILY, + "SD15", + "SD1.5", + "SDXL", + "SDXL_TURBO", + "FLUX_SCHNELL", + "FLUX_DEV", +] + +TEXT_ENCODER_TYPES: dict[str, str] = { + "SD15": "clip", + "SDXL": "sdxl_dual_clip", + "SDXL_TURBO": "sdxl_dual_clip", + "FLUX_SCHNELL": "flux_clip_l_t5xxl", + "FLUX_DEV": "flux_clip_l_t5xxl", +} + +REQUIRED_TEXT_ENCODERS: dict[str, list[str]] = { + "SD15": ["clip"], + "SDXL": ["clip_l", "clip_g"], + "SDXL_TURBO": ["clip_l", "clip_g"], + "FLUX_SCHNELL": ["clip_l", "t5xxl"], + "FLUX_DEV": ["clip_l", "t5xxl"], +} + +REQUIRED_VAE: dict[str, str] = { + "SD15": "optional", + "SDXL": "optional", + "SDXL_TURBO": "optional", + "FLUX_SCHNELL": "required", + "FLUX_DEV": "required", +} + +LATENT_SPECS: dict[str, dict[str, int]] = { + "SD15": {"latent_channels": 4, "downscale_ratio": 8}, + "SDXL": {"latent_channels": 4, "downscale_ratio": 8}, + "SDXL_TURBO": {"latent_channels": 4, "downscale_ratio": 8}, + "FLUX_SCHNELL": {"latent_channels": 128, "downscale_ratio": 16}, + "FLUX_DEV": {"latent_channels": 128, "downscale_ratio": 16}, +} + +BASE_RESOLUTIONS: dict[str, tuple[int, int]] = { + "SD15": (512, 512), + "SDXL": (1024, 1024), + "SDXL_TURBO": (1024, 1024), + "FLUX_SCHNELL": (1024, 1024), + "FLUX_DEV": (1024, 1024), +} + +GUIDANCE_EMBED: dict[str, bool] = { + "SD15": False, + "SDXL": False, + "SDXL_TURBO": False, + "FLUX_SCHNELL": False, + "FLUX_DEV": True, +} + +DEFAULT_GUIDANCE: dict[str, float | None] = { + "SD15": None, + "SDXL": None, + "SDXL_TURBO": None, + "FLUX_SCHNELL": None, + "FLUX_DEV": 3.5, +} + +SAMPLING_PRESETS: dict[str, dict[str, dict[str, float | int]]] = { + "SD15": { + "Fast": {"steps": 12, "cfg": 6.5}, + "Balanced": {"steps": 20, "cfg": 7.0}, + "High Quality": {"steps": 30, "cfg": 7.5}, + }, + "SDXL": { + "Fast": {"steps": 16, "cfg": 5.5}, + "Balanced": {"steps": 24, "cfg": 6.0}, + "High Quality": {"steps": 36, "cfg": 6.5}, + }, + "SDXL_TURBO": { + "Fast": {"steps": 4, "cfg": 1.0}, + "Balanced": {"steps": 6, "cfg": 1.5}, + "High Quality": {"steps": 8, "cfg": 2.0}, + }, + "FLUX_SCHNELL": { + "Fast": {"steps": 4, "cfg": 1.0}, + "Balanced": {"steps": 6, "cfg": 1.0}, + "High Quality": {"steps": 8, "cfg": 1.0}, + }, + "FLUX_DEV": { + "Fast": {"steps": 18, "cfg": 1.0}, + "Balanced": {"steps": 28, "cfg": 1.0}, + "High Quality": {"steps": 36, "cfg": 1.0}, + }, +} + +_LEGACY_MODEL_INFO_PATTERN = re.compile(r"(\w+)=([^|]+)") + + +def canonicalize_family(family: str | None) -> str: + if family is None: + return "SD15" + normalized = str(family).strip() + if normalized == AUTO_FAMILY: + return "SD15" + return LEGACY_FAMILY_ALIASES.get(normalized, normalized or "SD15") + + +def infer_family_from_name(model_name: str | None, fallback: str = "SD15") -> str: + if not model_name: + return canonicalize_family(fallback) + + name = str(model_name).lower() + if "flux" in name: + if "schnell" in name: + return "FLUX_SCHNELL" + return "FLUX_DEV" + if "turbo" in name and "xl" in name: + return "SDXL_TURBO" + if any(token in name for token in ("sdxl", "_xl", "-xl", "xl_")): + return "SDXL" + return canonicalize_family(fallback) + + +def is_flux_family(family: str | None) -> bool: + return canonicalize_family(family) in {"FLUX_SCHNELL", "FLUX_DEV"} + + +def is_sdxl_family(family: str | None) -> bool: + return canonicalize_family(family) in {"SDXL", "SDXL_TURBO"} + + +def family_requires_guidance(family: str | None) -> bool: + return bool(GUIDANCE_EMBED.get(canonicalize_family(family), False)) + + +def get_latent_spec(model_info: dict[str, Any] | str | None) -> dict[str, int]: + info = parse_model_info(model_info) + family = info["family"] + return { + "latent_channels": int(info.get("latent_channels", LATENT_SPECS[family]["latent_channels"])), + "downscale_ratio": int(info.get("downscale_ratio", LATENT_SPECS[family]["downscale_ratio"])), + } + + +def get_sampling_preset(model_info: dict[str, Any] | str | None, quality_preset: str) -> dict[str, float | int] | None: + if quality_preset == "Manual": + return None + info = parse_model_info(model_info) + family = info["family"] + family_presets = SAMPLING_PRESETS.get(family, SAMPLING_PRESETS["SD15"]) + return family_presets.get(quality_preset) + + +def parse_model_info(model_info: dict[str, Any] | str | None) -> dict[str, Any]: + raw: dict[str, Any] = {} + + if isinstance(model_info, dict): + raw = dict(model_info) + elif isinstance(model_info, str): + stripped = model_info.strip() + if stripped: + try: + parsed = json.loads(stripped) + except json.JSONDecodeError: + raw = { + key: value.strip() + for key, value in _LEGACY_MODEL_INFO_PATTERN.findall(stripped) + } + else: + if isinstance(parsed, dict): + raw = dict(parsed) + + family = canonicalize_family(raw.get("family") or infer_family_from_name(raw.get("model_name") or raw.get("ckpt_name") or raw.get("ckpt"), "SD15")) + width, height = BASE_RESOLUTIONS.get(family, BASE_RESOLUTIONS["SD15"]) + latent_spec = LATENT_SPECS.get(family, LATENT_SPECS["SD15"]) + required_text_encoders = raw.get("required_text_encoders", REQUIRED_TEXT_ENCODERS.get(family, ["clip"])) + if isinstance(required_text_encoders, str): + required_text_encoders = [required_text_encoders] + + info: dict[str, Any] = { + "family": family, + "text_encoder_type": raw.get("text_encoder_type", TEXT_ENCODER_TYPES.get(family, "clip")), + "has_embedded_vae": bool(raw.get("has_embedded_vae", False)), + "required_text_encoders": list(required_text_encoders), + "required_vae": raw.get("required_vae", REQUIRED_VAE.get(family, "optional")), + "is_turbo": bool(raw.get("is_turbo", family == "SDXL_TURBO")), + "guidance_embed": bool(raw.get("guidance_embed", GUIDANCE_EMBED.get(family, False))), + "guidance": raw.get("guidance", DEFAULT_GUIDANCE.get(family)), + "latent_channels": int(raw.get("latent_channels", latent_spec["latent_channels"])), + "downscale_ratio": int(raw.get("downscale_ratio", latent_spec["downscale_ratio"])), + "base_width": int(raw.get("base_width", width)), + "base_height": int(raw.get("base_height", height)), + } + + for key in ( + "model_name", + "model_source", + "text_encoder_source", + "vae_source", + "load_mode", + "has_embedded_text_encoder", + ): + if key in raw: + info[key] = raw[key] + + return info + + +def build_model_info(**kwargs: Any) -> str: + info = parse_model_info(kwargs) + for key, value in kwargs.items(): + if value is not None: + info[key] = value + info["family"] = canonicalize_family(info.get("family")) + info.setdefault("text_encoder_type", TEXT_ENCODER_TYPES.get(info["family"], "clip")) + return json.dumps(info, ensure_ascii=True, sort_keys=True)