修改替换背景节点逻辑

This commit is contained in:
Gin3601
2026-05-26 17:54:17 +08:00
parent 8f1fc69dda
commit 58af4586ef
15 changed files with 7598 additions and 2511 deletions
-831
View File
@@ -1,831 +0,0 @@
{
"id": "b6958508-1473-44fa-bd93-03897b3bb6cb",
"revision": 0,
"last_node_id": 87,
"last_link_id": 166,
"nodes": [
{
"id": 78,
"type": "ShowText|pysssss",
"pos": [
498,
2633
],
"size": [
492.36199951171875,
169.55548095703125
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"label": "text",
"name": "text",
"type": "STRING",
"link": 164
}
],
"outputs": [
{
"label": "text",
"name": "text",
"shape": 6,
"type": "STRING"
}
],
"properties": {
"Node name for S&R": "ShowText|pysssss",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 72,
"type": "KSampler",
"pos": [
1780,
1860
],
"size": [
315,
262
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"label": "model",
"name": "model",
"type": "MODEL",
"link": 142
},
{
"label": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 129
},
{
"label": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 130
},
{
"label": "latent_image",
"name": "latent_image",
"type": "LATENT",
"link": 145
}
],
"outputs": [
{
"label": "LATENT",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
137
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"widget_ue_connectable": {}
},
"widgets_values": [
479945352329433,
"randomize",
30,
1,
"euler",
"simple",
1
]
},
{
"id": 75,
"type": "CLIPTextEncode",
"pos": [
1510,
1930
],
"size": [
210.29940795898438,
88
],
"flags": {
"collapsed": false
},
"order": 6,
"mode": 0,
"inputs": [
{
"label": "clip",
"name": "clip",
"type": "CLIP",
"link": 138
}
],
"outputs": [
{
"label": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
139
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"widget_ue_connectable": {}
},
"widgets_values": [
""
]
},
{
"id": 69,
"type": "CLIPTextEncodeFlux",
"pos": [
1500,
1730
],
"size": [
210,
166
],
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"label": "clip",
"name": "clip",
"type": "CLIP",
"link": 144
},
{
"label": "t5xxl",
"name": "t5xxl",
"type": "STRING",
"widget": {
"name": "t5xxl"
},
"link": 165
}
],
"outputs": [
{
"label": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
120
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncodeFlux",
"widget_ue_connectable": {
"t5xxl": true
}
},
"widgets_values": [
"",
"",
3.5
]
},
{
"id": 76,
"type": "DifferentialDiffusion",
"pos": [
1500,
1610
],
"size": [
210,
58
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"label": "model",
"name": "model",
"type": "MODEL",
"link": 141
}
],
"outputs": [
{
"label": "MODEL",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
142
]
}
],
"properties": {
"Node name for S&R": "DifferentialDiffusion",
"widget_ue_connectable": {}
},
"widgets_values": [
1
]
},
{
"id": 70,
"type": "InpaintModelConditioning",
"pos": [
1780,
1600
],
"size": [
315,
138
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"label": "positive",
"name": "positive",
"type": "CONDITIONING",
"link": 120
},
{
"label": "negative",
"name": "negative",
"type": "CONDITIONING",
"link": 139
},
{
"label": "vae",
"name": "vae",
"type": "VAE",
"link": 122
},
{
"label": "pixels",
"name": "pixels",
"type": "IMAGE",
"link": 123
},
{
"label": "mask",
"name": "mask",
"type": "MASK",
"link": 124
}
],
"outputs": [
{
"label": "positive",
"name": "positive",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
129
]
},
{
"label": "negative",
"name": "negative",
"type": "CONDITIONING",
"slot_index": 1,
"links": [
130
]
},
{
"label": "latent",
"name": "latent",
"type": "LATENT",
"slot_index": 2,
"links": [
145
]
}
],
"properties": {
"Node name for S&R": "InpaintModelConditioning",
"widget_ue_connectable": {}
},
"widgets_values": [
false
]
},
{
"id": 60,
"type": "Note",
"pos": [
232,
1464
],
"size": [
210,
140
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"text": "",
"widget_ue_connectable": {}
},
"widgets_values": [
"1.上传图片后右键\n2.在遮罩编辑器中打开\n3.涂抹想要重绘的区域\n4.点击cave或者确定\n5.按住右键涂抹可以去掉涂错的地方"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 30,
"type": "LoadImage",
"pos": [
510,
1504
],
"size": [
470.2230224609375,
704.8888549804688
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
123
]
},
{
"label": "MASK",
"name": "MASK",
"type": "MASK",
"slot_index": 1,
"links": [
124
]
}
],
"properties": {
"Node name for S&R": "LoadImage",
"widget_ue_connectable": {}
},
"widgets_values": [
"clipspace/5c52c8440e713f6489a038f8c25e4f48e3dee18a3945147a28c7631104ea3f9c.png [input]",
"image"
]
},
{
"id": 46,
"type": "DualCLIPLoader",
"pos": [
1130,
1810
],
"size": [
315,
130
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "CLIP",
"name": "CLIP",
"type": "CLIP",
"slot_index": 0,
"links": [
138,
144
]
}
],
"properties": {
"Node name for S&R": "DualCLIPLoader",
"widget_ue_connectable": {}
},
"widgets_values": [
"clip_l.safetensors",
"t5xxl_fp8_e4m3fn.safetensors",
"flux",
"default"
]
},
{
"id": 54,
"type": "VAELoader",
"pos": [
1130,
2050
],
"size": [
315,
58
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 0,
"links": [
79,
122
]
}
],
"properties": {
"Node name for S&R": "VAELoader",
"widget_ue_connectable": {}
},
"widgets_values": [
"ae.sft"
]
},
{
"id": 45,
"type": "UNETLoader",
"pos": [
1130,
1620
],
"size": [
315,
82
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "MODEL",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
141
]
}
],
"properties": {
"Node name for S&R": "UNETLoader",
"widget_ue_connectable": {}
},
"widgets_values": [
"fluxFillFP8_v10.safetensors",
"fp8_e4m3fn"
]
},
{
"id": 53,
"type": "VAEDecode",
"pos": [
1510,
2070
],
"size": [
210,
46
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"label": "samples",
"name": "samples",
"type": "LATENT",
"link": 137
},
{
"label": "vae",
"name": "vae",
"type": "VAE",
"link": 79
}
],
"outputs": [
{
"label": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
166
]
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 87,
"type": "SaveImage",
"pos": [
2134,
1526
],
"size": [
459.5748291015625,
661.1332397460938
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"label": "images",
"name": "images",
"type": "IMAGE",
"link": 166
}
],
"outputs": [
{
"label": "image_urls",
"name": "image_urls",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "SaveImage",
"widget_ue_connectable": {}
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 86,
"type": "ArgosTranslateTextNode",
"pos": [
513,
2359
],
"size": [
469.2802429199219,
199.02728271484375
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"label": "text",
"name": "text",
"type": "STRING",
"slot_index": 0,
"links": [
164,
165
]
}
],
"properties": {
"Node name for S&R": "ArgosTranslateTextNode",
"widget_ue_connectable": {}
},
"widgets_values": [
"chinese",
"english",
"手镯"
]
}
],
"links": [
[
79,
54,
0,
53,
1,
"VAE"
],
[
120,
69,
0,
70,
0,
"CONDITIONING"
],
[
122,
54,
0,
70,
2,
"VAE"
],
[
123,
30,
0,
70,
3,
"IMAGE"
],
[
124,
30,
1,
70,
4,
"MASK"
],
[
129,
70,
0,
72,
1,
"CONDITIONING"
],
[
130,
70,
1,
72,
2,
"CONDITIONING"
],
[
137,
72,
0,
53,
0,
"LATENT"
],
[
138,
46,
0,
75,
0,
"CLIP"
],
[
139,
75,
0,
70,
1,
"CONDITIONING"
],
[
141,
45,
0,
76,
0,
"MODEL"
],
[
142,
76,
0,
72,
0,
"MODEL"
],
[
144,
46,
0,
69,
0,
"CLIP"
],
[
145,
70,
2,
72,
3,
"LATENT"
],
[
164,
86,
0,
78,
0,
"STRING"
],
[
165,
86,
0,
69,
1,
"STRING"
],
[
166,
53,
0,
87,
0,
"IMAGE"
]
],
"groups": [
{
"id": 1,
"title": "FLUX",
"bounding": [
1063,
1433,
1577,
796
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "上传图片并进入遮罩涂抹需要修饰的地方",
"bounding": [
475,
1426,
534,
795
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "输入中文提示词例如:口红、花环",
"bounding": [
472,
2267,
535,
574
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ue_links": [],
"0246.VERSION": [
0,
0,
4
],
"ds": {
"scale": 0.6588450000000002,
"offset": [
460.77367154024205,
-1317.5880656092918
]
},
"frontendVersion": "1.23.0",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
@@ -0,0 +1,917 @@
{
"id": "ef9bb8d6-183c-41ad-a0d0-f13d4dba3839",
"revision": 0,
"last_node_id": 15,
"last_link_id": 18,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [
40,
420
],
"size": [
270,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1,
2
]
},
{
"name": "MASK",
"type": "MASK",
"links": []
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"ChatGPT Image 2026年5月25日 18_14_30.png",
"image"
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
40,
60
],
"size": [
270,
314
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
3
]
},
{
"name": "MASK",
"type": "MASK",
"links": []
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"c580bea1ee8fcdc6b9663fada5540d6ee4a780ce9a8e309d63428a65a6a6821d.jpg",
"image"
]
},
{
"id": 3,
"type": "RMBG",
"pos": [
380,
430
],
"size": [
340,
292
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": []
},
{
"name": "MASK",
"type": "MASK",
"links": [
4
]
},
{
"name": "MASK_IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"cnr_id": "comfyui-rmbg",
"ver": "d7402513f23f58db7d56754b02a4f51a148b4941",
"Node name for S&R": "RMBG"
},
"widgets_values": [
"RMBG-2.0",
1,
1024,
0,
0,
false,
false,
"Alpha",
"#222222"
],
"color": "#222e40",
"bgcolor": "#364254"
},
{
"id": 4,
"type": "InvertMask",
"pos": [
780,
530
],
"size": [
140,
26
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 4
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
5
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "InvertMask"
}
},
{
"id": 5,
"type": "UNETLoader",
"pos": [
40,
860
],
"size": [
270,
82
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"label": "unet_name",
"name": "unet_name",
"type": "COMBO",
"widget": {
"name": "unet_name"
}
},
{
"label": "weight_dtype",
"name": "weight_dtype",
"type": "COMBO",
"widget": {
"name": "weight_dtype"
}
}
],
"outputs": [
{
"label": "MODEL",
"name": "MODEL",
"type": "MODEL",
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "UNETLoader",
"widget_ue_connectable": {}
},
"widgets_values": [
"flux-2-klein-9b.safetensors",
"default"
]
},
{
"id": 6,
"type": "CLIPLoader",
"pos": [
40,
970
],
"size": [
270,
106
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"label": "clip_name",
"name": "clip_name",
"type": "COMBO",
"widget": {
"name": "clip_name"
}
},
{
"label": "type",
"name": "type",
"type": "COMBO",
"widget": {
"name": "type"
}
},
{
"label": "device",
"name": "device",
"shape": 7,
"type": "COMBO",
"widget": {
"name": "device"
}
}
],
"outputs": [
{
"label": "CLIP",
"name": "CLIP",
"type": "CLIP",
"links": [
9
]
}
],
"properties": {
"Node name for S&R": "CLIPLoader",
"widget_ue_connectable": {}
},
"widgets_values": [
"qwen_3_8b.safetensors",
"flux2",
"cpu"
]
},
{
"id": 7,
"type": "VAELoader",
"pos": [
40,
1110
],
"size": [
330,
60
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"label": "vae_name",
"name": "vae_name",
"type": "COMBO",
"widget": {
"name": "vae_name"
}
}
],
"outputs": [
{
"label": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 0,
"links": [
10,
11
]
}
],
"properties": {
"Node name for S&R": "VAELoader",
"cnr_id": "comfy-core",
"ver": "0.3.48",
"widget_ue_connectable": {}
},
"widgets_values": [
"flux2-vae.safetensors"
]
},
{
"id": 8,
"type": "ModelSamplingAuraFlow",
"pos": [
380,
860
],
"size": [
246.1006317138672,
58
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"label": "model",
"name": "model",
"type": "MODEL",
"link": 6
},
{
"label": "shift",
"name": "shift",
"type": "FLOAT",
"widget": {
"name": "shift"
}
}
],
"outputs": [
{
"label": "MODEL",
"name": "MODEL",
"type": "MODEL",
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "ModelSamplingAuraFlow",
"cnr_id": "comfy-core",
"ver": "0.3.48",
"widget_ue_connectable": {}
},
"widgets_values": [
3
]
},
{
"id": 9,
"type": "CFGNorm",
"pos": [
680,
860
],
"size": [
248.726806640625,
58
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"label": "model",
"name": "model",
"type": "MODEL",
"link": 7
},
{
"label": "strength",
"name": "strength",
"type": "FLOAT",
"widget": {
"name": "strength"
}
}
],
"outputs": [
{
"label": "patched_model",
"name": "patched_model",
"type": "MODEL",
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "CFGNorm",
"cnr_id": "comfy-core",
"ver": "0.3.50",
"widget_ue_connectable": {}
},
"widgets_values": [
1
]
},
{
"id": 10,
"type": "LLSFlux2KleinEditTextEncode",
"pos": [
1040,
240
],
"size": [
400,
260
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 9
},
{
"name": "vae",
"type": "VAE",
"link": 10
},
{
"name": "image1",
"type": "IMAGE",
"link": 2
},
{
"name": "image2",
"shape": 7,
"type": "IMAGE",
"link": 3
},
{
"name": "image3",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": 5
}
],
"outputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"links": [
12,
13
]
},
{
"name": "latent",
"type": "LATENT",
"links": [
14
]
},
{
"name": "custom_output",
"type": "LLS_FLUX2KLEIN_OUTPUT",
"links": null
},
{
"name": "main_image",
"type": "IMAGE",
"links": null
},
{
"name": "mask",
"type": "MASK",
"links": null
}
],
"properties": {
"aux_id": "Gin3601/LLS-node",
"ver": "f6bf21e439db74bf21d96157e231dbc99b45fc88",
"Node name for S&R": "LLSFlux2KleinEditTextEncode"
},
"widgets_values": [
"Keep the product unchanged. Replace the plain background with a premium e-commerce environment that matches the product material, lighting, and usage context. Preserve the product silhouette, logo, label, and camera angle. Photorealistic, clean composition, ad-quality detail.",
1024,
"longest_edge",
"use_mask"
]
},
{
"id": 11,
"type": "ConditioningZeroOut",
"pos": [
1490,
350
],
"size": [
197.712890625,
26
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"label": "conditioning",
"name": "conditioning",
"type": "CONDITIONING",
"link": 13
}
],
"outputs": [
{
"label": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
15
]
}
],
"properties": {
"Node name for S&R": "ConditioningZeroOut",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 12,
"type": "KSampler",
"pos": [
1500,
740
],
"size": [
270,
262
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 8
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 12
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 15
},
{
"name": "latent_image",
"type": "LATENT",
"link": 14
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
16
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "KSampler"
},
"widgets_values": [
34227918990123,
"randomize",
20,
4,
"euler",
"simple",
1
]
},
{
"id": 13,
"type": "VAEDecode",
"pos": [
1840,
820
],
"size": [
140,
46
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 16
},
{
"name": "vae",
"type": "VAE",
"link": 11
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
17,
18
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 14,
"type": "PreviewImage",
"pos": [
2050,
720
],
"size": [
296.69873046875,
261.48797607421875
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 17
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.21.0",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 15,
"type": "SaveImage",
"pos": [
2050,
1010
],
"size": [
270,
270
],
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
}
}
],
"outputs": [
{
"name": "image_urls",
"type": "STRING"
}
],
"properties": {
"Node name for S&R": "SaveImage",
"widget_ue_connectable": {}
},
"widgets_values": [
"LLS-Flux2Klein-Background"
]
}
],
"links": [
[
1,
1,
0,
3,
0,
"IMAGE"
],
[
2,
1,
0,
10,
2,
"IMAGE"
],
[
3,
2,
0,
10,
3,
"IMAGE"
],
[
4,
3,
1,
4,
0,
"MASK"
],
[
5,
4,
0,
10,
5,
"MASK"
],
[
6,
5,
0,
8,
0,
"MODEL"
],
[
7,
8,
0,
9,
0,
"MODEL"
],
[
8,
9,
0,
12,
0,
"MODEL"
],
[
9,
6,
0,
10,
0,
"CLIP"
],
[
10,
7,
0,
10,
1,
"VAE"
],
[
11,
7,
0,
13,
1,
"VAE"
],
[
12,
10,
0,
12,
1,
"CONDITIONING"
],
[
13,
10,
0,
11,
0,
"CONDITIONING"
],
[
14,
10,
1,
12,
3,
"LATENT"
],
[
15,
11,
0,
12,
2,
"CONDITIONING"
],
[
16,
12,
0,
13,
0,
"LATENT"
],
[
17,
13,
0,
14,
0,
"IMAGE"
],
[
18,
13,
0,
15,
0,
"IMAGE"
]
],
"groups": [
{
"id": 1,
"title": "背景替换/电商详情图",
"bounding": [
10,
20,
2360,
1280
],
"color": "#3f789e",
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.8200000000000001,
"offset": [
-18,
8
]
},
"frontendVersion": "1.43.18"
},
"version": 0.4
}
-669
View File
@@ -1,669 +0,0 @@
{
"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,
"randomize",
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
}
-553
View File
@@ -1,553 +0,0 @@
{
"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,
"randomize",
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
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
# LLS Flux2Klein Background Workflow Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a ready-to-use ComfyUI example workflow that performs e-commerce background replacement with `LLSFlux2KleinEditTextEncode` on a fully matched Flux2/Klein model stack.
**Architecture:** Keep the backend node implementation as-is and add a new example workflow JSON plus a workflow regression test. The workflow must use `UNETLoader + CLIPLoader(type=flux2) + VAELoader(flux2-vae)` and derive the edit mask from `RMBG -> InvertMask` so the sampler receives a consistent Flux2/Klein edit payload.
**Tech Stack:** JSON workflow exports, Python `unittest`, existing ComfyUI core nodes, existing LLS custom node registration
---
### Task 1: Lock The Workflow Contract With A Failing Test
**Files:**
- Modify: `tests/test_example_workflows.py`
- [ ] **Step 1: Add a test that loads `LLS-Flux2Klein-电商换背景工作流.json`**
- [ ] **Step 2: Assert the workflow includes `LLSFlux2KleinEditTextEncode`, `UNETLoader`, `CLIPLoader`, `VAELoader`, `RMBG`, `InvertMask`, `ConditioningZeroOut`, `KSampler`, and `VAEDecode`**
- [ ] **Step 3: Assert `CLIPLoader` uses `flux2`, `VAELoader` uses `flux2-vae.safetensors`, and the LLS node uses `mask_mode = use_mask` with `resize_mode = longest_edge`**
- [ ] **Step 4: Run the focused workflow test and confirm it fails before the workflow JSON exists**
### Task 2: Add The New Background-Replacement Workflow
**Files:**
- Create: `LLS-Flux2Klein-电商换背景工作流.json`
- [ ] **Step 1: Build a compact workflow with `LoadImage` main product input feeding both `RMBG` and `LLSFlux2KleinEditTextEncode.image1`**
- [ ] **Step 2: Add one connected scene reference image for `image2` and leave `image3` optional**
- [ ] **Step 3: Use `RMBG -> InvertMask -> LLSFlux2KleinEditTextEncode.mask` and route `conditioning`, `ConditioningZeroOut`, `latent`, `KSampler`, and `VAEDecode` according to the edit flow contract**
- [ ] **Step 4: Route the model through a Flux2/Klein-safe sampler chain and add `PreviewImage` plus `SaveImage` outputs**
### Task 3: Verify The Workflow And Summarize Constraints
**Files:**
- Modify: files from earlier tasks only if verification exposes gaps
- [ ] **Step 1: Run `python3 -m unittest tests.test_example_workflows -v`**
- [ ] **Step 2: Run `python3 -m json.tool 'LLS-Flux2Klein-电商换背景工作流.json' > /tmp/lls_flux2klein_background_workflow.pretty.json` to validate JSON syntax**
- [ ] **Step 3: Re-run the targeted Flux2/Klein unit tests if the workflow change touches shared assumptions**
- [ ] **Step 4: Summarize the new workflow file, ComfyUI search name, minimal connection path, and the remaining compatibility-vs-native Flux2 API gaps**
+136 -20
View File
@@ -32,6 +32,16 @@ except Exception as exc: # pragma: no cover - optional runtime dependency
else: # pragma: no cover - exercised in ComfyUI runtime/tests when comfy exists
_COMFY_UTILS_ERR = None
try:
import comfy_extras.nodes_flux as native_flux_nodes
except Exception: # pragma: no cover - optional ComfyUI runtime dependency
native_flux_nodes = None
try:
import comfy_extras.nodes_edit_model as native_edit_model_nodes
except Exception: # pragma: no cover - optional ComfyUI runtime dependency
native_edit_model_nodes = None
RESIZE_MODE_CHOICES = ["longest_edge", "keep_original"]
MASK_MODE_CHOICES = ["none", "use_mask", "invert_mask"]
@@ -71,6 +81,13 @@ class _ConstantMask:
)
class _ShapeOnlyLatent:
"""Fallback latent object for tests when torch tensors are unavailable."""
def __init__(self, shape):
self.shape = tuple(shape)
def _extract_latent_samples(latent):
if isinstance(latent, dict) and "samples" in latent:
return latent["samples"]
@@ -87,6 +104,17 @@ def _get_image_batch_size(image) -> int:
return batch
def _extract_node_output_value(value):
if hasattr(value, "result"):
value = value.result
elif hasattr(value, "args"):
value = value.args
if isinstance(value, tuple) and len(value) == 1:
return value[0]
return value
def _scale_size_by_longest_edge(width: int, height: int, target_longest_edge: int) -> tuple[int, int]:
width = int(width)
height = int(height)
@@ -305,6 +333,70 @@ def _encode_reference_latent(vae, image, target_width: int, target_height: int,
return {"samples": samples}, prepared
def _encode_image_latent(vae, image, source_name: str):
try:
samples = vae.encode(image)
except Exception as exc:
raise RuntimeError(
f"[LLS] VAE encode failed for {source_name} / {source_name} 的 VAE 编码失败: {exc}"
) from exc
return {"samples": samples}
def _get_latent_channels(latent_samples) -> int:
shape = tuple(getattr(latent_samples, "shape", ()))
if len(shape) < 2:
raise RuntimeError("[LLS] Invalid latent tensor shape. 无法识别 latent 通道数。")
return int(shape[1])
def _build_empty_like_latent(samples):
shape = tuple(getattr(samples, "shape", ()))
if len(shape) != 4:
raise RuntimeError("[LLS] Invalid latent tensor shape for Empty Flux2 latent fallback.")
if torch is not None and isinstance(samples, torch.Tensor):
return {"samples": torch.zeros(shape, dtype=samples.dtype, device=samples.device)}
try:
return {"samples": samples.__class__(shape)}
except Exception:
return {"samples": _ShapeOnlyLatent(shape)}
def _build_empty_flux2_latent(main_image, main_latent):
width, height = get_image_size(main_image)
batch_size = _get_image_batch_size(main_image)
execute = getattr(getattr(native_flux_nodes, "EmptyFlux2LatentImage", None), "execute", None)
if callable(execute):
try:
return _extract_node_output_value(execute(width=width, height=height, batch_size=batch_size))
except TypeError:
return _extract_node_output_value(execute(width, height, batch_size))
except Exception:
pass
return _build_empty_like_latent(_extract_latent_samples(main_latent))
def _attach_reference_latents(conditioning, ref_latents):
if not ref_latents:
return conditioning
execute = getattr(getattr(native_edit_model_nodes, "ReferenceLatent", None), "execute", None)
if callable(execute):
updated = conditioning
for ref_latent in ref_latents:
try:
updated = _extract_node_output_value(execute(updated, latent=ref_latent))
except TypeError:
updated = _extract_node_output_value(execute(updated, ref_latent))
return updated
return _append_reference_latents(conditioning, [_extract_latent_samples(latent) for latent in ref_latents])
def _build_noise_mask(mask, latent_samples):
target_width = int(latent_samples.shape[-1])
target_height = int(latent_samples.shape[-2])
@@ -386,25 +478,48 @@ class LLSFlux2KleinEditTextEncode:
conditioning, full_prompt = _encode_multivision_conditioning(clip, str(prompt), vision_images)
target_width, target_height = get_image_size(main_image)
ref_latents = []
for index, image in enumerate(images_for_edit, start=1):
ref_latent, _prepared = _encode_reference_latent(
vae,
image,
target_width,
target_height,
source_name=f"image{index}",
)
ref_latents.append(ref_latent)
main_latent = _encode_image_latent(vae, main_image, source_name="image1")
main_latent_channels = _get_latent_channels(_extract_latent_samples(main_latent))
use_official_flux2_latent = main_latent_channels >= 128
conditioning = _append_reference_latents(
conditioning,
[_extract_latent_samples(latent) for latent in ref_latents],
)
ref_latents = [main_latent]
if reference_image_2 is not None:
if use_official_flux2_latent:
ref_latents.append(_encode_image_latent(vae, reference_image_2, source_name="image2"))
else:
ref_latent, _prepared = _encode_reference_latent(
vae,
reference_image_2,
target_width,
target_height,
source_name="image2",
)
ref_latents.append(ref_latent)
if reference_image_3 is not None:
if use_official_flux2_latent:
ref_latents.append(_encode_image_latent(vae, reference_image_3, source_name="image3"))
else:
ref_latent, _prepared = _encode_reference_latent(
vae,
reference_image_3,
target_width,
target_height,
source_name="image3",
)
ref_latents.append(ref_latent)
latent = {
"samples": _extract_latent_samples(ref_latents[0]),
}
conditioning = _attach_reference_latents(conditioning, ref_latents)
if use_official_flux2_latent:
latent = _build_empty_flux2_latent(main_image, main_latent)
latent_mode = "empty_flux2_latent"
conditioning_backend = "flux2klein_multivision_clip+reference_latent"
else:
latent = {
"samples": _extract_latent_samples(main_latent),
}
latent_mode = "image1_reference_latent"
conditioning_backend = "flux2klein_multivision_clip"
if mask is not None and str(mask_mode) != "none":
latent["noise_mask"] = _build_noise_mask(output_mask, latent["samples"])
@@ -426,13 +541,14 @@ class LLSFlux2KleinEditTextEncode:
"main_image": "image1",
"reference_images": reference_names,
"num_reference_images": len(reference_names),
"conditioning_backend": "flux2klein_multivision_clip",
"conditioning_backend": conditioning_backend,
"native_error": None,
"native_wrapper": "PainterFluxImageEdit-compatible",
"latent_mode": "image1_reference_latent",
"native_wrapper": "official Flux2 ReferenceLatent" if use_official_flux2_latent else "PainterFluxImageEdit-compatible",
"latent_mode": latent_mode,
"latent_has_noise_mask": "noise_mask" in latent,
"vision_image_count": len(vision_images),
"reference_latent_count": len(ref_latents),
"main_latent_channels": main_latent_channels,
"target_size": {"width": int(target_width), "height": int(target_height)},
"note": "image1 is main edit image; image2 and image3 are reference images; images are not concatenated",
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

+30
View File
@@ -41,6 +41,7 @@ from ..utils.model_info import (
get_family_defaults,
info_to_json,
infer_family_from_name,
is_flux2_family,
is_flux_family,
is_sdxl_family,
tag_lls_object,
@@ -62,6 +63,8 @@ _FLUX_T5_PATTERNS = ("t5xxl_fp8_e4m3fn.safetensors", "t5xxl_fp16.safetensors", "
_SDXL_CLIP_L_PATTERNS = ("clip_l.safetensors", "clip_l", "clip-l")
_SDXL_CLIP_G_PATTERNS = ("clip_g.safetensors", "clip_g", "clip-g")
_FLUX_VAE_PATTERNS = ("ae.safetensors", "ae", "vae")
_FLUX2_QWEN_PATTERNS = ("qwen_3_8b.safetensors", "qwen_3_8b_fp8mixed.safetensors", "qwen_3_8b")
_FLUX2_VAE_PATTERNS = ("flux2-vae.safetensors", "flux2-vae", "flux2_vae")
def _build_capability_tags(model_name: str, family: str) -> dict[str, object]:
@@ -226,6 +229,12 @@ def _load_external_text_encoder(family: str, name1: str, name2: str | None):
raise RuntimeError("[LLS] ComfyUI core CLIPLoader class was not found.")
return clip_loader_cls().load_clip(name1, type="stable_diffusion")[0]
if is_flux2_family(family):
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="flux2")[0]
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.")
@@ -255,6 +264,10 @@ def _resolve_external_text_encoder_names(
return name1, None
return (_match_file(available, _SDXL_CLIP_L_PATTERNS) or available[0] if available else None, None)
if is_flux2_family(family):
resolved_1 = name1 or _match_file(available, _FLUX2_QWEN_PATTERNS)
return resolved_1, None
if is_sdxl_family(family):
resolved_1 = name1 or _match_file(available, _SDXL_CLIP_L_PATTERNS)
resolved_2 = name2 or _match_file(available, _SDXL_CLIP_G_PATTERNS)
@@ -269,6 +282,8 @@ def _resolve_external_vae_name(family: str, name: str | None) -> str | None:
if name:
return name
available = _get_filename_list("vae")
if is_flux2_family(family):
return _match_file(available, _FLUX2_VAE_PATTERNS)
if is_flux_family(family):
return _match_file(available, _FLUX_VAE_PATTERNS)
return available[0] if available else None
@@ -285,6 +300,11 @@ def _missing_text_encoder_error(family: str, missing_role: str) -> RuntimeError:
f"[LLS] Missing SDXL text encoder {filename}. "
"Place it in ComfyUI/models/text_encoders/."
)
if is_flux2_family(family):
return RuntimeError(
"[LLS] Missing Flux2/Klein text encoder qwen_3_8b.safetensors "
"or qwen_3_8b_fp8mixed.safetensors. Place one of them in ComfyUI/models/text_encoders/."
)
filename = "clip_l.safetensors" if missing_role == "clip_l" else "t5xxl_fp8_e4m3fn.safetensors"
fallback = (
"[LLS] Missing FLUX text encoder t5xxl_fp8_e4m3fn.safetensors or t5xxl_fp16.safetensors. "
@@ -325,6 +345,11 @@ def _resolve_text_encoder(
raise _missing_text_encoder_error(family, "clip")
return _load_external_text_encoder(family, resolved_1, None), "external", resolved_1, None
if is_flux2_family(family):
if not resolved_1:
raise _missing_text_encoder_error(family, "qwen_3_8b")
return _load_external_text_encoder(family, resolved_1, None), "external", resolved_1, None
required_role_1 = "clip_l"
required_role_2 = "clip_g" if is_sdxl_family(family) else "t5xxl"
if not resolved_1:
@@ -367,6 +392,11 @@ def _resolve_vae(
if resolved_vae_name:
return _load_external_vae(resolved_vae_name), "external", resolved_vae_name
if is_flux2_family(family):
raise RuntimeError(
"[LLS] Missing Flux2/Klein VAE flux2-vae.safetensors. Place it in ComfyUI/models/vae/."
)
if is_flux_family(family):
raise RuntimeError(
"[LLS] Missing FLUX AE/VAE ae.safetensors. Place it in ComfyUI/models/vae/."
+33
View File
@@ -16,6 +16,12 @@ class TestExampleWorkflows(unittest.TestCase):
return node
self.fail(f"Missing node type {node_type!r} in workflow")
def _find_input(self, node: dict, name: str) -> dict:
for item in node.get("inputs", []):
if item.get("name") == name:
return item
self.fail(f"Missing input {name!r} in node {node.get('type')!r}")
def test_universal_txt2img_ksampler_keeps_seed_control_widget(self):
workflow = self._load("LLS-universal-txt2img-workflow.json")
node = self._find_node(workflow, "LLSSimpleKSampler")
@@ -36,6 +42,33 @@ class TestExampleWorkflows(unittest.TestCase):
self.assertEqual(widgets[2], "randomize")
self.assertEqual(widgets[3], 20)
def test_flux2klein_background_replace_workflow_uses_lls_edit_and_flux2_stack(self):
workflow = self._load("LLS-Flux2Klein-电商换背景工作流.json")
encode = self._find_node(workflow, "LLSFlux2KleinEditTextEncode")
unet = self._find_node(workflow, "UNETLoader")
clip = self._find_node(workflow, "CLIPLoader")
vae = self._find_node(workflow, "VAELoader")
self._find_node(workflow, "RMBG")
self._find_node(workflow, "InvertMask")
self._find_node(workflow, "ConditioningZeroOut")
self._find_node(workflow, "KSampler")
self._find_node(workflow, "VAEDecode")
self.assertEqual(unet.get("widgets_values", [None])[0], "flux-2-klein-9b.safetensors")
self.assertEqual(clip.get("widgets_values", [None, None])[1], "flux2")
self.assertEqual(vae.get("widgets_values", [None])[0], "flux2-vae.safetensors")
widgets = encode.get("widgets_values", [])
self.assertGreaterEqual(len(widgets), 4)
self.assertEqual(widgets[2], "longest_edge")
self.assertEqual(widgets[3], "use_mask")
self.assertIsNotNone(self._find_input(encode, "image1").get("link"))
self.assertIsNotNone(self._find_input(encode, "image2").get("link"))
self.assertIsNone(self._find_input(encode, "image3").get("link"))
self.assertIsNotNone(self._find_input(encode, "mask").get("link"))
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -12,6 +12,8 @@ try:
FakeMask,
FakeVAE,
FakeTensor,
NativeEditModelStub,
NativeFluxStub,
StandardClipStub,
import_plugin_submodule,
load_plugin_package,
@@ -22,6 +24,8 @@ except ImportError:
FakeMask,
FakeVAE,
FakeTensor,
NativeEditModelStub,
NativeFluxStub,
StandardClipStub,
import_plugin_submodule,
load_plugin_package,
@@ -137,6 +141,39 @@ class TestFlux2KleinNode(unittest.TestCase):
self.assertNotIn("noise_mask", latent)
self.assertEqual(custom_output["latent_has_noise_mask"], False)
def test_flux2_official_path_uses_empty_latent_and_reference_latent_nodes(self):
NativeFluxStub.reset()
NativeEditModelStub.reset()
with mock.patch.object(self.module, "native_flux_nodes", NativeFluxStub, create=True), mock.patch.object(
self.module,
"native_edit_model_nodes",
NativeEditModelStub,
create=True,
):
conditioning, latent, custom_output, main_image, output_mask = self.node.encode(
clip=StandardClipStub(),
vae=FakeVAE(latent_channels=128, downscale_ratio=16),
image1=FakeTensor((1, 832, 1248, 3), label="main"),
prompt="replace background with a clean kitchen scene",
ref_longest_edge=1248,
resize_mode="keep_original",
mask_mode="use_mask",
image2=FakeTensor((1, 640, 640, 3), label="ref2"),
image3=None,
mask=FakeMask((1, 832, 1248), mask_bbox=(50, 40, 500, 700), mask_area_ratio=0.35, label="mask"),
)
self.assertEqual(NativeFluxStub.last_empty_call, {"width": 1248, "height": 832, "batch_size": 1})
self.assertEqual(len(NativeEditModelStub.last_calls), 2)
self.assertEqual(latent["samples"].shape, (1, 128, 52, 78))
self.assertEqual(latent["noise_mask"].shape, (1, 52, 78))
self.assertEqual(custom_output["latent_mode"], "empty_flux2_latent")
self.assertEqual(custom_output["conditioning_backend"], "flux2klein_multivision_clip+reference_latent")
self.assertEqual(custom_output["reference_latent_count"], 2)
self.assertEqual(main_image.shape, (1, 832, 1248, 3))
self.assertEqual(output_mask.shape, (1, 832, 1248))
def test_missing_clip_backend_raises_clear_error(self):
with self.assertRaisesRegex(RuntimeError, "vision-aware|text encoding backend"):
self.node.encode(
+37
View File
@@ -448,6 +448,43 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(payload["text_encoder_name"], "clip_l.safetensors, t5xxl_fp16.safetensors")
self.assertEqual(payload["vae_name"], "ae.safetensors")
def test_universal_loader_loads_flux2_klein_with_qwen_and_flux2_vae(self):
load_plugin_package()
from lls_node_test_refactor.model_loader import nodes as loader_nodes
folder_paths = FolderPathsStub()
folder_paths._files["diffusion_models"].append("flux-2-klein-9b.safetensors")
folder_paths._files["text_encoders"].append("qwen_3_8b.safetensors")
folder_paths._files["vae"].append("flux2-vae.safetensors")
with mock.patch.object(loader_nodes, "folder_paths", folder_paths), 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/flux-2-klein-9b.safetensors",
"Auto",
"advanced",
"external",
"external",
"qwen_3_8b.safetensors",
"(auto)",
"flux2-vae.safetensors",
)
payload = json.loads(model_info)
self.assertEqual(model.label, "MODEL::flux-2-klein-9b.safetensors")
self.assertEqual(text_encoder.label, "CLIP::flux2::qwen_3_8b.safetensors")
self.assertEqual(vae.label, "VAE::flux2-vae.safetensors")
self.assertEqual(payload["model_family"], "FLUX2_KLEIN")
self.assertEqual(payload["text_encoder_name_1"], "qwen_3_8b.safetensors")
self.assertEqual(payload["text_encoder_name_2"], "")
self.assertEqual(payload["text_encoder_name"], "qwen_3_8b.safetensors")
self.assertEqual(payload["vae_name"], "flux2-vae.safetensors")
self.assertEqual(text_encoder._lls_text_encoder_type, "flux2_qwen")
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
+78 -1
View File
@@ -19,6 +19,9 @@ LEGACY_FAMILY_ALIASES: dict[str, str] = {
"FLUX": "FLUX_DEV",
"FLUX_SCHNELL": "FLUX_SCHNELL",
"FLUX_DEV": "FLUX_DEV",
"FLUX2": "FLUX2_DEV",
"FLUX2_DEV": "FLUX2_DEV",
"FLUX2_KLEIN": "FLUX2_KLEIN",
}
MODEL_FAMILY_CHOICES: list[str] = [
@@ -29,6 +32,8 @@ MODEL_FAMILY_CHOICES: list[str] = [
"SDXL_TURBO",
"FLUX_SCHNELL",
"FLUX_DEV",
"FLUX2_DEV",
"FLUX2_KLEIN",
]
FAMILY_DEFAULTS: dict[str, dict[str, Any]] = {
@@ -122,6 +127,42 @@ FAMILY_DEFAULTS: dict[str, dict[str, Any]] = {
"is_turbo": False,
"is_flux": True,
},
"FLUX2_DEV": {
"text_encoder_type": "flux2_qwen",
"required_text_encoders": ["qwen_3_8b"],
"required_vae": "required",
"latent_channels": 128,
"downscale_ratio": 16,
"default_width": 1024,
"default_height": 1024,
"default_steps": 20,
"default_cfg": 1.0,
"default_guidance": 3.5,
"default_sampler": "euler",
"default_scheduler": "simple",
"default_denoise": 1.0,
"guidance_embed": True,
"is_turbo": False,
"is_flux": True,
},
"FLUX2_KLEIN": {
"text_encoder_type": "flux2_qwen",
"required_text_encoders": ["qwen_3_8b"],
"required_vae": "required",
"latent_channels": 128,
"downscale_ratio": 16,
"default_width": 1024,
"default_height": 1024,
"default_steps": 20,
"default_cfg": 1.0,
"default_guidance": 3.5,
"default_sampler": "euler",
"default_scheduler": "simple",
"default_denoise": 1.0,
"guidance_embed": True,
"is_turbo": False,
"is_flux": True,
},
}
SAMPLING_PRESETS: dict[str, dict[str, dict[str, float | int | None]]] = {
@@ -190,6 +231,32 @@ SAMPLING_PRESETS: dict[str, dict[str, dict[str, float | int | None]]] = {
"Balanced": {"steps": 20, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
"High Quality": {"steps": 28, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
},
"FLUX2_DEV": {
FAMILY_DEFAULT_PRESET: {
"steps": 20,
"cfg": 1.0,
"guidance": 3.5,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1.0,
},
"Fast": {"steps": 12, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
"Balanced": {"steps": 20, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
"High Quality": {"steps": 28, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
},
"FLUX2_KLEIN": {
FAMILY_DEFAULT_PRESET: {
"steps": 20,
"cfg": 1.0,
"guidance": 3.5,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1.0,
},
"Fast": {"steps": 12, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
"Balanced": {"steps": 20, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
"High Quality": {"steps": 28, "cfg": 1.0, "guidance": 3.5, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0},
},
}
_LEGACY_MODEL_INFO_PATTERN = re.compile(r"(\w+)=([^|]+)")
@@ -219,6 +286,10 @@ def infer_family_from_name(model_name: str | None, fallback: str = "SD1.5") -> s
return canonicalize_family(fallback)
name = str(model_name).lower()
if "flux-2" in name or "flux2" in name or "flux.2" in name:
if "klein" in name:
return "FLUX2_KLEIN"
return "FLUX2_DEV"
if "flux" in name:
if "schnell" in name:
return "FLUX_SCHNELL"
@@ -231,7 +302,11 @@ def infer_family_from_name(model_name: str | None, fallback: str = "SD1.5") -> s
def is_flux_family(family: str | None) -> bool:
return canonicalize_family(family) in {"FLUX_SCHNELL", "FLUX_DEV"}
return canonicalize_family(family) in {"FLUX_SCHNELL", "FLUX_DEV", "FLUX2_DEV", "FLUX2_KLEIN"}
def is_flux2_family(family: str | None) -> bool:
return canonicalize_family(family) in {"FLUX2_DEV", "FLUX2_KLEIN"}
def is_sdxl_family(family: str | None) -> bool:
@@ -460,6 +535,8 @@ def infer_family_from_clip(clip) -> str:
tokens = clip.tokenize("")
except Exception:
return "SD1.5"
if "qwen" in getattr(clip, "_lls_text_encoder_type", ""):
return "FLUX2_KLEIN"
if "t5xxl" in tokens:
return "FLUX_DEV"
if "g" in tokens:
File diff suppressed because it is too large Load Diff
+3944 -436
View File
File diff suppressed because it is too large Load Diff