mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-13 09:10:12 +00:00
Compare commits
1 Commits
v3/model_m
...
accumulate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8bce44549 |
@@ -418,11 +418,12 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
|
|||||||
inputs = dynprompt.get_node(unique_id)['inputs']
|
inputs = dynprompt.get_node(unique_id)['inputs']
|
||||||
class_type = dynprompt.get_node(unique_id)['class_type']
|
class_type = dynprompt.get_node(unique_id)['class_type']
|
||||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||||
|
merge = inputs.get('accumulate') is True
|
||||||
cached = caches.outputs.get(unique_id)
|
cached = caches.outputs.get(unique_id)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
if server.client_id is not None:
|
if server.client_id is not None:
|
||||||
cached_ui = cached.ui or {}
|
cached_ui = cached.ui or {}
|
||||||
server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": cached_ui.get("output",None), "prompt_id": prompt_id }, server.client_id)
|
server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": cached_ui.get("output",None), "prompt_id": prompt_id, "merge": merge }, server.client_id)
|
||||||
if cached.ui is not None:
|
if cached.ui is not None:
|
||||||
ui_outputs[unique_id] = cached.ui
|
ui_outputs[unique_id] = cached.ui
|
||||||
get_progress_state().finish_progress(unique_id)
|
get_progress_state().finish_progress(unique_id)
|
||||||
@@ -549,7 +550,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
|
|||||||
"output": output_ui
|
"output": output_ui
|
||||||
}
|
}
|
||||||
if server.client_id is not None:
|
if server.client_id is not None:
|
||||||
server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": output_ui, "prompt_id": prompt_id }, server.client_id)
|
server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": output_ui, "prompt_id": prompt_id, "merge": merge }, server.client_id)
|
||||||
if has_subgraph:
|
if has_subgraph:
|
||||||
cached_outputs = []
|
cached_outputs = []
|
||||||
new_node_ids = []
|
new_node_ids = []
|
||||||
|
|||||||
8
nodes.py
8
nodes.py
@@ -1640,6 +1640,9 @@ class SaveImage:
|
|||||||
"images": ("IMAGE", {"tooltip": "The images to save."}),
|
"images": ("IMAGE", {"tooltip": "The images to save."}),
|
||||||
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
|
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
|
||||||
},
|
},
|
||||||
|
"optional": {
|
||||||
|
"accumulate": ("BOOLEAN", {"default": False, "tooltip": "When enabled, outputs accumulate into a growing gallery across queue runs instead of being replaced.", "advanced": True}),
|
||||||
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
||||||
},
|
},
|
||||||
@@ -1655,7 +1658,7 @@ class SaveImage:
|
|||||||
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
|
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
|
||||||
SEARCH_ALIASES = ["save", "save image", "export image", "output image", "write image", "download"]
|
SEARCH_ALIASES = ["save", "save image", "export image", "output image", "write image", "download"]
|
||||||
|
|
||||||
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, accumulate=False):
|
||||||
filename_prefix += self.prefix_append
|
filename_prefix += self.prefix_append
|
||||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
||||||
results = list()
|
results = list()
|
||||||
@@ -1696,6 +1699,9 @@ class PreviewImage(SaveImage):
|
|||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required":
|
return {"required":
|
||||||
{"images": ("IMAGE", ), },
|
{"images": ("IMAGE", ), },
|
||||||
|
"optional": {
|
||||||
|
"accumulate": ("BOOLEAN", {"default": False, "tooltip": "When enabled, outputs accumulate into a growing gallery across queue runs instead of being replaced.", "advanced": True}),
|
||||||
|
},
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
tests-unit/execution_test/test_accumulate_merge.py
Normal file
89
tests-unit/execution_test/test_accumulate_merge.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the accumulate toggle on SaveImage and PreviewImage nodes.
|
||||||
|
|
||||||
|
Tests that the accumulate input is correctly defined and that the merge flag
|
||||||
|
derivation logic in execution.py works for all input shapes.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import nodes
|
||||||
|
|
||||||
|
|
||||||
|
class TestSaveImageAccumulateInput:
|
||||||
|
"""Test SaveImage node definition includes accumulate input."""
|
||||||
|
|
||||||
|
def test_accumulate_in_optional_inputs(self):
|
||||||
|
input_types = nodes.SaveImage.INPUT_TYPES()
|
||||||
|
assert "optional" in input_types
|
||||||
|
assert "accumulate" in input_types["optional"]
|
||||||
|
|
||||||
|
def test_accumulate_is_boolean_type(self):
|
||||||
|
input_types = nodes.SaveImage.INPUT_TYPES()
|
||||||
|
accumulate_def = input_types["optional"]["accumulate"]
|
||||||
|
assert accumulate_def[0] == "BOOLEAN"
|
||||||
|
|
||||||
|
def test_accumulate_defaults_to_false(self):
|
||||||
|
input_types = nodes.SaveImage.INPUT_TYPES()
|
||||||
|
accumulate_def = input_types["optional"]["accumulate"]
|
||||||
|
assert accumulate_def[1]["default"] is False
|
||||||
|
|
||||||
|
def test_accumulate_is_advanced(self):
|
||||||
|
input_types = nodes.SaveImage.INPUT_TYPES()
|
||||||
|
accumulate_def = input_types["optional"]["accumulate"]
|
||||||
|
assert accumulate_def[1].get("advanced") is True
|
||||||
|
|
||||||
|
def test_save_images_accepts_accumulate_parameter(self):
|
||||||
|
sig = inspect.signature(nodes.SaveImage.save_images)
|
||||||
|
assert "accumulate" in sig.parameters
|
||||||
|
assert sig.parameters["accumulate"].default is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestPreviewImageAccumulateInput:
|
||||||
|
"""Test PreviewImage node definition includes accumulate input."""
|
||||||
|
|
||||||
|
def test_accumulate_in_optional_inputs(self):
|
||||||
|
input_types = nodes.PreviewImage.INPUT_TYPES()
|
||||||
|
assert "optional" in input_types
|
||||||
|
assert "accumulate" in input_types["optional"]
|
||||||
|
|
||||||
|
def test_accumulate_is_boolean_type(self):
|
||||||
|
input_types = nodes.PreviewImage.INPUT_TYPES()
|
||||||
|
accumulate_def = input_types["optional"]["accumulate"]
|
||||||
|
assert accumulate_def[0] == "BOOLEAN"
|
||||||
|
|
||||||
|
def test_accumulate_defaults_to_false(self):
|
||||||
|
input_types = nodes.PreviewImage.INPUT_TYPES()
|
||||||
|
accumulate_def = input_types["optional"]["accumulate"]
|
||||||
|
assert accumulate_def[1]["default"] is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccumulateMergeFlagDerivation:
|
||||||
|
"""Test the merge flag logic used in execution.py.
|
||||||
|
|
||||||
|
In execution.py, the merge flag is derived as:
|
||||||
|
merge = inputs.get('accumulate') is True
|
||||||
|
|
||||||
|
This must return True only for literal True, not for truthy values
|
||||||
|
like lists (which represent node links in the prompt).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"inputs,expected",
|
||||||
|
[
|
||||||
|
({"accumulate": True}, True),
|
||||||
|
({"accumulate": False}, False),
|
||||||
|
({}, False),
|
||||||
|
({"accumulate": None}, False),
|
||||||
|
# Node link: accumulate connected to another node's output
|
||||||
|
({"accumulate": ["other_node_id", 0]}, False),
|
||||||
|
# String "true" should not match
|
||||||
|
({"accumulate": "true"}, False),
|
||||||
|
# Integer 1 should not match
|
||||||
|
({"accumulate": 1}, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_merge_flag(self, inputs, expected):
|
||||||
|
merge = inputs.get("accumulate") is True
|
||||||
|
assert merge is expected
|
||||||
Reference in New Issue
Block a user