diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f64be2..b669ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## v1.63.0 — unreleased +- 新做法 **换场景·人物不变**:你递给 AI 一张图,AMB 自动抠出人物(inspyrenet-rembg),只重画人物以外的地方,AI 在提示词里写新场景;人物边缘留 6 像素一起重画,接缝更自然。实测 RTX 4050 6GB、SDXL 832×1216 约 30 秒。人物身上的光还是原图的光,换到光线差别很大的场景时看得出来(以后可以加重新打光)。AI 递给做法的图也认 `名字.png [output]` 这种写法了。 +- New way **New scene, same character**: from a picture handed to the AI, the character is cut out (inspyrenet-rembg) and only the rest is drawn anew from the prompt's new scene; 6 px of its edge are redrawn too for the seam. About 30 s for SDXL 832×1216 on an RTX 4050 6 GB. The character keeps the old picture's light, which shows in a very differently lit scene (relighting may come later). A picture handed to a block may also be written `name.png [output]`. + - AI 有了现成的做法可以照着用(参考做法):从擅长这些活的工作流里提炼出来的几块节点,AI 说一声用哪块,AMB 就在画布上搭好、接进你的工作流里,一次调用,不用 AI 一个个节点地搭。AI 开工时会收到这份清单,每块一行。 - **局部重绘·裁切缝合**:把圈出的地方连同周围一圈一起裁出来,放大到 1024 重画,再缝回原图,别处一点不动。AMB 顺手把采样强度调到 0.75,并提醒 AI 把提示词改成只写这块地方(比如“一只手,五根手指”),不然它会把整张图的内容(比如一整个人)画进这一小块里。需要节点包 ComfyUI-Inpaint-CropAndStitch。 - **局部重绘(官方节点)**:没装上面那个包时用它,只用 ComfyUI 自带的节点。 diff --git a/api/ai_pictures.py b/api/ai_pictures.py index 48a2fe0..0b7fce1 100644 --- a/api/ai_pictures.py +++ b/api/ai_pictures.py @@ -149,6 +149,8 @@ def to_input(image): from .mcp_tools import _resolve_folder_image rel = str(image or "").replace("\\", "/").strip("/") + if rel.endswith(" [output]"): # ComfyUI's mark for an output picture, as Load Image shows it + rel = rel[: -len(" [output]")] try: return _resolve_folder_image(rel, "input")[0] except ToolError: diff --git a/api/blocks/change_scene.json b/api/blocks/change_scene.json new file mode 100644 index 0000000..66f5009 --- /dev/null +++ b/api/blocks/change_scene.json @@ -0,0 +1,125 @@ +{ + "job": "repaint", + "rank": 4, + "short": "new background", + "title": { + "zh": "换场景·人物不变", + "en": "New scene, same character" + }, + "about": { + "zh": "你递给 AI 一张图,AMB 自动抠出人物,只重画人物以外的地方:AI 在提示词里写新场景,人物原样留下。人物边缘留几像素一起重画,让新背景和人物接得上。", + "en": "From a picture you hand the AI, the character is cut out automatically and everything else is drawn anew: the prompt says the new scene, the character stays as it was. A few pixels of its edge are redrawn too, so it sits in the new scene." + }, + "source": { + "zh": "常见的“抠图换背景”做法:inspyrenet-rembg 抠人物,反选成背景,用 ComfyUI 自带的局部重绘节点只重画背景", + "en": "The common cut-out-and-replace-the-background way: inspyrenet-rembg finds the character, its inverse is the background, ComfyUI's own inpainting nodes redraw only that" + }, + "packs": { + "InspyrenetRembg": "comfyui-inspyrenet-rembg" + }, + "takes": "picture", + "nodes": { + "picture": { + "class_type": "LoadImage", + "inputs": { + "image": "$image" + } + }, + "cut": { + "class_type": "InspyrenetRembg", + "inputs": { + "image": [ + "picture", + 0 + ], + "torchscript_jit": "default" + } + }, + "background": { + "class_type": "InvertMask", + "inputs": { + "mask": [ + "cut", + 1 + ] + } + }, + "edge": { + "class_type": "GrowMask", + "inputs": { + "mask": [ + "background", + 0 + ], + "expand": 6, + "tapered_corners": true + } + }, + "soft": { + "class_type": "DifferentialDiffusion", + "inputs": { + "model": [ + "$sampler.model", + "*" + ] + }, + "wire_to": "$sampler" + }, + "encode": { + "class_type": "VAEEncode", + "inputs": { + "pixels": [ + "picture", + 0 + ], + "vae": [ + "$decode.vae", + "*" + ] + } + }, + "area": { + "class_type": "SetLatentNoiseMask", + "inputs": { + "samples": [ + "encode", + 0 + ], + "mask": [ + "edge", + 0 + ] + }, + "wire_to": "$sampler" + }, + "look": { + "class_type": "PreviewImage", + "inputs": { + "images": [ + "cut", + 0 + ] + } + } + }, + "roles": { + "sampler": { + "denoise": 1.0 + } + }, + "after": "Write the new scene in the prompt and keep the character's own words (who, hair, clothes) so its edge fits, then run_workflow. The picture's size is the drawing's: the empty latent is no longer used. The kept character (cut out) is among the run's previews: if part of it is missing or the old background clings to it, say so; a halo at its edge: raise the GrowMask expand (6) a little.", + "notes": "Denoise 1.0: the background is drawn anew, not changed. GrowMask lets the background reach 6 px into the character so the seam is redrawn.", + "models": [], + "tested": { + "gpu": "RTX 4050 Laptop", + "vram_gb": 6, + "date": "2026-10-28", + "runs": [ + { + "model": "SDXL (Illustrious)", + "size": "832×1216", + "seconds": 30 + } + ] + } +} diff --git a/api/blocks/upscale_tile.json b/api/blocks/upscale_tile.json index a8e3fbd..bd725f0 100644 --- a/api/blocks/upscale_tile.json +++ b/api/blocks/upscale_tile.json @@ -1,7 +1,7 @@ { "job": "upscale", "rank": 2, - "short": "1.25×, held by a tile ControlNet, more detail", + "short": "1.25×, tile-held, more detail", "title": { "zh": "平铺放大", "en": "Upscale held by tile" diff --git a/tests/test_reference_blocks.py b/tests/test_reference_blocks.py index a60acba..2a3fcdc 100644 --- a/tests/test_reference_blocks.py +++ b/tests/test_reference_blocks.py @@ -57,7 +57,7 @@ class BlockTests(unittest.TestCase): self.assertLess(len(" ".join(everything.values())), 900, "small: it goes with begin_task") with mock.patch.object(reference_blocks, "DIRECTORY_CHARS", 200): names = reference_blocks.directory(EVERYTHING) - self.assertEqual(names[reference_blocks.JOBS["repaint"]], "inpaint_crop_stitch, inpaint_lanpaint, inpaint_basic", + self.assertEqual(names[reference_blocks.JOBS["repaint"]], "inpaint_crop_stitch, inpaint_lanpaint, inpaint_basic, change_scene", "too many to say each: the names alone") def test_a_block_turned_off_is_not_offered_and_cannot_be_built(self): @@ -288,6 +288,7 @@ class ReferencePictureTests(unittest.TestCase): mock.patch.object(folders, "get_input_directory", return_value=input_dir): name = ai_pictures.to_input("ai_inbox/ref.png") self.assertEqual(ai_pictures.to_input("ai_inbox\\ref.png"), name, "the same picture, the same file") + self.assertEqual(ai_pictures.to_input("ai_inbox/ref.png [output]"), name, "as Load Image names an output picture") self.assertEqual(ai_pictures.to_input("mine.png"), "mine.png") with self.assertRaises(ToolError): ai_pictures.to_input("ai_inbox/gone.png")