单独制作一个K采样器逻辑
This commit is contained in:
+831
@@ -0,0 +1,831 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,63 @@
|
||||
# FLUX Native Repair Routing 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:** Rebuild `LLSSimpleRepairPrepare` so FLUX local repair can follow the official Fill workflow shape while base models still fall back to generic repair behavior.
|
||||
|
||||
**Architecture:** Add a repair backend registry parallel to `pro_edit`, with a native FLUX backend that prepares `InpaintModelConditioning` payloads and marks the sampler to patch the model through `DifferentialDiffusion`. Keep the existing latent-mask / VAE-inpaint preparation as the generic fallback path for base or unsupported profiles.
|
||||
|
||||
**Tech Stack:** Python, unittest, existing ComfyUI node classes, model profile routing, fake tensor/mask helpers
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock in the failing tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_repair_registration.py`
|
||||
- Modify: `tests/test_repair_utils.py`
|
||||
- Modify: `tests/test_repair_prepare.py`
|
||||
- Create: `tests/test_repair_registry.py`
|
||||
|
||||
- [ ] **Step 1: Add schema coverage for optional `model` input and backend metadata**
|
||||
- [ ] **Step 2: Add routing tests showing FLUX fill/edit profiles choose the native backend and base profiles stay on fallback**
|
||||
- [ ] **Step 3: Add prepare tests showing native FLUX returns `InpaintModelConditioning`-style latent/conditioning instead of the old fallback latent payload**
|
||||
- [ ] **Step 4: Run focused repair tests and confirm the new expectations fail for the current implementation**
|
||||
|
||||
### Task 2: Add repair backend routing and runtime wrappers
|
||||
|
||||
**Files:**
|
||||
- Create: `repair/backends/base.py`
|
||||
- Create: `repair/backends/registry.py`
|
||||
- Create: `repair/backends/generic.py`
|
||||
- Create: `repair/backends/flux.py`
|
||||
- Create: `repair/runtime.py`
|
||||
|
||||
- [ ] **Step 1: Mirror the `pro_edit` routing/result structure for repair**
|
||||
- [ ] **Step 2: Wrap ComfyUI `InpaintModelConditioning` and `DifferentialDiffusion` with runtime-safe helpers**
|
||||
- [ ] **Step 3: Implement the FLUX native prepare path and generic fallback path**
|
||||
- [ ] **Step 4: Re-run focused routing tests until green**
|
||||
|
||||
### Task 3: Integrate repair prepare and sampler
|
||||
|
||||
**Files:**
|
||||
- Modify: `repair/repair_prepare.py`
|
||||
- Modify: `repair/repair_utils.py`
|
||||
- Modify: `repair/__init__.py`
|
||||
- Modify: `sampling/nodes.py`
|
||||
|
||||
- [ ] **Step 1: Add optional `model` input to `LLSSimpleRepairPrepare` and resolve the model profile from `model` + `model_info`**
|
||||
- [ ] **Step 2: Move backend-specific latent/conditioning preparation behind the new registry**
|
||||
- [ ] **Step 3: Extend `repair_info` with backend/routing/execution metadata**
|
||||
- [ ] **Step 4: Patch FLUX native models with `DifferentialDiffusion` inside `LLSSimpleKSampler` when `repair_info` requests it**
|
||||
- [ ] **Step 5: Re-run focused repair prepare + sampler tests**
|
||||
|
||||
### Task 4: Update the workflow JSON and verify regressions
|
||||
|
||||
**Files:**
|
||||
- Modify: `测试工作流.json`
|
||||
- Modify: other files from earlier tasks only if verification exposes gaps
|
||||
|
||||
- [ ] **Step 1: Rewire the FLUX repair example workflow to feed `model` into `LLSSimpleRepairPrepare` and keep the prompt text on the active path**
|
||||
- [ ] **Step 2: Run `python3 -m unittest tests.test_repair_registration tests.test_repair_registry tests.test_repair_prepare tests.test_repair_sampler tests.test_repair_utils -v`**
|
||||
- [ ] **Step 3: Run `python3 -m compileall __init__.py repair sampling model_profiles pro_edit qwen model_loader`**
|
||||
- [ ] **Step 4: Inspect the final JSON diff to confirm the workflow matches the intended FLUX repair path**
|
||||
@@ -0,0 +1 @@
|
||||
__all__ = []
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Protocol
|
||||
|
||||
from ..repair_utils import make_noise_mask
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RoutingResult:
|
||||
backend_name: str
|
||||
routing_reason: str
|
||||
capabilities: dict[str, Any]
|
||||
profile: dict[str, Any]
|
||||
execution_path: str
|
||||
|
||||
|
||||
class RepairBackend(Protocol):
|
||||
backend_name: str
|
||||
|
||||
def supports(self, profile: dict[str, Any]) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def prepare(self, **kwargs) -> dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def validate_backend(backend_name: str, backend: RepairBackend, profile: dict[str, Any]) -> None:
|
||||
if backend.supports(profile):
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"[LLS] Repair backend '{backend_name}' is incompatible with profile "
|
||||
f"'{profile.get('profile_id')}' (family '{profile.get('family')}', role '{profile.get('role')}')."
|
||||
)
|
||||
|
||||
|
||||
def build_fallback_latent(vae, work_image, work_mask, *, repair_kernel: str, latent_source: str):
|
||||
latent_samples = vae.encode(work_image)
|
||||
latent = {
|
||||
"samples": latent_samples,
|
||||
"source": latent_source,
|
||||
}
|
||||
if str(repair_kernel or "") == "latent_mask":
|
||||
latent["noise_mask"] = make_noise_mask(work_mask, latent_samples)
|
||||
return latent
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import runtime as repair_runtime
|
||||
from .base import build_fallback_latent
|
||||
from .registry import register_backend
|
||||
|
||||
|
||||
class FluxRepairBackend:
|
||||
backend_name = "flux"
|
||||
|
||||
def supports(self, profile):
|
||||
return str(profile.get("family") or "").strip().upper().startswith("FLUX")
|
||||
|
||||
def prepare(self, *, vae, work_image, work_mask, positive, negative, workspace, repair_kernel, routing, **_kwargs):
|
||||
if routing.execution_path != "native_repair":
|
||||
latent = build_fallback_latent(
|
||||
vae,
|
||||
work_image,
|
||||
work_mask,
|
||||
repair_kernel=repair_kernel,
|
||||
latent_source=f"repair_prepare_{workspace['repair_scope']}",
|
||||
)
|
||||
return {
|
||||
"latent": latent,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
"backend_hints": {
|
||||
"backend_name": self.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "",
|
||||
},
|
||||
}
|
||||
|
||||
if positive is None or negative is None:
|
||||
raise RuntimeError("[LLS] Native FLUX repair requires positive and negative conditioning.")
|
||||
|
||||
positive_out, negative_out, latent = repair_runtime.encode_inpaint_conditioning(
|
||||
positive,
|
||||
negative,
|
||||
vae,
|
||||
work_image,
|
||||
work_mask,
|
||||
noise_mask=True,
|
||||
)
|
||||
latent["source"] = f"repair_prepare_{workspace['repair_scope']}"
|
||||
return {
|
||||
"latent": latent,
|
||||
"positive": positive_out,
|
||||
"negative": negative_out,
|
||||
"backend_hints": {
|
||||
"backend_name": self.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "differential_diffusion",
|
||||
"model_patch_strength": 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
register_backend(FluxRepairBackend())
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import build_fallback_latent
|
||||
from .registry import register_backend
|
||||
|
||||
|
||||
class GenericRepairBackend:
|
||||
backend_name = "generic"
|
||||
|
||||
def supports(self, profile):
|
||||
del profile
|
||||
return True
|
||||
|
||||
def prepare(self, *, vae, work_image, work_mask, positive, negative, workspace, repair_kernel, routing, **_kwargs):
|
||||
latent = build_fallback_latent(
|
||||
vae,
|
||||
work_image,
|
||||
work_mask,
|
||||
repair_kernel=repair_kernel,
|
||||
latent_source=f"repair_prepare_{workspace['repair_scope']}",
|
||||
)
|
||||
return {
|
||||
"latent": latent,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
"backend_hints": {
|
||||
"backend_name": self.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
register_backend(GenericRepairBackend())
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
from .base import RoutingResult, validate_backend
|
||||
from ...utils.model_info import canonicalize_family, parse_jsonish_info, resolve_edit_capabilities
|
||||
|
||||
|
||||
_BACKENDS = {}
|
||||
_BUILTINS_LOADED = False
|
||||
|
||||
|
||||
def _ensure_builtin_backends():
|
||||
global _BUILTINS_LOADED
|
||||
if _BUILTINS_LOADED:
|
||||
return
|
||||
importlib.import_module(f"{__package__}.generic")
|
||||
importlib.import_module(f"{__package__}.sdxl")
|
||||
importlib.import_module(f"{__package__}.flux")
|
||||
_BUILTINS_LOADED = True
|
||||
|
||||
|
||||
def register_backend(backend):
|
||||
_BACKENDS[str(backend.backend_name).strip().lower()] = backend
|
||||
return backend
|
||||
|
||||
|
||||
def get_backend(name: str):
|
||||
_ensure_builtin_backends()
|
||||
backend = _BACKENDS.get(str(name or "").strip().lower())
|
||||
if backend is None:
|
||||
raise RuntimeError(f"[LLS] Unknown repair backend '{name}'.")
|
||||
return backend
|
||||
|
||||
|
||||
def _build_profile(capabilities: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"family": capabilities["model_family"],
|
||||
"role": capabilities["model_role"],
|
||||
"profile_id": capabilities["profile_id"],
|
||||
"backend_type": capabilities["backend_type"],
|
||||
"sampler_strategy": capabilities["sampler_strategy"],
|
||||
"loader_strategy": capabilities["loader_strategy"],
|
||||
"supports_inpaint_native": capabilities["supports_inpaint_native"],
|
||||
"supports_image_edit_native": capabilities["supports_image_edit_native"],
|
||||
"preferred_edit_backend": capabilities["preferred_edit_backend"],
|
||||
}
|
||||
|
||||
|
||||
def _resolve_profile_and_capabilities(model=None, model_info=None, repair_info=None):
|
||||
raw_model_info = parse_jsonish_info(model_info)
|
||||
raw_repair_info = parse_jsonish_info(repair_info)
|
||||
merged_info = dict(raw_model_info)
|
||||
merged_info.update(raw_repair_info)
|
||||
capabilities = resolve_edit_capabilities(model=model, model_info=merged_info)
|
||||
return _build_profile(capabilities), capabilities
|
||||
|
||||
|
||||
def _resolve_fallback_backend_name(family: str) -> str:
|
||||
normalized = canonicalize_family(family)
|
||||
if normalized.startswith("SDXL"):
|
||||
return "sdxl"
|
||||
if normalized.startswith("FLUX"):
|
||||
return "flux"
|
||||
return "generic"
|
||||
|
||||
|
||||
def resolve_backend(repair_kernel: str, *, model=None, model_info=None, repair_info=None):
|
||||
_ensure_builtin_backends()
|
||||
profile, capabilities = _resolve_profile_and_capabilities(
|
||||
model=model,
|
||||
model_info=model_info,
|
||||
repair_info=repair_info,
|
||||
)
|
||||
|
||||
if str(repair_kernel or "") == "native_fill":
|
||||
native_backend_name = {
|
||||
"sdxl_native": "sdxl",
|
||||
"flux_edit": "flux",
|
||||
}.get(str(profile.get("backend_type") or "").strip().lower())
|
||||
if native_backend_name is not None:
|
||||
backend = get_backend(native_backend_name)
|
||||
validate_backend(native_backend_name, backend, profile)
|
||||
return backend, RoutingResult(
|
||||
backend.backend_name,
|
||||
"profile.backend_type",
|
||||
capabilities,
|
||||
profile,
|
||||
"native_repair",
|
||||
)
|
||||
|
||||
fallback_backend_name = _resolve_fallback_backend_name(str(profile.get("family") or ""))
|
||||
backend = get_backend(fallback_backend_name)
|
||||
validate_backend(fallback_backend_name, backend, profile)
|
||||
return backend, RoutingResult(
|
||||
backend.backend_name,
|
||||
"profile.family_fallback",
|
||||
capabilities,
|
||||
profile,
|
||||
"fallback_repair",
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import runtime as repair_runtime
|
||||
from .base import build_fallback_latent
|
||||
from .registry import register_backend
|
||||
|
||||
|
||||
class SDXLRepairBackend:
|
||||
backend_name = "sdxl"
|
||||
|
||||
def supports(self, profile):
|
||||
return str(profile.get("family") or "").strip().upper().startswith("SDXL")
|
||||
|
||||
def prepare(self, *, vae, work_image, work_mask, positive, negative, workspace, repair_kernel, routing, **_kwargs):
|
||||
if routing.execution_path != "native_repair":
|
||||
latent = build_fallback_latent(
|
||||
vae,
|
||||
work_image,
|
||||
work_mask,
|
||||
repair_kernel=repair_kernel,
|
||||
latent_source=f"repair_prepare_{workspace['repair_scope']}",
|
||||
)
|
||||
return {
|
||||
"latent": latent,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
"backend_hints": {
|
||||
"backend_name": self.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "",
|
||||
},
|
||||
}
|
||||
|
||||
if positive is None or negative is None:
|
||||
raise RuntimeError("[LLS] Native SDXL repair requires positive and negative conditioning.")
|
||||
|
||||
positive_out, negative_out, latent = repair_runtime.encode_inpaint_conditioning(
|
||||
positive,
|
||||
negative,
|
||||
vae,
|
||||
work_image,
|
||||
work_mask,
|
||||
noise_mask=True,
|
||||
)
|
||||
latent["source"] = f"repair_prepare_{workspace['repair_scope']}"
|
||||
return {
|
||||
"latent": latent,
|
||||
"positive": positive_out,
|
||||
"negative": negative_out,
|
||||
"backend_hints": {
|
||||
"backend_name": self.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
register_backend(SDXLRepairBackend())
|
||||
+144
-71
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .backends.registry import resolve_backend
|
||||
from .repair_utils import (
|
||||
build_canvas_info,
|
||||
build_canvas_repair_mask,
|
||||
@@ -10,9 +11,7 @@ from .repair_utils import (
|
||||
expand_canvas_mask,
|
||||
get_image_size,
|
||||
get_mask_metrics,
|
||||
make_noise_mask,
|
||||
merge_masks,
|
||||
normalize_model_info,
|
||||
preprocess_mask,
|
||||
recommend_denoise,
|
||||
resize_image_to,
|
||||
@@ -21,6 +20,7 @@ from .repair_utils import (
|
||||
resolve_repair_scope,
|
||||
resolve_work_size,
|
||||
)
|
||||
from ..utils.model_info import resolve_edit_capabilities
|
||||
|
||||
|
||||
_REPAIR_SCOPE_CHOICES = ["auto", "region", "crop", "canvas"]
|
||||
@@ -43,6 +43,67 @@ _AUTO_RECOMMEND_CHOICES = ["enabled", "disabled"]
|
||||
_CANVAS_FILL_CHOICES = ["edge", "blur", "black", "white", "neutral"]
|
||||
|
||||
|
||||
def _build_repair_info(
|
||||
*,
|
||||
repair_scope: str,
|
||||
repair_kernel: str,
|
||||
task_hint: str,
|
||||
original_size: tuple[int, int],
|
||||
work_size: tuple[int, int],
|
||||
crop_box,
|
||||
crop_scale,
|
||||
canvas_expand: list[int],
|
||||
mask_grow: int,
|
||||
mask_blur: float,
|
||||
mask_threshold: float,
|
||||
invert_mask: bool,
|
||||
recommended_denoise: float,
|
||||
routing,
|
||||
warnings: list[str],
|
||||
original_box_in_canvas=None,
|
||||
backend_hints: dict | None = None,
|
||||
):
|
||||
profile = routing.profile
|
||||
info = {
|
||||
"repair_scope": repair_scope,
|
||||
"repair_kernel": repair_kernel,
|
||||
"task_hint": str(task_hint or "auto"),
|
||||
"original_size": original_size,
|
||||
"work_size": work_size,
|
||||
"crop_box": crop_box,
|
||||
"crop_scale": crop_scale,
|
||||
"canvas_expand": list(canvas_expand),
|
||||
"original_box_in_canvas": original_box_in_canvas,
|
||||
"mask_grow": int(mask_grow),
|
||||
"mask_blur": float(mask_blur),
|
||||
"mask_threshold": float(mask_threshold),
|
||||
"invert_mask": bool(invert_mask),
|
||||
"recommended_denoise": float(recommended_denoise),
|
||||
"model_family": profile["family"],
|
||||
"model_role": profile["role"],
|
||||
"profile_id": profile["profile_id"],
|
||||
"backend_type": profile["backend_type"],
|
||||
"sampler_strategy": profile["sampler_strategy"],
|
||||
"loader_strategy": profile["loader_strategy"],
|
||||
"supports_inpaint_native": profile["supports_inpaint_native"],
|
||||
"supports_image_edit_native": profile["supports_image_edit_native"],
|
||||
"preferred_edit_backend": profile["preferred_edit_backend"],
|
||||
"backend_name": routing.backend_name,
|
||||
"routing_reason": routing.routing_reason,
|
||||
"execution_path": routing.execution_path,
|
||||
"model_patch": "",
|
||||
"model_patch_strength": 1.0,
|
||||
"repair_payload_version": "1.0",
|
||||
"has_mask": False,
|
||||
"mask_area_ratio": 0.0,
|
||||
"mask_bbox": None,
|
||||
"warnings": list(warnings),
|
||||
}
|
||||
if backend_hints:
|
||||
info.update(backend_hints)
|
||||
return info
|
||||
|
||||
|
||||
class LLSSimpleRepairPrepare:
|
||||
CATEGORY = "LLS/Image Repair"
|
||||
FUNCTION = "prepare"
|
||||
@@ -77,6 +138,7 @@ class LLSSimpleRepairPrepare:
|
||||
"auto_recommend": (_AUTO_RECOMMEND_CHOICES, {"default": "enabled"}),
|
||||
},
|
||||
"optional": {
|
||||
"model": ("MODEL",),
|
||||
"model_info": ("STRING",),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
@@ -106,6 +168,7 @@ class LLSSimpleRepairPrepare:
|
||||
expand_bottom,
|
||||
canvas_fill,
|
||||
auto_recommend,
|
||||
model=None,
|
||||
model_info=None,
|
||||
positive=None,
|
||||
negative=None,
|
||||
@@ -115,7 +178,7 @@ class LLSSimpleRepairPrepare:
|
||||
|
||||
image_width, image_height = get_image_size(image)
|
||||
original_size = (image_width, image_height)
|
||||
normalized_model = normalize_model_info(model_info)
|
||||
capabilities = resolve_edit_capabilities(model=model, model_info=model_info)
|
||||
processed_mask = preprocess_mask(
|
||||
mask,
|
||||
original_size,
|
||||
@@ -144,51 +207,75 @@ class LLSSimpleRepairPrepare:
|
||||
scope=effective_scope,
|
||||
task_hint=task_hint,
|
||||
mask_area_ratio=mask_area_ratio,
|
||||
model_info=normalized_model,
|
||||
model_info=capabilities,
|
||||
)
|
||||
backend, routing = resolve_backend(
|
||||
effective_kernel,
|
||||
model=model,
|
||||
model_info=capabilities,
|
||||
)
|
||||
warnings = list(warnings)
|
||||
if effective_kernel == "native_fill":
|
||||
effective_kernel = "vae_inpaint"
|
||||
warnings.append(
|
||||
"native_fill requested but runtime sampler support is not implemented yet; using vae_inpaint"
|
||||
)
|
||||
|
||||
recommended = recommend_denoise(task_hint, effective_scope, effective_kernel, auto_recommend)
|
||||
repair_info = {
|
||||
"repair_scope": effective_scope,
|
||||
"repair_kernel": effective_kernel,
|
||||
"task_hint": str(task_hint or "auto"),
|
||||
"original_size": original_size,
|
||||
"work_size": original_size,
|
||||
"crop_box": None,
|
||||
"crop_scale": 1.0,
|
||||
"canvas_expand": canvas_expand,
|
||||
"mask_grow": int(mask_grow),
|
||||
"mask_blur": float(mask_blur),
|
||||
"mask_threshold": float(mask_threshold),
|
||||
"invert_mask": bool(invert_mask),
|
||||
"recommended_denoise": recommended,
|
||||
"model_family": normalized_model["model_family"],
|
||||
"model_role": normalized_model["model_role"],
|
||||
"repair_payload_version": "1.0",
|
||||
"has_mask": False,
|
||||
"mask_area_ratio": 0.0,
|
||||
"mask_bbox": None,
|
||||
"warnings": warnings,
|
||||
}
|
||||
|
||||
def finalize(work_image, work_mask, *, work_size, crop_box=None, crop_scale=1.0, original_box_in_canvas=None):
|
||||
workspace = {
|
||||
"repair_scope": effective_scope,
|
||||
"original_size": original_size,
|
||||
"work_size": work_size,
|
||||
"crop_box": crop_box,
|
||||
"crop_scale": crop_scale,
|
||||
"canvas_expand": list(canvas_expand),
|
||||
"original_box_in_canvas": original_box_in_canvas,
|
||||
"work_image": work_image,
|
||||
"work_mask": work_mask,
|
||||
}
|
||||
prepared = backend.prepare(
|
||||
model=model,
|
||||
vae=vae,
|
||||
work_image=work_image,
|
||||
work_mask=work_mask,
|
||||
positive=positive,
|
||||
negative=negative,
|
||||
workspace=workspace,
|
||||
routing=routing,
|
||||
repair_kernel=effective_kernel,
|
||||
)
|
||||
info = _build_repair_info(
|
||||
repair_scope=effective_scope,
|
||||
repair_kernel=effective_kernel,
|
||||
task_hint=task_hint,
|
||||
original_size=original_size,
|
||||
work_size=work_size,
|
||||
crop_box=None if crop_box is None else list(crop_box),
|
||||
crop_scale=crop_scale,
|
||||
canvas_expand=canvas_expand,
|
||||
mask_grow=mask_grow,
|
||||
mask_blur=mask_blur,
|
||||
mask_threshold=mask_threshold,
|
||||
invert_mask=invert_mask,
|
||||
recommended_denoise=recommended,
|
||||
routing=routing,
|
||||
warnings=list(warnings),
|
||||
original_box_in_canvas=None if original_box_in_canvas is None else list(original_box_in_canvas),
|
||||
backend_hints=prepared.get("backend_hints"),
|
||||
)
|
||||
final_bbox, final_area_ratio = get_mask_metrics(work_mask, work_size)
|
||||
info["has_mask"] = final_bbox is not None and final_area_ratio > 0.0
|
||||
info["mask_bbox"] = list(final_bbox) if final_bbox is not None else None
|
||||
info["mask_area_ratio"] = final_area_ratio
|
||||
return (
|
||||
prepared["latent"],
|
||||
work_image,
|
||||
work_mask,
|
||||
info,
|
||||
recommended,
|
||||
prepared["positive"],
|
||||
prepared["negative"],
|
||||
)
|
||||
|
||||
if effective_scope == "region":
|
||||
work_image = image
|
||||
work_mask = resize_mask_to(processed_mask, image_width, image_height)
|
||||
latent_samples = vae.encode(work_image)
|
||||
latent = {"samples": latent_samples, "source": "repair_prepare_region"}
|
||||
if effective_kernel == "latent_mask":
|
||||
latent["noise_mask"] = make_noise_mask(work_mask, latent_samples)
|
||||
final_bbox, final_area_ratio = get_mask_metrics(work_mask, original_size)
|
||||
repair_info["has_mask"] = final_bbox is not None and final_area_ratio > 0.0
|
||||
repair_info["mask_bbox"] = list(final_bbox) if final_bbox is not None else None
|
||||
repair_info["mask_area_ratio"] = final_area_ratio
|
||||
return latent, work_image, work_mask, repair_info, recommended, positive, negative
|
||||
return finalize(work_image, work_mask, work_size=original_size)
|
||||
|
||||
if effective_scope == "crop":
|
||||
if mask_bbox is None or mask_area_ratio <= 0.0:
|
||||
@@ -203,23 +290,15 @@ class LLSSimpleRepairPrepare:
|
||||
int(max_size),
|
||||
resize_mode,
|
||||
)
|
||||
cropped_image = crop_image_to(image, crop_box)
|
||||
cropped_mask = crop_mask_to(processed_mask, crop_box)
|
||||
work_image = resize_image_to(cropped_image, work_width, work_height)
|
||||
work_mask = resize_mask_to(cropped_mask, work_width, work_height)
|
||||
latent_samples = vae.encode(work_image)
|
||||
latent = {"samples": latent_samples, "source": "repair_prepare_crop"}
|
||||
if effective_kernel == "latent_mask":
|
||||
latent["noise_mask"] = make_noise_mask(work_mask, latent_samples)
|
||||
|
||||
repair_info["crop_box"] = list(crop_box)
|
||||
repair_info["crop_scale"] = crop_scale
|
||||
repair_info["work_size"] = (work_width, work_height)
|
||||
final_bbox, final_area_ratio = get_mask_metrics(work_mask, (work_width, work_height))
|
||||
repair_info["has_mask"] = final_bbox is not None and final_area_ratio > 0.0
|
||||
repair_info["mask_bbox"] = list(final_bbox) if final_bbox is not None else None
|
||||
repair_info["mask_area_ratio"] = final_area_ratio
|
||||
return latent, work_image, work_mask, repair_info, recommended, positive, negative
|
||||
work_image = resize_image_to(crop_image_to(image, crop_box), work_width, work_height)
|
||||
work_mask = resize_mask_to(crop_mask_to(processed_mask, crop_box), work_width, work_height)
|
||||
return finalize(
|
||||
work_image,
|
||||
work_mask,
|
||||
work_size=(work_width, work_height),
|
||||
crop_box=crop_box,
|
||||
crop_scale=crop_scale,
|
||||
)
|
||||
|
||||
if effective_scope == "canvas":
|
||||
canvas_info = build_canvas_info(
|
||||
@@ -251,18 +330,12 @@ class LLSSimpleRepairPrepare:
|
||||
original_box=original_box,
|
||||
)
|
||||
work_mask = merge_masks(user_mask, canvas_mask, (work_width, work_height))
|
||||
latent_samples = vae.encode(work_image)
|
||||
latent = {"samples": latent_samples, "source": "repair_prepare_canvas"}
|
||||
if effective_kernel == "latent_mask":
|
||||
latent["noise_mask"] = make_noise_mask(work_mask, latent_samples)
|
||||
|
||||
repair_info["work_size"] = (work_width, work_height)
|
||||
repair_info["original_box_in_canvas"] = list(original_box)
|
||||
final_bbox, final_area_ratio = get_mask_metrics(work_mask, (work_width, work_height))
|
||||
repair_info["has_mask"] = final_bbox is not None and final_area_ratio > 0.0
|
||||
repair_info["mask_bbox"] = list(final_bbox) if final_bbox is not None else None
|
||||
repair_info["mask_area_ratio"] = final_area_ratio
|
||||
return latent, work_image, work_mask, repair_info, recommended, positive, negative
|
||||
return finalize(
|
||||
work_image,
|
||||
work_mask,
|
||||
work_size=(work_width, work_height),
|
||||
original_box_in_canvas=original_box,
|
||||
)
|
||||
|
||||
raise RuntimeError(f"[LLS] Unsupported repair scope '{effective_scope}'.")
|
||||
|
||||
|
||||
+42
-7
@@ -59,8 +59,14 @@ def normalize_model_info(model_info: dict[str, Any] | str | None) -> dict[str, A
|
||||
raw_family = raw.get("model_family") or raw.get("family")
|
||||
return {
|
||||
"model_family": _normalize_model_family(raw_family),
|
||||
"model_role": str(raw.get("model_role") or raw.get("role") or "unknown"),
|
||||
"model_role": str(raw.get("model_role") or raw.get("role") or "unknown").strip().lower(),
|
||||
"profile_id": str(raw.get("profile_id") or ""),
|
||||
"backend_type": str(raw.get("backend_type") or "none").strip().lower(),
|
||||
"sampler_strategy": str(raw.get("sampler_strategy") or "").strip().lower(),
|
||||
"loader_strategy": str(raw.get("loader_strategy") or "").strip().lower(),
|
||||
"supports_inpaint_native": _coerce_bool(raw.get("supports_inpaint_native"), False),
|
||||
"supports_image_edit_native": _coerce_bool(raw.get("supports_image_edit_native"), False),
|
||||
"preferred_edit_backend": str(raw.get("preferred_edit_backend") or "").strip().lower(),
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +90,27 @@ def normalize_repair_info(repair_info: dict[str, Any] | str | None) -> dict[str,
|
||||
info["invert_mask"] = _coerce_bool(info.get("invert_mask"), False)
|
||||
info["recommended_denoise"] = _coerce_float(info.get("recommended_denoise"), 0.55)
|
||||
info["model_family"] = _normalize_model_family(info.get("model_family") or model_info["model_family"])
|
||||
info["model_role"] = str(info.get("model_role") or model_info["model_role"])
|
||||
info["model_role"] = str(info.get("model_role") or model_info["model_role"]).strip().lower()
|
||||
info["profile_id"] = str(info.get("profile_id") or model_info["profile_id"])
|
||||
info["backend_type"] = str(info.get("backend_type") or model_info["backend_type"] or "none").strip().lower()
|
||||
info["sampler_strategy"] = str(info.get("sampler_strategy") or model_info["sampler_strategy"]).strip().lower()
|
||||
info["loader_strategy"] = str(info.get("loader_strategy") or model_info["loader_strategy"]).strip().lower()
|
||||
info["supports_inpaint_native"] = _coerce_bool(
|
||||
info.get("supports_inpaint_native", model_info["supports_inpaint_native"]),
|
||||
model_info["supports_inpaint_native"],
|
||||
)
|
||||
info["supports_image_edit_native"] = _coerce_bool(
|
||||
info.get("supports_image_edit_native", model_info["supports_image_edit_native"]),
|
||||
model_info["supports_image_edit_native"],
|
||||
)
|
||||
info["preferred_edit_backend"] = str(
|
||||
info.get("preferred_edit_backend") or model_info["preferred_edit_backend"] or ""
|
||||
).strip().lower()
|
||||
info["backend_name"] = str(info.get("backend_name") or "")
|
||||
info["routing_reason"] = str(info.get("routing_reason") or "")
|
||||
info["execution_path"] = str(info.get("execution_path") or "")
|
||||
info["model_patch"] = str(info.get("model_patch") or "")
|
||||
info["model_patch_strength"] = _coerce_float(info.get("model_patch_strength"), 1.0)
|
||||
info["repair_payload_version"] = str(info.get("repair_payload_version") or "1.0")
|
||||
info["warnings"] = _normalize_warning_list(info.get("warnings"))
|
||||
return info
|
||||
@@ -140,15 +162,12 @@ def resolve_repair_kernel(
|
||||
if kernel != "auto":
|
||||
if kernel not in _REPAIR_KERNEL_VALUES:
|
||||
raise RuntimeError(f"[LLS] Unsupported repair_kernel '{requested_kernel}'.")
|
||||
if kernel == "native_fill" and not normalized_model["supports_inpaint_native"]:
|
||||
if kernel == "native_fill" and not _supports_native_repair(normalized_model):
|
||||
warnings.append("native_fill requested but unsupported; falling back to vae_inpaint")
|
||||
return "vae_inpaint", warnings
|
||||
return kernel, warnings
|
||||
|
||||
if (
|
||||
normalized_model["model_role"] in {"inpaint", "fill", "edit"}
|
||||
and normalized_model["supports_inpaint_native"]
|
||||
):
|
||||
if normalized_model["model_role"] in {"inpaint", "fill", "edit"} and _supports_native_repair(normalized_model):
|
||||
return "native_fill", warnings
|
||||
|
||||
if scope != "canvas" and mask_area_ratio <= 0.20 and task_hint in _TASK_HINT_LATENT_MASK:
|
||||
@@ -157,6 +176,22 @@ def resolve_repair_kernel(
|
||||
return "vae_inpaint", warnings
|
||||
|
||||
|
||||
def _supports_native_repair(model_info: dict[str, Any]) -> bool:
|
||||
if _coerce_bool(model_info.get("supports_inpaint_native"), False):
|
||||
return True
|
||||
|
||||
family = _normalize_model_family(model_info.get("model_family"))
|
||||
role = str(model_info.get("model_role") or "").strip().lower()
|
||||
backend_type = str(model_info.get("backend_type") or "").strip().lower()
|
||||
preferred_backend = str(model_info.get("preferred_edit_backend") or "").strip().lower()
|
||||
supports_image_edit_native = _coerce_bool(model_info.get("supports_image_edit_native"), False)
|
||||
|
||||
if family.startswith("FLUX") and role in {"inpaint", "edit", "fill"}:
|
||||
return supports_image_edit_native or backend_type == "flux_edit" or preferred_backend == "flux"
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def recommend_denoise(task_hint: str, scope: str, kernel: str, auto_recommend: str) -> float:
|
||||
if auto_recommend == "disabled":
|
||||
if scope == "canvas":
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
comfy_core_nodes = importlib.import_module("nodes")
|
||||
except Exception as exc: # pragma: no cover - runtime-only import
|
||||
comfy_core_nodes = None
|
||||
_CORE_NODES_ERR = exc
|
||||
else: # pragma: no cover - runtime-only import
|
||||
_CORE_NODES_ERR = None
|
||||
|
||||
try:
|
||||
nodes_differential_diffusion = importlib.import_module("comfy_extras.nodes_differential_diffusion")
|
||||
except Exception as exc: # pragma: no cover - runtime-only import
|
||||
nodes_differential_diffusion = None
|
||||
_DIFFUSION_ERR = exc
|
||||
else: # pragma: no cover - runtime-only import
|
||||
_DIFFUSION_ERR = None
|
||||
|
||||
|
||||
def _require_class(module: Any, class_name: str, import_error: Exception | None = None):
|
||||
if module is None:
|
||||
raise RuntimeError(
|
||||
f"[LLS] Required ComfyUI runtime component '{class_name}' is unavailable."
|
||||
) from import_error
|
||||
cls = getattr(module, class_name, None)
|
||||
if cls is None:
|
||||
raise RuntimeError(
|
||||
f"[LLS] Required ComfyUI runtime component '{class_name}' is unavailable."
|
||||
) from import_error
|
||||
return cls
|
||||
|
||||
|
||||
def _unwrap_result(result: Any):
|
||||
if hasattr(result, "result"):
|
||||
return result.result
|
||||
return result
|
||||
|
||||
|
||||
def _unwrap_first(result: Any):
|
||||
values = _unwrap_result(result)
|
||||
if isinstance(values, tuple):
|
||||
return values[0]
|
||||
if isinstance(values, list):
|
||||
return values[0]
|
||||
return values
|
||||
|
||||
|
||||
def _unwrap_three(result: Any):
|
||||
values = _unwrap_result(result)
|
||||
if isinstance(values, tuple):
|
||||
return values[:3]
|
||||
if isinstance(values, list):
|
||||
return tuple(values[:3])
|
||||
raise RuntimeError("[LLS] Expected a three-value runtime result from ComfyUI.")
|
||||
|
||||
|
||||
def encode_inpaint_conditioning(positive, negative, vae, pixels, mask, *, noise_mask: bool = True):
|
||||
node_cls = _require_class(comfy_core_nodes, "InpaintModelConditioning", _CORE_NODES_ERR)
|
||||
node = node_cls()
|
||||
if not hasattr(node, "encode"):
|
||||
raise RuntimeError(
|
||||
"[LLS] ComfyUI InpaintModelConditioning is missing the expected encode() method."
|
||||
)
|
||||
return _unwrap_three(
|
||||
node.encode(
|
||||
positive,
|
||||
negative,
|
||||
pixels,
|
||||
vae,
|
||||
mask,
|
||||
bool(noise_mask),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def apply_differential_diffusion(model, *, strength: float = 1.0):
|
||||
node_cls = _require_class(
|
||||
nodes_differential_diffusion,
|
||||
"DifferentialDiffusion",
|
||||
_DIFFUSION_ERR,
|
||||
)
|
||||
node = node_cls()
|
||||
if hasattr(node, "execute"):
|
||||
return _unwrap_first(node.execute(model, float(strength)))
|
||||
if hasattr(node, "apply"):
|
||||
return _unwrap_first(node.apply(model, float(strength)))
|
||||
if hasattr(node, "patch"):
|
||||
return _unwrap_first(node.patch(model, float(strength)))
|
||||
if hasattr(node_cls, "execute"):
|
||||
return _unwrap_first(node_cls.execute(model, float(strength)))
|
||||
raise RuntimeError(
|
||||
"[LLS] ComfyUI DifferentialDiffusion is missing a supported execute/apply method."
|
||||
)
|
||||
+19
-1
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
from ..repair import runtime as repair_runtime
|
||||
from ..repair.repair_utils import normalize_model_info, normalize_repair_info, resolve_adapter_mode
|
||||
from ..utils.model_info import (
|
||||
FAMILY_DEFAULT_PRESET,
|
||||
@@ -152,6 +153,18 @@ def _normalize_ksampler_compat_values(
|
||||
return denoise_mode, adapter_mode, flux_guidance, model_family
|
||||
|
||||
|
||||
def _apply_repair_model_patch(model, repair_meta):
|
||||
if repair_meta is None:
|
||||
return model
|
||||
|
||||
patch_name = str(repair_meta.get("model_patch") or "").strip().lower()
|
||||
if patch_name == "differential_diffusion":
|
||||
strength = float(repair_meta.get("model_patch_strength", 1.0))
|
||||
return repair_runtime.apply_differential_diffusion(model, strength=strength)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def _common_ksampler(
|
||||
model,
|
||||
seed: int,
|
||||
@@ -404,10 +417,12 @@ class LLSSimpleKSampler:
|
||||
if seed == -1:
|
||||
actual_seed = random.randint(0, 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
sample_model = _apply_repair_model_patch(model, repair_meta)
|
||||
|
||||
# 复用 ComfyUI 原生 common_ksampler
|
||||
try:
|
||||
result_latent = _common_ksampler(
|
||||
model=model,
|
||||
model=sample_model,
|
||||
seed=actual_seed,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
@@ -438,6 +453,9 @@ class LLSSimpleKSampler:
|
||||
"repair_mode": repair_meta is not None,
|
||||
"repair_scope": repair_meta.get("repair_scope") if repair_meta else None,
|
||||
"repair_kernel": repair_meta.get("repair_kernel") if repair_meta else None,
|
||||
"repair_backend": repair_meta.get("backend_name") if repair_meta else None,
|
||||
"repair_execution_path": repair_meta.get("execution_path") if repair_meta else None,
|
||||
"repair_model_patch": repair_meta.get("model_patch") if repair_meta else None,
|
||||
"guidance_used": bool(guidance_stack),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
@@ -47,6 +48,16 @@ class FakeTensor:
|
||||
original_box=original_box,
|
||||
)
|
||||
|
||||
def masked_fill(self, mask, fill_value):
|
||||
del mask
|
||||
return FakeTensor(
|
||||
self.shape,
|
||||
label=f"{self.label}:masked[{fill_value}]",
|
||||
crop_box=self.crop_box,
|
||||
fill_mode=self.fill_mode,
|
||||
original_box=self.original_box,
|
||||
)
|
||||
|
||||
|
||||
class FakeMask:
|
||||
def __init__(self, shape, mask_bbox=None, mask_area_ratio=0.0, label="mask", crop_box=None):
|
||||
@@ -262,9 +273,46 @@ class FakeLatentTensor:
|
||||
|
||||
|
||||
class FakeVAE:
|
||||
def __init__(self, latent_channels=4, downscale_ratio=8):
|
||||
self.latent_channels = int(latent_channels)
|
||||
self.downscale_ratio = int(downscale_ratio)
|
||||
|
||||
def encode(self, image):
|
||||
batch, height, width, _ = image.shape
|
||||
return FakeLatentTensor((batch, 4, height // 8, width // 8))
|
||||
latent_height = max(1, height // self.downscale_ratio)
|
||||
latent_width = max(1, width // self.downscale_ratio)
|
||||
return FakeLatentTensor((batch, self.latent_channels, latent_height, latent_width))
|
||||
|
||||
|
||||
class FakeModel:
|
||||
def __init__(
|
||||
self,
|
||||
family="SDXL",
|
||||
model_role="base",
|
||||
supports_inpaint_native=False,
|
||||
supports_image_edit_native=False,
|
||||
preferred_edit_backend=None,
|
||||
model_name="demo-model.safetensors",
|
||||
profile_id="",
|
||||
backend_type="",
|
||||
sampler_strategy="",
|
||||
loader_strategy="",
|
||||
):
|
||||
self._lls_family = family
|
||||
self._lls_model_role = model_role
|
||||
self._lls_supports_inpaint_native = supports_inpaint_native
|
||||
self._lls_supports_image_edit_native = supports_image_edit_native
|
||||
self._lls_preferred_edit_backend = preferred_edit_backend
|
||||
self._lls_model_name = model_name
|
||||
self._lls_checkpoint_name = model_name
|
||||
self._lls_profile_id = profile_id
|
||||
self._lls_backend_type = backend_type
|
||||
self._lls_sampler_strategy = sampler_strategy
|
||||
self._lls_loader_strategy = loader_strategy
|
||||
|
||||
|
||||
def make_conditioning(label):
|
||||
return [[label, {"origin": label}]]
|
||||
|
||||
|
||||
def load_plugin_package():
|
||||
@@ -281,3 +329,7 @@ def load_plugin_package():
|
||||
sys.modules[MODULE_NAME] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def import_plugin_submodule(plugin, dotted_name: str):
|
||||
return importlib.import_module(f"{plugin.__name__}.{dotted_name}")
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from .test_repair_helpers import FakeMask, FakeVAE, FakeTensor, load_plugin_package
|
||||
from .test_repair_helpers import (
|
||||
FakeMask,
|
||||
FakeModel,
|
||||
FakeTensor,
|
||||
FakeVAE,
|
||||
import_plugin_submodule,
|
||||
load_plugin_package,
|
||||
make_conditioning,
|
||||
)
|
||||
except ImportError: # pragma: no cover - discovery mode imports from top level
|
||||
from test_repair_helpers import FakeMask, FakeVAE, FakeTensor, load_plugin_package
|
||||
from test_repair_helpers import (
|
||||
FakeMask,
|
||||
FakeModel,
|
||||
FakeTensor,
|
||||
FakeVAE,
|
||||
import_plugin_submodule,
|
||||
load_plugin_package,
|
||||
make_conditioning,
|
||||
)
|
||||
|
||||
|
||||
class TestRepairPrepare(unittest.TestCase):
|
||||
@@ -234,6 +251,78 @@ class TestRepairPrepare(unittest.TestCase):
|
||||
self.assertIs(positive_out, positive)
|
||||
self.assertIs(negative_out, negative)
|
||||
|
||||
def test_prepare_region_routes_flux_fill_profile_to_native_backend(self):
|
||||
plugin = load_plugin_package()
|
||||
node = plugin.NODE_CLASS_MAPPINGS["LLSSimpleRepairPrepare"]()
|
||||
repair_runtime = import_plugin_submodule(plugin, "repair.runtime")
|
||||
|
||||
image = FakeTensor((1, 1024, 1024, 3), label="flux-region-image")
|
||||
mask = FakeMask((1, 1024, 1024), mask_bbox=(192, 192, 512, 512), mask_area_ratio=0.10)
|
||||
model = FakeModel(
|
||||
family="FLUX_DEV",
|
||||
model_role="fill",
|
||||
supports_inpaint_native=False,
|
||||
supports_image_edit_native=True,
|
||||
preferred_edit_backend="flux",
|
||||
profile_id="flux_edit",
|
||||
backend_type="flux_edit",
|
||||
sampler_strategy="flux_guided",
|
||||
loader_strategy="flux_split_or_bundle",
|
||||
)
|
||||
native_positive = make_conditioning("native-positive")
|
||||
native_negative = make_conditioning("native-negative")
|
||||
native_latent = {"samples": "native-latent", "noise_mask": "native-mask"}
|
||||
|
||||
with mock.patch.object(
|
||||
repair_runtime,
|
||||
"encode_inpaint_conditioning",
|
||||
return_value=(native_positive, native_negative, native_latent),
|
||||
) as encode_inpaint:
|
||||
latent, work_image, work_mask, repair_info, recommended, positive, negative = node.prepare(
|
||||
image=image,
|
||||
mask=mask,
|
||||
vae=FakeVAE(latent_channels=16, downscale_ratio=16),
|
||||
repair_scope="region",
|
||||
repair_kernel="auto",
|
||||
task_hint="fill",
|
||||
mask_grow=8,
|
||||
mask_blur=4.0,
|
||||
mask_threshold=0.5,
|
||||
invert_mask=False,
|
||||
crop_context=64,
|
||||
crop_context_factor=1.5,
|
||||
min_size=512,
|
||||
max_size=1024,
|
||||
resize_mode="fit",
|
||||
expand_left=0,
|
||||
expand_right=0,
|
||||
expand_top=0,
|
||||
expand_bottom=0,
|
||||
canvas_fill="edge",
|
||||
auto_recommend="enabled",
|
||||
model=model,
|
||||
model_info=None,
|
||||
positive=make_conditioning("positive"),
|
||||
negative=make_conditioning("negative"),
|
||||
)
|
||||
|
||||
encode_inpaint.assert_called_once()
|
||||
self.assertIs(latent, native_latent)
|
||||
self.assertEqual(latent["source"], "repair_prepare_region")
|
||||
self.assertEqual(work_image.shape, (1, 1024, 1024, 3))
|
||||
self.assertEqual(work_mask.shape, (1, 1024, 1024))
|
||||
self.assertEqual(repair_info["repair_kernel"], "native_fill")
|
||||
self.assertEqual(repair_info["backend_name"], "flux")
|
||||
self.assertEqual(repair_info["execution_path"], "native_repair")
|
||||
self.assertEqual(repair_info["model_patch"], "differential_diffusion")
|
||||
self.assertEqual(repair_info["profile_id"], "flux_edit")
|
||||
self.assertEqual(repair_info["backend_type"], "flux_edit")
|
||||
self.assertEqual(repair_info["sampler_strategy"], "flux_guided")
|
||||
self.assertTrue(repair_info["supports_image_edit_native"])
|
||||
self.assertGreater(recommended, 0.0)
|
||||
self.assertIs(positive, native_positive)
|
||||
self.assertIs(negative, native_negative)
|
||||
|
||||
def test_prepare_requires_vae(self):
|
||||
plugin = load_plugin_package()
|
||||
node = plugin.NODE_CLASS_MAPPINGS["LLSSimpleRepairPrepare"]()
|
||||
|
||||
@@ -65,8 +65,9 @@ class TestRepairRegistration(unittest.TestCase):
|
||||
):
|
||||
self.assertIn(field, required)
|
||||
|
||||
for field in ("model_info", "positive", "negative"):
|
||||
for field in ("model", "model_info", "positive", "negative"):
|
||||
self.assertIn(field, optional)
|
||||
self.assertEqual(optional["model"], ("MODEL",))
|
||||
self.assertEqual(required["image"], ("IMAGE",))
|
||||
self.assertEqual(required["mask"], ("MASK",))
|
||||
self.assertEqual(required["vae"], ("VAE",))
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from .test_repair_helpers import FakeModel, import_plugin_submodule, load_plugin_package
|
||||
except ImportError: # pragma: no cover - discovery mode imports from top level
|
||||
from test_repair_helpers import FakeModel, import_plugin_submodule, load_plugin_package
|
||||
|
||||
|
||||
class TestRepairRegistry(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.plugin = load_plugin_package()
|
||||
self.registry = import_plugin_submodule(self.plugin, "repair.backends.registry")
|
||||
|
||||
def test_native_fill_routes_flux_edit_profile_to_flux_backend(self):
|
||||
model = FakeModel(
|
||||
family="FLUX_DEV",
|
||||
model_role="fill",
|
||||
supports_inpaint_native=False,
|
||||
supports_image_edit_native=True,
|
||||
preferred_edit_backend="flux",
|
||||
profile_id="flux_edit",
|
||||
backend_type="flux_edit",
|
||||
sampler_strategy="flux_guided",
|
||||
loader_strategy="flux_split_or_bundle",
|
||||
)
|
||||
|
||||
backend, routing = self.registry.resolve_backend("native_fill", model=model)
|
||||
|
||||
self.assertEqual(backend.backend_name, "flux")
|
||||
self.assertEqual(routing.backend_name, "flux")
|
||||
self.assertEqual(routing.routing_reason, "profile.backend_type")
|
||||
self.assertEqual(routing.execution_path, "native_repair")
|
||||
self.assertEqual(routing.profile["profile_id"], "flux_edit")
|
||||
self.assertEqual(routing.profile["backend_type"], "flux_edit")
|
||||
self.assertEqual(routing.capabilities["model_family"], "FLUX_DEV")
|
||||
|
||||
def test_fallback_routes_sdxl_base_profile_to_sdxl_backend(self):
|
||||
model = FakeModel(
|
||||
family="SDXL",
|
||||
model_role="base",
|
||||
supports_inpaint_native=False,
|
||||
supports_image_edit_native=False,
|
||||
preferred_edit_backend=None,
|
||||
profile_id="sdxl_base",
|
||||
backend_type="none",
|
||||
sampler_strategy="standard_k",
|
||||
loader_strategy="sdxl_checkpoint",
|
||||
)
|
||||
|
||||
backend, routing = self.registry.resolve_backend("vae_inpaint", model=model)
|
||||
|
||||
self.assertEqual(backend.backend_name, "sdxl")
|
||||
self.assertEqual(routing.routing_reason, "profile.family_fallback")
|
||||
self.assertEqual(routing.execution_path, "fallback_repair")
|
||||
self.assertEqual(routing.profile["profile_id"], "sdxl_base")
|
||||
|
||||
def test_fallback_routes_sd15_base_profile_to_generic_backend(self):
|
||||
model = FakeModel(
|
||||
family="SD1.5",
|
||||
model_role="base",
|
||||
supports_inpaint_native=False,
|
||||
supports_image_edit_native=False,
|
||||
preferred_edit_backend=None,
|
||||
profile_id="sd15_base",
|
||||
backend_type="none",
|
||||
sampler_strategy="standard_k",
|
||||
loader_strategy="sd15_checkpoint",
|
||||
)
|
||||
|
||||
backend, routing = self.registry.resolve_backend("latent_mask", model=model)
|
||||
|
||||
self.assertEqual(backend.backend_name, "generic")
|
||||
self.assertEqual(routing.routing_reason, "profile.family_fallback")
|
||||
self.assertEqual(routing.execution_path, "fallback_repair")
|
||||
self.assertEqual(routing.profile["profile_id"], "sd15_base")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -112,6 +112,65 @@ class TestRepairSampler(unittest.TestCase):
|
||||
self.assertEqual(payload["repair_scope"], "region")
|
||||
self.assertEqual(payload["repair_kernel"], "latent_mask")
|
||||
|
||||
def test_sampler_patches_native_flux_repair_model_before_sampling(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_repair.sampling import nodes as sampling_nodes
|
||||
|
||||
node = sampling_nodes.LLSSimpleKSampler()
|
||||
latent = {"samples": FakeLatentTensor((1, 16, 64, 64)), "source": "repair_prepare_region"}
|
||||
model = object()
|
||||
patched_model = object()
|
||||
with mock.patch.object(sampling_nodes, "comfy_sample", object()), mock.patch.object(
|
||||
sampling_nodes,
|
||||
"comfy_samplers",
|
||||
types.SimpleNamespace(KSampler=types.SimpleNamespace(SAMPLERS=["euler"], SCHEDULERS=["simple"])),
|
||||
), mock.patch.object(
|
||||
sampling_nodes,
|
||||
"_apply_repair_model_patch",
|
||||
return_value=patched_model,
|
||||
) as patch_model, mock.patch.object(
|
||||
sampling_nodes,
|
||||
"_common_ksampler",
|
||||
return_value={"samples": "done"},
|
||||
) as common:
|
||||
_result_latent, sample_info = node.sample(
|
||||
model=model,
|
||||
positive="positive",
|
||||
negative="negative",
|
||||
latent_image=latent,
|
||||
quality_preset="Manual",
|
||||
seed=7,
|
||||
steps=20,
|
||||
cfg=1.0,
|
||||
sampler_name="euler",
|
||||
scheduler="simple",
|
||||
denoise=0.33,
|
||||
denoise_mode="auto_from_repair",
|
||||
adapter_mode="auto",
|
||||
flux_guidance=3.5,
|
||||
model_family="Auto",
|
||||
repair_info={
|
||||
"repair_scope": "region",
|
||||
"repair_kernel": "native_fill",
|
||||
"recommended_denoise": 0.61,
|
||||
"model_family": "FLUX_DEV",
|
||||
"model_role": "fill",
|
||||
"backend_name": "flux",
|
||||
"execution_path": "native_repair",
|
||||
"model_patch": "differential_diffusion",
|
||||
"model_patch_strength": 1.0,
|
||||
},
|
||||
guidance_stack=None,
|
||||
model_info=None,
|
||||
)
|
||||
|
||||
patch_model.assert_called_once()
|
||||
self.assertIs(common.call_args.kwargs["model"], patched_model)
|
||||
payload = json.loads(sample_info)
|
||||
self.assertEqual(payload["repair_kernel"], "native_fill")
|
||||
self.assertEqual(payload["repair_backend"], "flux")
|
||||
self.assertEqual(payload["repair_execution_path"], "native_repair")
|
||||
|
||||
def test_sampler_recovers_shifted_values_from_broken_widget_order(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_repair.sampling import nodes as sampling_nodes
|
||||
|
||||
@@ -52,6 +52,13 @@ class TestRepairUtils(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
utils.normalize_model_info({"supports_inpaint_native": 1})["supports_inpaint_native"]
|
||||
)
|
||||
self.assertTrue(
|
||||
utils.normalize_model_info({"supports_image_edit_native": "true"})["supports_image_edit_native"]
|
||||
)
|
||||
self.assertEqual(
|
||||
utils.normalize_model_info({"backend_type": "flux_edit"})["backend_type"],
|
||||
"flux_edit",
|
||||
)
|
||||
|
||||
def test_canonicalized_family_feeds_sd_classic_adapter_resolution(self):
|
||||
utils = load_repair_utils()
|
||||
@@ -100,9 +107,16 @@ class TestRepairUtils(unittest.TestCase):
|
||||
utils.resolve_repair_kernel(
|
||||
"auto",
|
||||
scope="region",
|
||||
task_hint="repair",
|
||||
task_hint="fill",
|
||||
mask_area_ratio=0.05,
|
||||
model_info={"model_role": "fill", "supports_inpaint_native": True},
|
||||
model_info={
|
||||
"model_family": "FLUX_DEV",
|
||||
"model_role": "fill",
|
||||
"backend_type": "flux_edit",
|
||||
"supports_inpaint_native": False,
|
||||
"supports_image_edit_native": True,
|
||||
"preferred_edit_backend": "flux",
|
||||
},
|
||||
)[0],
|
||||
"native_fill",
|
||||
)
|
||||
@@ -127,6 +141,29 @@ class TestRepairUtils(unittest.TestCase):
|
||||
"vae_inpaint",
|
||||
)
|
||||
|
||||
def test_resolve_repair_kernel_downgrades_flux_base_profile_from_native_fill(self):
|
||||
utils = load_repair_utils()
|
||||
|
||||
kernel, warnings = utils.resolve_repair_kernel(
|
||||
"native_fill",
|
||||
scope="region",
|
||||
task_hint="repair",
|
||||
mask_area_ratio=0.05,
|
||||
model_info={
|
||||
"model_family": "FLUX_DEV",
|
||||
"model_role": "base",
|
||||
"backend_type": "none",
|
||||
"supports_inpaint_native": False,
|
||||
"supports_image_edit_native": False,
|
||||
"preferred_edit_backend": None,
|
||||
},
|
||||
)
|
||||
self.assertEqual(kernel, "vae_inpaint")
|
||||
self.assertEqual(
|
||||
warnings,
|
||||
["native_fill requested but unsupported; falling back to vae_inpaint"],
|
||||
)
|
||||
|
||||
def test_resolve_repair_kernel_downgrades_explicit_native_fill_when_unsupported(self):
|
||||
utils = load_repair_utils()
|
||||
|
||||
|
||||
+27
-12
@@ -2,7 +2,7 @@
|
||||
"id": "196d3f49-c541-4fcd-b800-ad54aecdf346",
|
||||
"revision": 0,
|
||||
"last_node_id": 78,
|
||||
"last_link_id": 128,
|
||||
"last_link_id": 129,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 28,
|
||||
@@ -1040,15 +1040,15 @@
|
||||
"Manual",
|
||||
452874131519053,
|
||||
"randomize",
|
||||
20,
|
||||
3,
|
||||
30,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
1,
|
||||
"manual",
|
||||
"flux",
|
||||
"auto",
|
||||
3.5,
|
||||
"FLUX_SCHNELL",
|
||||
"Auto",
|
||||
""
|
||||
]
|
||||
},
|
||||
@@ -1143,6 +1143,12 @@
|
||||
"type": "VAE",
|
||||
"link": 74
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"shape": 7,
|
||||
"type": "MODEL",
|
||||
"link": 129
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"shape": 7,
|
||||
@@ -1210,9 +1216,9 @@
|
||||
"Node name for S&R": "LLSSimpleRepairPrepare"
|
||||
},
|
||||
"widgets_values": [
|
||||
"crop",
|
||||
"latent_mask",
|
||||
"repair",
|
||||
"auto",
|
||||
"auto",
|
||||
"replace",
|
||||
24,
|
||||
8,
|
||||
0.5,
|
||||
@@ -1251,7 +1257,8 @@
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
68
|
||||
68,
|
||||
129
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1283,7 +1290,7 @@
|
||||
"Node name for S&R": "LLSSimpleCheckpointLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"flux1-schnell-fp8.safetensors",
|
||||
"diffusion_models/fluxFillFP8_v10.safetensors",
|
||||
"Auto",
|
||||
"simple",
|
||||
"auto",
|
||||
@@ -2146,7 +2153,7 @@
|
||||
44,
|
||||
0,
|
||||
52,
|
||||
3,
|
||||
4,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
@@ -2154,7 +2161,7 @@
|
||||
44,
|
||||
1,
|
||||
52,
|
||||
4,
|
||||
5,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
@@ -2420,6 +2427,14 @@
|
||||
69,
|
||||
3,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
129,
|
||||
45,
|
||||
0,
|
||||
52,
|
||||
3,
|
||||
"MODEL"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
|
||||
Reference in New Issue
Block a user