Compare commits

..

4 Commits

Author SHA1 Message Date
Jedrzej Kosinski
fa50fe1c46 Merge branch 'master' into accumulate-save-image-option 2026-03-24 23:49:50 -07:00
Jedrzej Kosinski
8181e83ee0 Merge branch 'master' into accumulate-save-image-option 2026-03-24 18:15:38 -07:00
bymyself
c69f9791d7 chore: remove accumulate unit tests to reduce PR scope 2026-03-17 15:56:14 +00:00
bymyself
a78f700552 feat: add accumulate toggle to SaveImage and PreviewImage nodes
Add 'accumulate' BOOLEAN optional input (advanced, default False) to
SaveImage and PreviewImage INPUT_TYPES. Accept the parameter in
save_images() so the execution engine can pass it through.

Set merge flag in the 'executed' websocket message when accumulate is
enabled, so the frontend appends outputs instead of replacing them.

Add unit tests for the accumulate input definition and merge flag
derivation logic.

Amp-Thread-ID: https://ampcode.com/threads/T-019cbb66-bb43-771e-b47b-0f05a436b9cb
2026-03-17 15:56:14 +00:00
4 changed files with 10 additions and 26 deletions

View File

@@ -419,11 +419,12 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
inputs = dynprompt.get_node(unique_id)['inputs']
class_type = dynprompt.get_node(unique_id)['class_type']
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
merge = inputs.get('accumulate') is True
cached = await caches.outputs.get(unique_id)
if cached is not None:
if server.client_id is not None:
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:
ui_outputs[unique_id] = cached.ui
get_progress_state().finish_progress(unique_id)
@@ -550,7 +551,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
"output": output_ui
}
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:
cached_outputs = []
new_node_ids = []

View File

@@ -139,16 +139,7 @@ def execute_prestartup_script():
spec.loader.exec_module(module)
return True
except Exception as e:
import traceback
logging.error(f"Failed to execute startup-script: {script_path} / {e}")
from nodes import NODE_STARTUP_ERRORS, get_module_name
node_module_name = get_module_name(os.path.dirname(script_path))
NODE_STARTUP_ERRORS[node_module_name] = {
"module_path": os.path.dirname(script_path),
"error": str(e),
"traceback": traceback.format_exc(),
"phase": "prestartup",
}
return False
node_paths = folder_paths.get_folder_paths("custom_nodes")

View File

@@ -1638,6 +1638,9 @@ class SaveImage:
"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."})
},
"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"
},
@@ -1653,7 +1656,7 @@ class SaveImage:
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
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
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()
@@ -1694,6 +1697,9 @@ class PreviewImage(SaveImage):
def INPUT_TYPES(s):
return {"required":
{"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"},
}
@@ -2181,9 +2187,6 @@ EXTENSION_WEB_DIRS = {}
# Dictionary of successfully loaded module names and associated directories.
LOADED_MODULE_DIRS = {}
# Dictionary of custom node startup errors, keyed by module name.
NODE_STARTUP_ERRORS: dict[str, dict] = {}
def get_module_name(module_path: str) -> str:
"""
@@ -2301,13 +2304,6 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
except Exception as e:
logging.warning(traceback.format_exc())
logging.warning(f"Cannot import {module_path} module for custom nodes: {e}")
module_name = get_module_name(module_path)
NODE_STARTUP_ERRORS[module_name] = {
"module_path": module_path,
"error": str(e),
"traceback": traceback.format_exc(),
"phase": "import",
}
return False
async def init_external_custom_nodes():

View File

@@ -753,10 +753,6 @@ class PromptServer():
out[node_class] = node_info(node_class)
return web.json_response(out)
@routes.get("/custom_node_startup_errors")
async def get_custom_node_startup_errors(request):
return web.json_response(nodes.NODE_STARTUP_ERRORS)
@routes.get("/api/jobs")
async def get_jobs(request):
"""List all jobs with filtering, sorting, and pagination.