diff --git a/FLUX 局部重绘.json b/FLUX 局部重绘.json new file mode 100644 index 0000000..3a05092 --- /dev/null +++ b/FLUX 局部重绘.json @@ -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 +} \ No newline at end of file diff --git a/SDXL版局部重绘SDXL局部重绘1.0.json b/SDXL版局部重绘SDXL局部重绘1.0.json new file mode 100644 index 0000000..1c0cf88 --- /dev/null +++ b/SDXL版局部重绘SDXL局部重绘1.0.json @@ -0,0 +1 @@ +{"id":"a3fa1de3-74f4-49de-921a-07bd5f8c6b87","revision":0,"last_node_id":21,"last_link_id":26,"nodes":[{"id":18,"type":"Image Comparer (rgthree)","pos":{"0":17.813167572021484,"1":-31.999448776245117},"size":{"0":468.2283630371094,"1":681.1610107421875},"flags":{},"order":15,"mode":0,"inputs":[{"dir":3,"label":"图像_A","name":"image_a","type":"IMAGE","link":25},{"dir":3,"label":"图像_B","name":"image_b","type":"IMAGE","link":26}],"outputs":[],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"comparer_mode":"Slide"},"widgets_values":[[{"name":"A","selected":true,"url":"https://liblibai-tmp-image.liblib.cloud/sd-images/520ad7ec26f64e5c101c5a554010aa4a8b3472ecc2bbd046715e0762a71d924c.png?filename=sd-images/520ad7ec26f64e5c101c5a554010aa4a8b3472ecc2bbd046715e0762a71d924c.png&type=output&rand=0.4960752857567199"},{"name":"B","selected":true,"url":"https://liblibai-tmp-image.liblib.cloud/sd-images/520d126264b83073237569940318cd63ae47583a5eafd0810f8bba9111f00d09.png?filename=sd-images/520d126264b83073237569940318cd63ae47583a5eafd0810f8bba9111f00d09.png&type=output&rand=0.7331449171495302"}]],"color":"#223","bgcolor":"#335"},{"id":16,"type":"SaveImage","pos":{"0":2048.770263671875,"1":-53.6751594543457},"size":{"0":434.68963623046875,"1":697.916259765625},"flags":{},"order":14,"mode":0,"inputs":[{"label":"图像","localized_name":"图片","name":"images","type":"IMAGE","link":22}],"outputs":[],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"SaveImage"},"widgets_values":["ComfyUI"],"color":"#223","bgcolor":"#335"},{"id":6,"type":"CLIPTextEncode","pos":{"0":1200.54248046875,"1":487.1212158203125},"size":{"0":404.938720703125,"1":58},"flags":{"collapsed":true,"pinned":true},"order":7,"mode":0,"inputs":[{"label":"CLIP","localized_name":"clip","name":"clip","type":"CLIP","link":3},{"label":"文本","name":"text","type":"STRING","widget":{"name":"text"},"link":21}],"outputs":[{"label":"条件","localized_name":"条件","name":"CONDITIONING","type":"CONDITIONING","slot_index":0,"links":[13]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]},{"id":8,"type":"VAEDecode","pos":{"0":982.548583984375,"1":489.24566650390625},"size":{"0":210,"1":46},"flags":{"collapsed":true,"pinned":true},"order":12,"mode":0,"inputs":[{"label":"Latent","localized_name":"Latent","name":"samples","type":"LATENT","link":7},{"label":"VAE","localized_name":"vae","name":"vae","type":"VAE","link":8}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[23]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"VAEDecode"},"widgets_values":[]},{"id":15,"type":"LibLibTranslate","pos":{"0":956.8103637695312,"1":252.75009155273438},"size":{"0":417.13525390625,"1":239.72265625},"flags":{"pinned":true},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"text","localized_name":"text","name":"text","type":"STRING","links":[21]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"LibLibTranslate"},"widgets_values":["Chinese","English","美丽的钻石项链,闪烁着彩色的光泽"],"color":"#232","bgcolor":"#353"},{"id":19,"type":"Note","pos":{"0":965.3333740234375,"1":103.11063385009766},"size":{"0":390.7978210449219,"1":88.03952026367188},"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["选择你想要的大模型,sdxl和1.5都可以尝试效果,输入要重绘的内容提示词,增加生图的稳定性\n"],"color":"#432","bgcolor":"#653"},{"id":10,"type":"LoadImage","pos":{"0":523.9032592773438,"1":-40.48138427734375},"size":{"0":402.35870361328125,"1":552.4603881835938},"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[10,25]},{"label":"遮罩","localized_name":"遮罩","name":"MASK","type":"MASK","links":[11]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"LoadImage"},"widgets_values":["clipspace-mask-959257.5.png","image",""],"color":"#332922","bgcolor":"#593930","_widget_cache_map":{"087351937cc233a5b72f02c8daf1037fad73165bdac4fe69cd9158eaa1e6adb7.png":{"value":"img/2f3c29283769421e9c82c5aecea17067/087351937cc233a5b72f02c8daf1037fad73165bdac4fe69cd9158eaa1e6adb7.png","name":"087351937cc233a5b72f02c8daf1037fad73165bdac4fe69cd9158eaa1e6adb7.png","url":"https://liblibai-tmp-image.liblib.cloud/img/2f3c29283769421e9c82c5aecea17067/087351937cc233a5b72f02c8daf1037fad73165bdac4fe69cd9158eaa1e6adb7.png","width":680,"height":1536},"clipspace-mask-959257.5.png":{"value":"img/2f3c29283769421e9c82c5aecea17067/4eebddb3d3d6f872e99d5246b69c77cd2c96113686a60e229b6cfb8c8be4fefa.png","name":"clipspace-mask-959257.5.png","url":"https://liblibai-tmp-image.liblib.cloud/img/2f3c29283769421e9c82c5aecea17067/4eebddb3d3d6f872e99d5246b69c77cd2c96113686a60e229b6cfb8c8be4fefa.png","width":680,"height":1536}}},{"id":20,"type":"Note","pos":{"0":1677.2254638671875,"1":582.8976440429688},"size":{"0":365.8736877441406,"1":88},"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["采样器一般是不需要动参数的,如果生图的强度不够大可以增加降噪值或者增加CFG值\n"],"color":"#432","bgcolor":"#653"},{"id":21,"type":"Note","pos":{"0":541.30224609375,"1":548.7765502929688},"size":{"0":365.8736877441406,"1":88},"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["加载图片后右键选择遮罩模式,并将要重绘的区域涂抹出来并保存"],"color":"#432","bgcolor":"#653"},{"id":7,"type":"CLIPTextEncode","pos":{"0":952.3636474609375,"1":536.620361328125},"size":{"0":425.27801513671875,"1":112.84394836425781},"flags":{"pinned":true},"order":8,"mode":0,"inputs":[{"label":"CLIP","localized_name":"clip","name":"clip","type":"CLIP","link":5}],"outputs":[{"label":"条件","localized_name":"条件","name":"CONDITIONING","type":"CONDITIONING","slot_index":0,"links":[14]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"CLIPTextEncode"},"widgets_values":["text, watermark,Deformities, Deformity, six fingers, low quality, wrong perspective, wrong structure, wrong perspective, wrong structure,Excess face, excess organs, excess body parts"],"color":"#322","bgcolor":"#533"},{"id":4,"type":"CheckpointLoaderSimple","pos":{"0":959.9573364257812,"1":-38.68690490722656},"size":{"0":390.39508056640625,"1":98},"flags":{"pinned":true},"order":5,"mode":0,"inputs":[],"outputs":[{"label":"模型","localized_name":"模型","name":"MODEL","type":"MODEL","slot_index":0,"links":[1]},{"label":"CLIP","localized_name":"CLIP","name":"CLIP","type":"CLIP","slot_index":1,"links":[3,5]},{"label":"VAE","localized_name":"VAE","name":"VAE","type":"VAE","slot_index":2,"links":[8,15]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"CheckpointLoaderSimple"},"widgets_values":["juggernautXL_v9-Lightning_4S_V9 + RDPhoto 2"],"color":"#232","bgcolor":"#353","_widget_cache_map":{"AWPainting_v1.3":{"name":"AWPainting_v1.4","value":"ea3f612759734012bdcc5134780825e4"},"juggernautXL_v9-Lightning_4S_V9 + RDPhoto 2":{"name":"juggernautXL_v9-Lightning_4S_V9 + RDPhoto 2","baseType":3,"value":"027512841f8d425c8dbff87d4dbec60d"}}},{"id":11,"type":"InpaintCrop","pos":{"0":1388.11962890625,"1":-44.25188446044922},"size":{"0":255.85467529296875,"1":458},"flags":{"pinned":true},"order":6,"mode":0,"inputs":[{"label":"图像","localized_name":"image","name":"image","type":"IMAGE","link":10},{"label":"遮罩","localized_name":"mask","name":"mask","type":"MASK","link":11},{"label":"上下文遮罩(可选)","localized_name":"optional_context_mask","name":"optional_context_mask","shape":7,"type":"MASK","link":null}],"outputs":[{"label":"接缝","localized_name":"stitch","name":"stitch","type":"STITCH","links":[12]},{"localized_name":"cropped_image","name":"cropped_image","type":"IMAGE","links":[16]},{"localized_name":"cropped_mask","name":"cropped_mask","type":"MASK","links":[17,24]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"InpaintCrop"},"widgets_values":[20,1,true,16,false,16,"bicubic","ranged size",1024,1024,1,512,512,768,768,32]},{"id":3,"type":"KSampler","pos":{"0":1396.42041015625,"1":389.5669860839844},"size":{"0":249.02920532226562,"1":267.654541015625},"flags":{"pinned":true},"order":11,"mode":0,"inputs":[{"label":"模型","localized_name":"模型","name":"model","type":"MODEL","link":1},{"label":"正面条件","localized_name":"正面条件","name":"positive","type":"CONDITIONING","link":18},{"label":"负面条件","localized_name":"负面条件","name":"negative","type":"CONDITIONING","link":19},{"label":"Latent","localized_name":"Latent图像","name":"latent_image","type":"LATENT","link":20}],"outputs":[{"label":"Latent","localized_name":"Latent","name":"LATENT","type":"LATENT","slot_index":0,"links":[7]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"KSampler"},"widgets_values":[77888486311209,"randomize",25,7.49,"dpmpp_2m","karras",0.6],"color":"#232","bgcolor":"#353"},{"id":12,"type":"InpaintStitch","pos":{"0":1701.01025390625,"1":145.17880249023438},"size":{"0":296.1511535644531,"1":78},"flags":{"collapsed":false,"pinned":true},"order":13,"mode":0,"inputs":[{"label":"接缝","localized_name":"stitch","name":"stitch","type":"STITCH","link":12},{"label":"图像","localized_name":"inpainted_image","name":"inpainted_image","type":"IMAGE","link":23}],"outputs":[{"label":"图像","localized_name":"image","name":"image","type":"IMAGE","links":[22,26]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"InpaintStitch"},"widgets_values":["bislerp"]},{"id":13,"type":"InpaintModelConditioning","pos":{"0":1694.41357421875,"1":-50.84837341308594},"size":{"0":315,"1":138},"flags":{"pinned":true},"order":10,"mode":0,"inputs":[{"label":"正面条件","localized_name":"正面条件","name":"positive","type":"CONDITIONING","link":13},{"label":"负面条件","localized_name":"负面条件","name":"negative","type":"CONDITIONING","link":14},{"label":"VAE","localized_name":"vae","name":"vae","type":"VAE","link":15},{"label":"图像","localized_name":"像素","name":"pixels","type":"IMAGE","link":16},{"label":"遮罩","localized_name":"遮罩","name":"mask","type":"MASK","link":17}],"outputs":[{"label":"正面条件","localized_name":"正面条件","name":"positive","type":"CONDITIONING","links":[18]},{"label":"负面条件","localized_name":"负面条件","name":"negative","type":"CONDITIONING","links":[19]},{"label":"Latent","localized_name":"Latent","name":"latent","type":"LATENT","links":[20]}],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"InpaintModelConditioning"},"widgets_values":[true]},{"id":17,"type":"MaskPreview+","pos":{"0":1695.4805908203125,"1":266.7456970214844},"size":{"0":307.07135009765625,"1":273.8615417480469},"flags":{"pinned":true},"order":9,"mode":0,"inputs":[{"label":"遮罩","localized_name":"mask","name":"mask","type":"MASK","link":24}],"outputs":[],"properties":{"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"Node name for S&R":"MaskPreview+"},"widgets_values":[]}],"links":[[1,4,0,3,0,"MODEL"],[3,4,1,6,0,"CLIP"],[5,4,1,7,0,"CLIP"],[7,3,0,8,0,"LATENT"],[8,4,2,8,1,"VAE"],[10,10,0,11,0,"IMAGE"],[11,10,1,11,1,"MASK"],[12,11,0,12,0,"STITCH"],[13,6,0,13,0,"CONDITIONING"],[14,7,0,13,1,"CONDITIONING"],[15,4,2,13,2,"VAE"],[16,11,1,13,3,"IMAGE"],[17,11,2,13,4,"MASK"],[18,13,0,3,1,"CONDITIONING"],[19,13,1,3,2,"CONDITIONING"],[20,13,2,3,3,"LATENT"],[21,15,0,6,1,"STRING"],[22,12,0,16,0,"IMAGE"],[23,8,0,12,1,"IMAGE"],[24,11,2,17,0,"MASK"],[25,10,0,18,0,"IMAGE"],[26,12,0,18,1,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8769226950000005,"offset":[66.39603970840889,295.3206114778465]},"ue_links":[],"VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4} \ No newline at end of file diff --git a/docs/superpowers/plans/2026-05-22-flux-native-repair-routing.md b/docs/superpowers/plans/2026-05-22-flux-native-repair-routing.md new file mode 100644 index 0000000..83b5173 --- /dev/null +++ b/docs/superpowers/plans/2026-05-22-flux-native-repair-routing.md @@ -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** diff --git a/repair/backends/__init__.py b/repair/backends/__init__.py new file mode 100644 index 0000000..a9a2c5b --- /dev/null +++ b/repair/backends/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/repair/backends/base.py b/repair/backends/base.py new file mode 100644 index 0000000..e7ba883 --- /dev/null +++ b/repair/backends/base.py @@ -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 diff --git a/repair/backends/flux.py b/repair/backends/flux.py new file mode 100644 index 0000000..d084068 --- /dev/null +++ b/repair/backends/flux.py @@ -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()) diff --git a/repair/backends/generic.py b/repair/backends/generic.py new file mode 100644 index 0000000..04f6a68 --- /dev/null +++ b/repair/backends/generic.py @@ -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()) diff --git a/repair/backends/registry.py b/repair/backends/registry.py new file mode 100644 index 0000000..da2d9c2 --- /dev/null +++ b/repair/backends/registry.py @@ -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", + ) diff --git a/repair/backends/sdxl.py b/repair/backends/sdxl.py new file mode 100644 index 0000000..b9aa535 --- /dev/null +++ b/repair/backends/sdxl.py @@ -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()) diff --git a/repair/repair_prepare.py b/repair/repair_prepare.py index c96b4d3..638192f 100644 --- a/repair/repair_prepare.py +++ b/repair/repair_prepare.py @@ -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}'.") diff --git a/repair/repair_utils.py b/repair/repair_utils.py index 1c66e9f..96e3a23 100644 --- a/repair/repair_utils.py +++ b/repair/repair_utils.py @@ -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": diff --git a/repair/runtime.py b/repair/runtime.py new file mode 100644 index 0000000..00fd1d0 --- /dev/null +++ b/repair/runtime.py @@ -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." + ) diff --git a/sampling/nodes.py b/sampling/nodes.py index f9ad9f8..197b213 100644 --- a/sampling/nodes.py +++ b/sampling/nodes.py @@ -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), } ) diff --git a/tests/test_repair_helpers.py b/tests/test_repair_helpers.py index 1c75cd6..208214a 100644 --- a/tests/test_repair_helpers.py +++ b/tests/test_repair_helpers.py @@ -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}") diff --git a/tests/test_repair_prepare.py b/tests/test_repair_prepare.py index 4b0eafe..f3ffb9b 100644 --- a/tests/test_repair_prepare.py +++ b/tests/test_repair_prepare.py @@ -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"]() diff --git a/tests/test_repair_registration.py b/tests/test_repair_registration.py index 5273bb9..09d4a48 100644 --- a/tests/test_repair_registration.py +++ b/tests/test_repair_registration.py @@ -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",)) diff --git a/tests/test_repair_registry.py b/tests/test_repair_registry.py new file mode 100644 index 0000000..9058535 --- /dev/null +++ b/tests/test_repair_registry.py @@ -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() diff --git a/tests/test_repair_sampler.py b/tests/test_repair_sampler.py index a066705..a917ba6 100644 --- a/tests/test_repair_sampler.py +++ b/tests/test_repair_sampler.py @@ -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 diff --git a/tests/test_repair_utils.py b/tests/test_repair_utils.py index 0ad8055..01659f5 100644 --- a/tests/test_repair_utils.py +++ b/tests/test_repair_utils.py @@ -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() diff --git a/测试工作流.json b/测试工作流.json index f49aebf..80d3a41 100644 --- a/测试工作流.json +++ b/测试工作流.json @@ -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": [