重新设计模型加载节点

This commit is contained in:
Gin3601
2026-05-22 16:54:14 +08:00
parent 5bea2122d0
commit d7aae497c2
5 changed files with 1654 additions and 73 deletions
+668
View File
@@ -0,0 +1,668 @@
{
"id": "dcac7a27-30f1-4cd1-a4a4-5b994457b3a3",
"revision": 0,
"last_node_id": 26,
"last_link_id": 20,
"nodes": [
{
"id": 20,
"type": "LoadImage",
"pos": [
80,
180
],
"size": [
340,
520
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"label": "IMAGE",
"slot_index": 0,
"links": [
1
]
},
{
"name": "MASK",
"type": "MASK",
"label": "MASK",
"slot_index": 1,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png",
"image",
""
]
},
{
"id": 21,
"type": "LLSUniversalModelLoader",
"pos": [
460,
80
],
"size": [
360,
260
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "model",
"type": "MODEL",
"slot_index": 0,
"links": [
3
]
},
{
"name": "text_encoder",
"type": "CLIP",
"slot_index": 1,
"links": [
4
]
},
{
"name": "vae",
"type": "VAE",
"slot_index": 2,
"links": [
5,
6
]
},
{
"name": "model_info",
"type": "STRING",
"slot_index": 3,
"links": [
7,
8,
9
]
}
],
"properties": {
"Node name for S&R": "LLSUniversalModelLoader"
},
"widgets_values": [
"diffusion_models/flux1-fill-dev.safetensors",
"Auto",
"simple",
"auto",
"auto",
"(auto)",
"(auto)",
"(auto)"
]
},
{
"id": 22,
"type": "LLSUniversalPromptEncode",
"pos": [
880,
80
],
"size": [
360,
190
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "text_encoder",
"type": "CLIP",
"link": 4,
"label": "text_encoder"
},
{
"name": "model_info",
"type": "STRING",
"link": 7,
"label": "model_info"
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
10
]
},
{
"name": "negative",
"type": "CONDITIONING",
"slot_index": 1,
"links": [
11
]
},
{
"name": "prompt_info",
"type": "STRING",
"slot_index": 2,
"links": [
12
]
}
],
"properties": {
"Node name for S&R": "LLSUniversalPromptEncode"
},
"widgets_values": [
"a small brown dog sitting in the masked area",
"",
-1
]
},
{
"id": 23,
"type": "LLSNativeInpaintConditioning",
"pos": [
880,
300
],
"size": [
400,
230
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 3,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 10,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 11,
"label": "negative"
},
{
"name": "vae",
"type": "VAE",
"link": 5,
"label": "vae"
},
{
"name": "image",
"type": "IMAGE",
"link": 1,
"label": "image"
},
{
"name": "mask",
"type": "MASK",
"link": 2,
"label": "mask"
},
{
"name": "model_info",
"type": "STRING",
"link": 8,
"label": "model_info"
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"slot_index": 0,
"links": [
13
]
},
{
"name": "positive",
"type": "CONDITIONING",
"slot_index": 1,
"links": [
14
]
},
{
"name": "negative",
"type": "CONDITIONING",
"slot_index": 2,
"links": [
15
]
},
{
"name": "latent",
"type": "LATENT",
"slot_index": 3,
"links": [
16
]
},
{
"name": "inpaint_info",
"type": "STRING",
"slot_index": 4,
"links": null
}
],
"properties": {
"Node name for S&R": "LLSNativeInpaintConditioning"
},
"widgets_values": [
"auto",
1.0,
true
]
},
{
"id": 24,
"type": "LLSSimpleKSampler",
"pos": [
1330,
220
],
"size": [
380,
320
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 13,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 14,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 15,
"label": "negative"
},
{
"name": "latent_image",
"type": "LATENT",
"link": 16,
"label": "latent_image"
},
{
"name": "repair_info",
"type": "LLS_REPAIR_INFO",
"link": null,
"label": "repair_info"
},
{
"name": "guidance_stack",
"type": "LLS_GUIDANCE_STACK",
"link": null,
"label": "guidance_stack"
},
{
"name": "model_info",
"type": "STRING",
"link": 9,
"label": "model_info"
}
],
"outputs": [
{
"name": "latent",
"type": "LATENT",
"slot_index": 0,
"links": [
17
]
},
{
"name": "sample_info",
"type": "STRING",
"slot_index": 1,
"links": [
18
]
}
],
"properties": {
"Node name for S&R": "LLSSimpleKSampler"
},
"widgets_values": [
"Family Default",
-1,
20,
7.0,
"euler",
"normal",
1.0,
"manual",
"auto",
3.5,
"Auto"
]
},
{
"id": 25,
"type": "LLSSimpleVAEDecode",
"pos": [
1750,
240
],
"size": [
270,
80
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 17,
"label": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": 6,
"label": "vae"
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"slot_index": 0,
"links": [
19
]
},
{
"name": "decode_info",
"type": "STRING",
"slot_index": 1,
"links": [
20
]
}
],
"properties": {
"Node name for S&R": "LLSSimpleVAEDecode"
},
"widgets_values": []
},
{
"id": 26,
"type": "LLSSaveImage",
"pos": [
2060,
180
],
"size": [
320,
220
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 19,
"label": "image"
},
{
"name": "prompt_info",
"type": "STRING",
"link": 12,
"label": "prompt_info"
},
{
"name": "latent_info",
"type": "STRING",
"link": null,
"label": "latent_info"
},
{
"name": "sample_info",
"type": "STRING",
"link": 18,
"label": "sample_info"
},
{
"name": "decode_info",
"type": "STRING",
"link": 20,
"label": "decode_info"
},
{
"name": "upscale_info",
"type": "STRING",
"link": null,
"label": "upscale_info"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "LLSSaveImage"
},
"widgets_values": [
"LLS-Inpaint",
"preview_only",
true
]
}
],
"links": [
[
1,
20,
0,
23,
4,
"IMAGE"
],
[
2,
20,
1,
23,
5,
"MASK"
],
[
3,
21,
0,
23,
0,
"MODEL"
],
[
4,
21,
1,
22,
0,
"CLIP"
],
[
5,
21,
2,
23,
3,
"VAE"
],
[
6,
21,
2,
25,
1,
"VAE"
],
[
7,
21,
3,
22,
1,
"STRING"
],
[
8,
21,
3,
23,
6,
"STRING"
],
[
9,
21,
3,
24,
6,
"STRING"
],
[
10,
22,
0,
23,
1,
"CONDITIONING"
],
[
11,
22,
1,
23,
2,
"CONDITIONING"
],
[
12,
22,
2,
26,
1,
"STRING"
],
[
13,
23,
0,
24,
0,
"MODEL"
],
[
14,
23,
1,
24,
1,
"CONDITIONING"
],
[
15,
23,
2,
24,
2,
"CONDITIONING"
],
[
16,
23,
3,
24,
3,
"LATENT"
],
[
17,
24,
0,
25,
0,
"LATENT"
],
[
18,
24,
1,
26,
3,
"STRING"
],
[
19,
25,
0,
26,
0,
"IMAGE"
],
[
20,
25,
1,
26,
4,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.0,
"offset": [
0.0,
0.0
]
}
},
"version": 0.4
}
+552
View File
@@ -0,0 +1,552 @@
{
"id": "0794c057-df8e-49ea-a3b6-8feed69f2af4",
"revision": 0,
"last_node_id": 15,
"last_link_id": 15,
"nodes": [
{
"id": 10,
"type": "LLSUniversalModelLoader",
"pos": [
80,
80
],
"size": [
360,
260
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "model",
"type": "MODEL",
"slot_index": 0,
"links": [
1,
11
]
},
{
"name": "text_encoder",
"type": "CLIP",
"slot_index": 1,
"links": [
2
]
},
{
"name": "vae",
"type": "VAE",
"slot_index": 2,
"links": [
3
]
},
{
"name": "model_info",
"type": "STRING",
"slot_index": 3,
"links": [
4,
5
]
}
],
"properties": {
"Node name for S&R": "LLSUniversalModelLoader"
},
"widgets_values": [
"diffusion_models/flux1-schnell.safetensors",
"Auto",
"simple",
"auto",
"auto",
"(auto)",
"(auto)",
"(auto)"
]
},
{
"id": 11,
"type": "LLSUniversalPromptEncode",
"pos": [
500,
80
],
"size": [
360,
190
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "text_encoder",
"type": "CLIP",
"link": 2,
"label": "text_encoder"
},
{
"name": "model_info",
"type": "STRING",
"link": 4,
"label": "model_info"
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
},
{
"name": "negative",
"type": "CONDITIONING",
"slot_index": 1,
"links": [
7
]
},
{
"name": "prompt_info",
"type": "STRING",
"slot_index": 2,
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "LLSUniversalPromptEncode"
},
"widgets_values": [
"a small brown dog standing on the grass",
"",
-1
]
},
{
"id": 12,
"type": "LLSSimpleEmptyLatent",
"pos": [
500,
320
],
"size": [
360,
230
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1,
"label": "model"
},
{
"name": "image",
"type": "IMAGE",
"link": null,
"label": "image"
},
{
"name": "vae",
"type": "VAE",
"link": null,
"label": "vae"
}
],
"outputs": [
{
"name": "latent",
"type": "LATENT",
"slot_index": 0,
"links": [
9
]
},
{
"name": "width",
"type": "INT",
"slot_index": 1,
"links": null
},
{
"name": "height",
"type": "INT",
"slot_index": 2,
"links": null
},
{
"name": "latent_info",
"type": "STRING",
"slot_index": 3,
"links": [
10
]
}
],
"properties": {
"Node name for S&R": "LLSSimpleEmptyLatent"
},
"widgets_values": [
"Family Default",
1024,
1024,
1,
"Auto",
"keep_aspect"
]
},
{
"id": 13,
"type": "LLSSimpleKSampler",
"pos": [
920,
180
],
"size": [
380,
320
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 11,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 6,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 7,
"label": "negative"
},
{
"name": "latent_image",
"type": "LATENT",
"link": 9,
"label": "latent_image"
},
{
"name": "repair_info",
"type": "LLS_REPAIR_INFO",
"link": null,
"label": "repair_info"
},
{
"name": "guidance_stack",
"type": "LLS_GUIDANCE_STACK",
"link": null,
"label": "guidance_stack"
},
{
"name": "model_info",
"type": "STRING",
"link": 5,
"label": "model_info"
}
],
"outputs": [
{
"name": "latent",
"type": "LATENT",
"slot_index": 0,
"links": [
12
]
},
{
"name": "sample_info",
"type": "STRING",
"slot_index": 1,
"links": [
13
]
}
],
"properties": {
"Node name for S&R": "LLSSimpleKSampler"
},
"widgets_values": [
"Family Default",
-1,
20,
7.0,
"euler",
"normal",
1.0,
"manual",
"auto",
3.5,
"Auto"
]
},
{
"id": 14,
"type": "LLSSimpleVAEDecode",
"pos": [
1330,
200
],
"size": [
270,
80
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 12,
"label": "samples"
},
{
"name": "vae",
"type": "VAE",
"link": 3,
"label": "vae"
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"slot_index": 0,
"links": [
14
]
},
{
"name": "decode_info",
"type": "STRING",
"slot_index": 1,
"links": [
15
]
}
],
"properties": {
"Node name for S&R": "LLSSimpleVAEDecode"
},
"widgets_values": []
},
{
"id": 15,
"type": "LLSSaveImage",
"pos": [
1640,
140
],
"size": [
320,
220
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 14,
"label": "image"
},
{
"name": "prompt_info",
"type": "STRING",
"link": 8,
"label": "prompt_info"
},
{
"name": "latent_info",
"type": "STRING",
"link": 10,
"label": "latent_info"
},
{
"name": "sample_info",
"type": "STRING",
"link": 13,
"label": "sample_info"
},
{
"name": "decode_info",
"type": "STRING",
"link": 15,
"label": "decode_info"
},
{
"name": "upscale_info",
"type": "STRING",
"link": null,
"label": "upscale_info"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "LLSSaveImage"
},
"widgets_values": [
"LLS",
"preview_only",
true
]
}
],
"links": [
[
1,
10,
0,
12,
0,
"MODEL"
],
[
2,
10,
1,
11,
0,
"CLIP"
],
[
3,
10,
2,
14,
1,
"VAE"
],
[
4,
10,
3,
11,
1,
"STRING"
],
[
5,
10,
3,
13,
6,
"STRING"
],
[
6,
11,
0,
13,
1,
"CONDITIONING"
],
[
7,
11,
1,
13,
2,
"CONDITIONING"
],
[
8,
11,
2,
15,
1,
"STRING"
],
[
9,
12,
0,
13,
3,
"LATENT"
],
[
10,
12,
3,
15,
2,
"STRING"
],
[
11,
10,
0,
13,
0,
"MODEL"
],
[
12,
13,
0,
14,
0,
"LATENT"
],
[
13,
13,
1,
15,
3,
"STRING"
],
[
14,
14,
0,
15,
0,
"IMAGE"
],
[
15,
14,
1,
15,
4,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.0,
"offset": [
0.0,
0.0
]
}
},
"version": 0.4
}
+103
View File
@@ -13,6 +13,7 @@ from ..utils.model_info import (
info_to_json,
is_flux_family,
is_sdxl_family,
parse_jsonish_info,
resolve_model_family,
)
@@ -221,10 +222,112 @@ class LLSSimplePromptEncode:
return (positive, negative, prompt_info)
class LLSUniversalPromptEncode:
"""
统一文本编码节点。
- 只公开一个 `text_encoder` 输入
- SDXL / FLUX 的双编码器在 Loader 内部组装,对下游透明
- 可选 `model_info` 用于明确 family / checkpoint / encoder metadata
"""
CATEGORY = "LLS/Conditioning"
FUNCTION = "encode"
RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "STRING")
RETURN_NAMES = ("positive", "negative", "prompt_info")
DESCRIPTION = (
"Encode prompts using one text_encoder input across SD1.5, SDXL, and FLUX families."
)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"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",),
},
}
def encode(
self,
text_encoder,
positive_prompt: str = "",
negative_prompt: str = "",
clip_skip=-1,
model_info=None,
):
info = parse_jsonish_info(model_info)
family_hint = str(info.get("model_family") or info.get("family") or "Auto")
family = resolve_model_family(family_hint, clip=text_encoder)
defaults = get_family_defaults(family)
clip_skip = _normalize_clip_skip(clip_skip)
encoder = text_encoder
if is_flux_family(family):
clip_skip = -1
if clip_skip != -1:
try:
encoder = encoder.clone()
encoder.clip_layer(clip_skip)
except Exception:
pass
negative_mode = "standard"
negative_for_encode = negative_prompt
if family == "SDXL_TURBO" and negative_prompt.strip():
negative_for_encode = ""
negative_mode = "weakened_for_turbo"
try:
if is_flux_family(family):
guidance = defaults.get("default_guidance")
positive = _encode_flux_prompt(encoder, positive_prompt, guidance)
negative = _encode_flux_prompt(encoder, "", guidance)
negative_mode = "ignored_for_flux"
elif is_sdxl_family(family):
width = int(defaults.get("default_width", 1024))
height = int(defaults.get("default_height", 1024))
positive = _encode_sdxl_prompt(encoder, positive_prompt, width, height)
negative = _encode_sdxl_prompt(encoder, negative_for_encode, width, height)
else:
positive = _encode_standard_prompt(encoder, positive_prompt)
negative = _encode_standard_prompt(encoder, negative_for_encode)
except Exception as exc:
raise RuntimeError(f"[LLS] Failed to encode prompts for family {family}: {exc}") from exc
prompt_mode = "flux" if is_flux_family(family) else "sdxl" if is_sdxl_family(family) else "clip"
prompt_info = info_to_json(
{
"model_family": family,
"checkpoint_name": info.get("checkpoint_name") or info.get("model_name") or "",
"text_encoder_type": info.get("text_encoder_type") or defaults.get("text_encoder_type", "clip"),
"text_encoder_name": info.get("text_encoder_name") or "",
"text_encoder_name_1": info.get("text_encoder_name_1") or "",
"text_encoder_name_2": info.get("text_encoder_name_2") or "",
"positive_prompt": positive_prompt,
"negative_prompt": negative_prompt,
"prompt_mode": prompt_mode,
"positive_prompt_length": len(positive_prompt),
"negative_prompt_length": len(negative_prompt),
"clip_skip": clip_skip,
"negative_mode": negative_mode,
"guidance": defaults.get("default_guidance") if is_flux_family(family) else None,
}
)
return (positive, negative, prompt_info)
NODE_CLASS_MAPPINGS: dict[str, type] = {
"LLSSimplePromptEncode": LLSSimplePromptEncode,
"LLSUniversalPromptEncode": LLSUniversalPromptEncode,
}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
"LLSSimplePromptEncode": "LLS Simple Prompt Encode",
"LLSUniversalPromptEncode": "LLS Universal Prompt Encode",
}
+175 -73
View File
@@ -39,6 +39,7 @@ from ..utils.model_info import (
MODEL_FAMILY_CHOICES,
canonicalize_family,
get_family_defaults,
info_to_json,
infer_family_from_name,
is_flux_family,
is_sdxl_family,
@@ -53,6 +54,8 @@ NO_TEXT_ENCODER_PLACEHOLDER = "(no text encoders found)"
LOAD_MODE_CHOICES = ["simple", "advanced"]
VAE_SOURCE_CHOICES = ["auto", "embedded", "external", "none"]
TEXT_ENCODER_SOURCE_CHOICES = ["auto", "embedded", "external", "manual"]
UNIVERSAL_VAE_SOURCE_CHOICES = ["auto", "embedded", "external"]
UNIVERSAL_TEXT_ENCODER_SOURCE_CHOICES = ["auto", "embedded", "external"]
_FLUX_CLIP_L_PATTERNS = ("clip_l.safetensors", "clip_l", "clip-l")
_FLUX_T5_PATTERNS = ("t5xxl_fp8_e4m3fn.safetensors", "t5xxl_fp16.safetensors", "t5xxl", "t5")
@@ -371,6 +374,120 @@ def _resolve_vae(
return None, "none", None
def _resolve_loader_family(model_name: str, model_family: str) -> str:
if model_family == "Auto":
return infer_family_from_name(model_name, "SD1.5")
return canonicalize_family(model_family)
def _load_tagged_resources(
*,
model_name: str,
model_family: str,
load_mode: str,
vae_source: str,
text_encoder_source: str,
vae_name: str,
text_encoder_name_1: str,
text_encoder_name_2: str,
):
if folder_paths is None:
raise RuntimeError(
"[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 ComfyUI."
) from _COMFY_SD_ERR
family = _resolve_loader_family(model_name, model_family)
model_source, model_path = _resolve_model_path(model_name, family)
model, embedded_text_encoder, embedded_vae = _load_model(model_source, model_path)
if model is None:
raise RuntimeError(f"[LLS] The selected model '{model_name}' did not produce a valid diffusion model.")
external_vae_name = _normalize_choice(vae_name, NO_VAE_PLACEHOLDER)
external_text_encoder_1 = _normalize_choice(text_encoder_name_1, NO_TEXT_ENCODER_PLACEHOLDER)
external_text_encoder_2 = _normalize_choice(text_encoder_name_2, NO_TEXT_ENCODER_PLACEHOLDER)
text_encoder, resolved_text_encoder_source, resolved_te_1, resolved_te_2 = _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, resolved_vae_name = _resolve_vae(
family=family,
source=vae_source,
embedded_vae=embedded_vae,
external_vae_name=external_vae_name,
)
defaults = get_family_defaults(family)
capability_tags = _build_capability_tags(model_name, family)
resolved_text_encoder_name = resolved_te_1 or ("embedded" if text_encoder is not None else None)
resolved_text_encoder_name_1 = resolved_te_1 or ("embedded" if text_encoder is not None and not resolved_te_2 else None)
resolved_text_encoder_name_2 = resolved_te_2
if resolved_te_1 and resolved_te_2:
resolved_text_encoder_name = ", ".join([resolved_te_1, resolved_te_2])
resolved_vae_label = resolved_vae_name or ("embedded" if vae is not None else None)
tag_lls_object(
model,
family=family,
model_name=model_name,
checkpoint_name=model_name,
load_mode=load_mode,
**capability_tags,
)
tag_lls_object(
text_encoder,
family=family,
model_name=model_name,
checkpoint_name=model_name,
text_encoder_type=defaults["text_encoder_type"],
text_encoder_source=resolved_text_encoder_source,
text_encoder_name=resolved_text_encoder_name,
text_encoder_name_1=resolved_text_encoder_name_1,
text_encoder_name_2=resolved_text_encoder_name_2,
**capability_tags,
)
tag_lls_object(
vae,
family=family,
model_name=model_name,
checkpoint_name=model_name,
vae_name=resolved_vae_label,
vae_source=resolved_vae_source,
**capability_tags,
)
model_info = {
"model_family": family,
"checkpoint_name": model_name,
"model_name": model_name,
"load_mode": load_mode,
"model_source": model_source,
"profile_id": capability_tags["profile_id"],
"backend_type": capability_tags["backend_type"],
"sampler_strategy": capability_tags["sampler_strategy"],
"loader_strategy": capability_tags["loader_strategy"],
"model_role": capability_tags["model_role"],
"supports_inpaint_native": capability_tags["supports_inpaint_native"],
"supports_image_edit_native": capability_tags["supports_image_edit_native"],
"preferred_edit_backend": capability_tags["preferred_edit_backend"],
"text_encoder_type": defaults["text_encoder_type"],
"text_encoder_source": resolved_text_encoder_source,
"text_encoder_name": resolved_text_encoder_name or "",
"text_encoder_name_1": resolved_text_encoder_name_1 or "",
"text_encoder_name_2": resolved_text_encoder_name_2 or "",
"vae_name": resolved_vae_label or "",
"vae_source": resolved_vae_source,
}
return model, text_encoder, vae, model_info
class LLSSimpleCheckpointLoader:
"""
统一封装的基础模型加载器。
@@ -418,89 +535,74 @@ class LLSSimpleCheckpointLoader:
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 ComfyUI."
) from _FOLDER_PATHS_ERR
if comfy_sd is None:
raise RuntimeError(
"[LLS] comfy.sd is not available. Make sure this node runs inside ComfyUI."
) from _COMFY_SD_ERR
if model_family == "Auto":
family = infer_family_from_name(ckpt_name, "SD1.5")
else:
family = canonicalize_family(model_family)
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] The selected model '{ckpt_name}' did not produce a valid diffusion model.")
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)
text_encoder, resolved_text_encoder_source, resolved_te_1, resolved_te_2 = _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, resolved_vae_name = _resolve_vae(
family=family,
source=vae_source,
embedded_vae=embedded_vae,
external_vae_name=external_vae_name,
)
defaults = get_family_defaults(family)
capability_tags = _build_capability_tags(ckpt_name, family)
resolved_text_encoder_name = resolved_te_1 or ("embedded" if text_encoder is not None else None)
resolved_text_encoder_name_1 = resolved_te_1 or ("embedded" if text_encoder is not None and not resolved_te_2 else None)
resolved_text_encoder_name_2 = resolved_te_2
if resolved_te_1 and resolved_te_2:
resolved_text_encoder_name = ", ".join([resolved_te_1, resolved_te_2])
resolved_vae_label = resolved_vae_name or ("embedded" if vae is not None else None)
tag_lls_object(
model,
family=family,
model, text_encoder, vae, _model_info = _load_tagged_resources(
model_name=ckpt_name,
checkpoint_name=ckpt_name,
model_family=model_family,
load_mode=load_mode,
**capability_tags,
vae_source=vae_source,
text_encoder_source=text_encoder_source,
vae_name=external_vae_name,
text_encoder_name_1=external_text_encoder_1,
text_encoder_name_2=external_text_encoder_2,
)
tag_lls_object(
text_encoder,
family=family,
model_name=ckpt_name,
checkpoint_name=ckpt_name,
text_encoder_type=defaults["text_encoder_type"],
text_encoder_source=resolved_text_encoder_source,
text_encoder_name=resolved_text_encoder_name,
text_encoder_name_1=resolved_text_encoder_name_1,
text_encoder_name_2=resolved_text_encoder_name_2,
**capability_tags,
)
tag_lls_object(
vae,
family=family,
model_name=ckpt_name,
checkpoint_name=ckpt_name,
vae_name=resolved_vae_label,
vae_source=resolved_vae_source,
**capability_tags,
)
return (model, text_encoder, vae, text_encoder)
class LLSUniversalModelLoader:
CATEGORY = "LLS/Model Loader"
FUNCTION = "load"
RETURN_TYPES = ("MODEL", "CLIP", "VAE", "STRING")
RETURN_NAMES = ("model", "text_encoder", "vae", "model_info")
DESCRIPTION = (
"Unified model loader for SD1.5, SDXL, and FLUX families. "
"Internally resolves single or dual text encoders but always exposes one text_encoder output."
)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_name": (_get_model_name_choices(),),
"model_family": (MODEL_FAMILY_CHOICES, {"default": "Auto"}),
"load_mode": (LOAD_MODE_CHOICES, {"default": "simple"}),
"vae_source": (UNIVERSAL_VAE_SOURCE_CHOICES, {"default": "auto"}),
"text_encoder_source": (UNIVERSAL_TEXT_ENCODER_SOURCE_CHOICES, {"default": "auto"}),
"text_encoder_1": (_get_text_encoder_choices(), {"default": AUTO_PLACEHOLDER}),
"text_encoder_2": (_get_text_encoder_choices(), {"default": AUTO_PLACEHOLDER}),
"vae_name": (_get_vae_choices(), {"default": AUTO_PLACEHOLDER}),
},
}
def load(
self,
model_name: str,
model_family: str,
load_mode: str,
vae_source: str,
text_encoder_source: str,
text_encoder_1: str,
text_encoder_2: str,
vae_name: str,
):
model, text_encoder, vae, model_info = _load_tagged_resources(
model_name=model_name,
model_family=model_family,
load_mode=load_mode,
vae_source=vae_source,
text_encoder_source=text_encoder_source,
vae_name=vae_name,
text_encoder_name_1=text_encoder_1,
text_encoder_name_2=text_encoder_2,
)
return model, text_encoder, vae, info_to_json(model_info)
NODE_CLASS_MAPPINGS: dict[str, type] = {
"LLSSimpleCheckpointLoader": LLSSimpleCheckpointLoader,
"LLSUniversalModelLoader": LLSUniversalModelLoader,
}
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
"LLSSimpleCheckpointLoader": "LLS Simple Checkpoint Loader",
"LLSUniversalModelLoader": "LLS Universal Model Loader",
}
+156
View File
@@ -297,6 +297,35 @@ class TestLoaderPromptRefactor(unittest.TestCase):
)
self.assertNotIn("optional", node_cls.INPUT_TYPES())
def test_universal_loader_schema_exposes_single_text_encoder_output(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSUniversalModelLoader"]
required = node_cls.INPUT_TYPES()["required"]
self.assertEqual(
node_cls.RETURN_TYPES,
("MODEL", "CLIP", "VAE", "STRING"),
)
self.assertEqual(
node_cls.RETURN_NAMES,
("model", "text_encoder", "vae", "model_info"),
)
self.assertEqual(
tuple(required.keys()),
(
"model_name",
"model_family",
"load_mode",
"vae_source",
"text_encoder_source",
"text_encoder_1",
"text_encoder_2",
"vae_name",
),
)
self.assertEqual(required["model_family"][1]["default"], "Auto")
self.assertEqual(required["text_encoder_source"][1]["default"], "auto")
def test_loader_tags_sd15_resources_without_context_output(self):
load_plugin_package()
from lls_node_test_refactor.model_loader import nodes as loader_nodes
@@ -328,6 +357,36 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(clip._lls_text_encoder_type, "clip")
self.assertEqual(vae._lls_family, "SD1.5")
def test_universal_loader_loads_sd15_into_single_text_encoder_port(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.LLSUniversalModelLoader()
model, text_encoder, vae, model_info = node.load(
"sd15.safetensors",
"SD1.5",
"simple",
"auto",
"auto",
"(auto)",
"(auto)",
"(auto)",
)
payload = json.loads(model_info)
self.assertEqual(model.label, "MODEL::SD15")
self.assertEqual(text_encoder.label, "CLIP::SD15")
self.assertEqual(vae.label, "VAE::SD15")
self.assertEqual(payload["model_family"], "SD1.5")
self.assertEqual(payload["checkpoint_name"], "sd15.safetensors")
self.assertEqual(payload["text_encoder_source"], "embedded")
self.assertEqual(payload["vae_source"], "embedded")
def test_loader_tags_flux_resources_and_keeps_standard_text_encoder_alias(self):
load_plugin_package()
from lls_node_test_refactor.model_loader import nodes as loader_nodes
@@ -358,6 +417,37 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(clip._lls_model_name, "diffusion_models/flux1-schnell.safetensors")
self.assertEqual(vae._lls_vae_name, "ae.safetensors")
def test_universal_loader_loads_flux_dual_encoders_into_single_text_encoder_port(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.LLSUniversalModelLoader()
model, text_encoder, vae, model_info = node.load(
"diffusion_models/flux1-schnell.safetensors",
"FLUX_SCHNELL",
"advanced",
"external",
"external",
"clip_l.safetensors",
"t5xxl_fp16.safetensors",
"ae.safetensors",
)
payload = json.loads(model_info)
self.assertEqual(model.label, "MODEL::flux1-schnell.safetensors")
self.assertEqual(text_encoder.label, "CLIP::flux::clip_l.safetensors+t5xxl_fp16.safetensors")
self.assertEqual(vae.label, "VAE::ae.safetensors")
self.assertEqual(payload["model_family"], "FLUX_SCHNELL")
self.assertEqual(payload["text_encoder_name_1"], "clip_l.safetensors")
self.assertEqual(payload["text_encoder_name_2"], "t5xxl_fp16.safetensors")
self.assertEqual(payload["text_encoder_name"], "clip_l.safetensors, t5xxl_fp16.safetensors")
self.assertEqual(payload["vae_name"], "ae.safetensors")
def test_loader_raises_clear_error_when_flux_text_encoder_is_missing(self):
load_plugin_package()
from lls_node_test_refactor.model_loader import nodes as loader_nodes
@@ -394,6 +484,18 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(optional["clip"][0], "CLIP")
self.assertNotIn("task_context", optional)
def test_universal_prompt_encode_schema_requires_single_text_encoder_input(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSUniversalPromptEncode"]
required = node_cls.INPUT_TYPES()["required"]
optional = node_cls.INPUT_TYPES()["optional"]
self.assertEqual(node_cls.RETURN_TYPES, ("CONDITIONING", "CONDITIONING", "STRING"))
self.assertEqual(required["text_encoder"][0], "CLIP")
self.assertEqual(required["clip_skip"][1]["default"], -1)
self.assertNotIn("clip", required)
self.assertEqual(optional["model_info"][0], "STRING")
def test_prompt_encode_dispatches_sdxl_using_inferred_family_defaults(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimplePromptEncode"]
@@ -451,6 +553,60 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(positive[0][0], "cond::a robot in a forest")
self.assertEqual(negative[0][0], "cond::")
def test_universal_prompt_encode_dispatches_sdxl_from_model_info(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSUniversalPromptEncode"]
node = node_cls()
clip = RecordingClip(("g", "l"))
positive, negative, prompt_info = node.encode(
text_encoder=clip,
positive_prompt="a castle",
negative_prompt="low quality",
clip_skip=-1,
model_info=json.dumps({"model_family": "SDXL", "checkpoint_name": "sdxl_turbo.safetensors"}),
)
self.assertEqual(len(clip.calls), 2)
self.assertEqual(clip.calls[0]["add_dict"]["width"], 1024)
payload = json.loads(prompt_info)
self.assertEqual(payload["model_family"], "SDXL")
self.assertEqual(payload["prompt_mode"], "sdxl")
self.assertEqual(payload["checkpoint_name"], "sdxl_turbo.safetensors")
self.assertEqual(positive[0][0], "cond::a castle")
self.assertEqual(negative[0][0], "cond::low quality")
def test_universal_prompt_encode_dispatches_flux_from_model_info(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSUniversalPromptEncode"]
node = node_cls()
clip = RecordingClip(("t5xxl",))
positive, negative, prompt_info = node.encode(
text_encoder=clip,
positive_prompt="a robot in a forest",
negative_prompt="ugly, blurry",
clip_skip=-1,
model_info=json.dumps(
{
"model_family": "FLUX_DEV",
"checkpoint_name": "diffusion_models/flux1-schnell.safetensors",
"text_encoder_name": "clip_l.safetensors, t5xxl_fp16.safetensors",
}
),
)
self.assertEqual(len(clip.calls), 2)
self.assertEqual(clip.calls[0]["add_dict"]["guidance"], 3.5)
self.assertEqual(clip.calls[1]["tokens"]["text"], "")
payload = json.loads(prompt_info)
self.assertEqual(payload["model_family"], "FLUX_DEV")
self.assertEqual(payload["prompt_mode"], "flux")
self.assertEqual(payload["checkpoint_name"], "diffusion_models/flux1-schnell.safetensors")
self.assertEqual(payload["text_encoder_name"], "clip_l.safetensors, t5xxl_fp16.safetensors")
self.assertEqual(positive[0][0], "cond::a robot in a forest")
self.assertEqual(negative[0][0], "cond::")
def test_empty_latent_uses_model_family_inference_for_flux_shape(self):
load_plugin_package()
from lls_node_test_refactor.latent import nodes as latent_nodes