新增内增外部节点和修改图像保存节点(支持保存和预览双模式)
This commit is contained in:
@@ -40,6 +40,7 @@ else:
|
||||
|
||||
_RESIZE_MODES = ["keep_aspect", "crop_center", "stretch", "none"]
|
||||
_SIZE_SOURCES = ["input_image", "custom", "model_recommended"]
|
||||
_SAVE_OUTPUT_MODES = ["save", "preview_only"]
|
||||
|
||||
|
||||
def _round_to_multiple(value: int, multiple: int) -> int:
|
||||
@@ -351,6 +352,7 @@ class LLSSaveImage:
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "LLS"}),
|
||||
"output_mode": (_SAVE_OUTPUT_MODES, {"default": "save"}),
|
||||
"save_metadata": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -429,6 +431,7 @@ class LLSSaveImage:
|
||||
self,
|
||||
image,
|
||||
filename_prefix: str = "LLS",
|
||||
output_mode: str = "save",
|
||||
save_metadata: bool = True,
|
||||
model=None,
|
||||
clip=None,
|
||||
@@ -441,6 +444,16 @@ class LLSSaveImage:
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
if output_mode == "preview_only":
|
||||
if comfy_core_nodes is None or not hasattr(comfy_core_nodes, "PreviewImage"):
|
||||
raise RuntimeError("[LLS] ComfyUI core PreviewImage node is not available.")
|
||||
previewer = comfy_core_nodes.PreviewImage()
|
||||
return previewer.save_images(
|
||||
image,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=None,
|
||||
)
|
||||
|
||||
if comfy_core_nodes is None or not hasattr(comfy_core_nodes, "SaveImage"):
|
||||
raise RuntimeError("[LLS] ComfyUI core SaveImage node is not available.")
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ from .repair_finish import (
|
||||
NODE_CLASS_MAPPINGS as FINISH_NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as FINISH_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
from .native_inpaint import (
|
||||
NODE_CLASS_MAPPINGS as NATIVE_INPAINT_NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as NATIVE_INPAINT_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
from .repair_prepare import (
|
||||
NODE_CLASS_MAPPINGS as PREPARE_NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as PREPARE_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
@@ -25,11 +29,13 @@ def _merge_registry_maps(*maps):
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = _merge_registry_maps(
|
||||
NATIVE_INPAINT_NODE_CLASS_MAPPINGS,
|
||||
PREPARE_NODE_CLASS_MAPPINGS,
|
||||
FINISH_NODE_CLASS_MAPPINGS,
|
||||
)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = _merge_registry_maps(
|
||||
NATIVE_INPAINT_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
PREPARE_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
FINISH_NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..utils.model_info import info_to_json, resolve_edit_capabilities
|
||||
from . import runtime as repair_runtime
|
||||
|
||||
|
||||
_PATCH_MODE_CHOICES = ["auto", "disabled", "differential_diffusion"]
|
||||
|
||||
|
||||
class LLSNativeInpaintConditioning:
|
||||
CATEGORY = "LLS/Image Repair"
|
||||
FUNCTION = "encode"
|
||||
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "STRING")
|
||||
RETURN_NAMES = ("model", "positive", "negative", "latent", "inpaint_info")
|
||||
DESCRIPTION = (
|
||||
"Thin wrapper around ComfyUI InpaintModelConditioning. "
|
||||
"Keeps the standard KSampler path and can auto-apply DifferentialDiffusion for FLUX."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"vae": ("VAE",),
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"patch_mode": (_PATCH_MODE_CHOICES, {"default": "auto"}),
|
||||
"patch_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"noise_mask": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"model_info": ("STRING",),
|
||||
},
|
||||
}
|
||||
|
||||
def encode(
|
||||
self,
|
||||
model,
|
||||
positive,
|
||||
negative,
|
||||
vae,
|
||||
image,
|
||||
mask,
|
||||
patch_mode,
|
||||
patch_strength,
|
||||
noise_mask,
|
||||
model_info=None,
|
||||
):
|
||||
capabilities = resolve_edit_capabilities(model=model, model_info=model_info)
|
||||
|
||||
apply_patch = False
|
||||
if patch_mode == "differential_diffusion":
|
||||
apply_patch = True
|
||||
elif patch_mode == "auto" and str(capabilities["model_family"]).startswith("FLUX"):
|
||||
apply_patch = True
|
||||
|
||||
patched_model = model
|
||||
applied_patch = ""
|
||||
if apply_patch:
|
||||
patched_model = repair_runtime.apply_differential_diffusion(
|
||||
model,
|
||||
strength=float(patch_strength),
|
||||
)
|
||||
applied_patch = "differential_diffusion"
|
||||
|
||||
positive_out, negative_out, latent = repair_runtime.encode_inpaint_conditioning(
|
||||
positive,
|
||||
negative,
|
||||
vae,
|
||||
image,
|
||||
mask,
|
||||
noise_mask=bool(noise_mask),
|
||||
)
|
||||
|
||||
inpaint_info = info_to_json(
|
||||
{
|
||||
"model_family": capabilities["model_family"],
|
||||
"model_role": capabilities["model_role"],
|
||||
"profile_id": capabilities["profile_id"],
|
||||
"backend_type": capabilities["backend_type"],
|
||||
"sampler_strategy": capabilities["sampler_strategy"],
|
||||
"loader_strategy": capabilities["loader_strategy"],
|
||||
"patch_mode": patch_mode,
|
||||
"applied_patch": applied_patch,
|
||||
"patch_strength": float(patch_strength),
|
||||
"noise_mask": bool(noise_mask),
|
||||
}
|
||||
)
|
||||
return (
|
||||
patched_model,
|
||||
positive_out,
|
||||
negative_out,
|
||||
latent,
|
||||
inpaint_info,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LLSNativeInpaintConditioning": LLSNativeInpaintConditioning,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LLSNativeInpaintConditioning": "LLS Native Inpaint Conditioning",
|
||||
}
|
||||
@@ -90,6 +90,9 @@ class ComfySDStub:
|
||||
|
||||
|
||||
class CoreNodesStub:
|
||||
save_calls = []
|
||||
preview_calls = []
|
||||
|
||||
class VAELoader:
|
||||
@staticmethod
|
||||
def vae_list(_self):
|
||||
@@ -108,6 +111,14 @@ class CoreNodesStub:
|
||||
|
||||
class SaveImage:
|
||||
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
||||
CoreNodesStub.save_calls.append(
|
||||
{
|
||||
"images": images,
|
||||
"filename_prefix": filename_prefix,
|
||||
"prompt": prompt,
|
||||
"extra_pnginfo": extra_pnginfo,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ui": {
|
||||
"images": [
|
||||
@@ -122,6 +133,29 @@ class CoreNodesStub:
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewImage:
|
||||
def save_images(self, images, prompt=None, extra_pnginfo=None):
|
||||
CoreNodesStub.preview_calls.append(
|
||||
{
|
||||
"images": images,
|
||||
"prompt": prompt,
|
||||
"extra_pnginfo": extra_pnginfo,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ui": {
|
||||
"images": [
|
||||
{
|
||||
"filename": "preview.png",
|
||||
"subfolder": "",
|
||||
"type": "temp",
|
||||
"extra_pnginfo": extra_pnginfo,
|
||||
"prompt": prompt,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RecordingClip:
|
||||
def __init__(self, token_keys, calls=None):
|
||||
@@ -829,6 +863,27 @@ class TestLoaderPromptRefactor(unittest.TestCase):
|
||||
self.assertEqual(metadata["steps"], 20)
|
||||
self.assertEqual(metadata["upscale_mode"], "none")
|
||||
|
||||
def test_save_image_preview_only_uses_native_preview_node_without_saving(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_refactor.image import nodes as image_nodes
|
||||
|
||||
CoreNodesStub.save_calls = []
|
||||
CoreNodesStub.preview_calls = []
|
||||
|
||||
with mock.patch.object(image_nodes, "comfy_core_nodes", CoreNodesStub()):
|
||||
node = image_nodes.LLSSaveImage()
|
||||
result = node.save(
|
||||
image=FakeTensor((1, 512, 512, 3)),
|
||||
filename_prefix="LLS",
|
||||
save_metadata=True,
|
||||
output_mode="preview_only",
|
||||
)
|
||||
|
||||
self.assertEqual(CoreNodesStub.save_calls, [])
|
||||
self.assertEqual(len(CoreNodesStub.preview_calls), 1)
|
||||
self.assertEqual(result["ui"]["images"][0]["type"], "temp")
|
||||
self.assertIsNone(result["ui"]["images"][0]["extra_pnginfo"])
|
||||
|
||||
def test_generation_config_uses_model_inference_without_context(self):
|
||||
plugin = load_plugin_package()
|
||||
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSGenerationConfig"]
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import json
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from .test_repair_helpers import FakeMask, FakeModel, FakeTensor, import_plugin_submodule, load_plugin_package
|
||||
except ImportError: # pragma: no cover - discovery mode imports from top level
|
||||
from test_repair_helpers import FakeMask, FakeModel, FakeTensor, import_plugin_submodule, load_plugin_package
|
||||
|
||||
|
||||
class TestRepairNativeInpaint(unittest.TestCase):
|
||||
def test_node_registers_with_expected_schema(self):
|
||||
plugin = load_plugin_package()
|
||||
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSNativeInpaintConditioning"]
|
||||
schema = node_cls.INPUT_TYPES()
|
||||
|
||||
self.assertEqual(plugin.NODE_DISPLAY_NAME_MAPPINGS["LLSNativeInpaintConditioning"], "LLS Native Inpaint Conditioning")
|
||||
self.assertEqual(node_cls.CATEGORY, "LLS/Image Repair")
|
||||
self.assertEqual(node_cls.FUNCTION, "encode")
|
||||
self.assertEqual(
|
||||
node_cls.RETURN_TYPES,
|
||||
("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "STRING"),
|
||||
)
|
||||
self.assertEqual(
|
||||
node_cls.RETURN_NAMES,
|
||||
("model", "positive", "negative", "latent", "inpaint_info"),
|
||||
)
|
||||
self.assertEqual(schema["required"]["patch_mode"][0], ["auto", "disabled", "differential_diffusion"])
|
||||
self.assertEqual(schema["optional"]["model_info"], ("STRING",))
|
||||
|
||||
def test_node_wraps_inpaint_model_conditioning_without_flux_patch(self):
|
||||
plugin = load_plugin_package()
|
||||
node = plugin.NODE_CLASS_MAPPINGS["LLSNativeInpaintConditioning"]()
|
||||
repair_runtime = import_plugin_submodule(plugin, "repair.runtime")
|
||||
model = FakeModel(
|
||||
family="SDXL",
|
||||
model_role="inpaint",
|
||||
supports_inpaint_native=True,
|
||||
supports_image_edit_native=False,
|
||||
preferred_edit_backend="sdxl",
|
||||
profile_id="sdxl_inpaint",
|
||||
backend_type="sdxl_native",
|
||||
sampler_strategy="standard_k",
|
||||
loader_strategy="sdxl_checkpoint",
|
||||
)
|
||||
positive = [["pos", {}]]
|
||||
negative = [["neg", {}]]
|
||||
latent = {"samples": "latent"}
|
||||
|
||||
with mock.patch.object(
|
||||
repair_runtime,
|
||||
"encode_inpaint_conditioning",
|
||||
return_value=(positive, negative, latent),
|
||||
) as encode_inpaint, mock.patch.object(
|
||||
repair_runtime,
|
||||
"apply_differential_diffusion",
|
||||
) as patch_model:
|
||||
model_out, positive_out, negative_out, latent_out, info = node.encode(
|
||||
model=model,
|
||||
positive=positive,
|
||||
negative=negative,
|
||||
vae=object(),
|
||||
image=FakeTensor((1, 1024, 1024, 3)),
|
||||
mask=FakeMask((1, 1024, 1024), mask_bbox=(64, 64, 256, 256), mask_area_ratio=0.04),
|
||||
patch_mode="auto",
|
||||
patch_strength=1.0,
|
||||
noise_mask=True,
|
||||
)
|
||||
|
||||
encode_inpaint.assert_called_once()
|
||||
patch_model.assert_not_called()
|
||||
self.assertIs(model_out, model)
|
||||
self.assertIs(positive_out, positive)
|
||||
self.assertIs(negative_out, negative)
|
||||
self.assertIs(latent_out, latent)
|
||||
payload = json.loads(info)
|
||||
self.assertEqual(payload["model_family"], "SDXL")
|
||||
self.assertEqual(payload["applied_patch"], "")
|
||||
|
||||
def test_node_auto_applies_flux_differential_diffusion(self):
|
||||
plugin = load_plugin_package()
|
||||
node = plugin.NODE_CLASS_MAPPINGS["LLSNativeInpaintConditioning"]()
|
||||
repair_runtime = import_plugin_submodule(plugin, "repair.runtime")
|
||||
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",
|
||||
)
|
||||
patched_model = object()
|
||||
|
||||
with mock.patch.object(
|
||||
repair_runtime,
|
||||
"apply_differential_diffusion",
|
||||
return_value=patched_model,
|
||||
) as patch_model, mock.patch.object(
|
||||
repair_runtime,
|
||||
"encode_inpaint_conditioning",
|
||||
return_value=([["pos", {}]], [["neg", {}]], {"samples": "latent"}),
|
||||
):
|
||||
model_out, _positive_out, _negative_out, _latent_out, info = node.encode(
|
||||
model=model,
|
||||
positive=[["pos", {}]],
|
||||
negative=[["neg", {}]],
|
||||
vae=object(),
|
||||
image=FakeTensor((1, 1024, 1024, 3)),
|
||||
mask=FakeMask((1, 1024, 1024), mask_bbox=(64, 64, 256, 256), mask_area_ratio=0.04),
|
||||
patch_mode="auto",
|
||||
patch_strength=0.75,
|
||||
noise_mask=True,
|
||||
)
|
||||
|
||||
patch_model.assert_called_once_with(model, strength=0.75)
|
||||
self.assertIs(model_out, patched_model)
|
||||
payload = json.loads(info)
|
||||
self.assertEqual(payload["model_family"], "FLUX_DEV")
|
||||
self.assertEqual(payload["applied_patch"], "differential_diffusion")
|
||||
self.assertEqual(payload["patch_strength"], 0.75)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user