From ed6c7b8dc0a780a8a9393122834721ceb80b2181 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 23:11:37 +0200 Subject: [PATCH] Simplify accumulator variant collection --- README.md | 17 ++++++++++++++--- loop_nodes.py | 46 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7cdb7c3..631cf2f 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,20 @@ you want to update each iteration. They are separate from the collector and grow dynamically in the UI as you connect them. `SxCP Accumulator` stores outputs across executions under a `store_key` or the -node id. Put it after an image-producing step inside or after a loop, connect the -generated `image`, and connect `For Loop Start.index` to `entry_id` when you want -reruns to replace the same row instead of appending duplicates. Its outputs are: +node id. Put it after an image-producing step inside or after a loop and connect +the generated `image`. + +For camera/pose tuning, leave `action=append_variant`. Every rerun is kept as a +new variant, so you can regenerate row 1 several times without managing ids. If +you connect `For Loop Start.index` to `entry_id`, variants are labelled by row +internally; they still append instead of replacing. + +For deterministic loop resume/dedupe, set `action=replace_by_entry_id` and +connect `For Loop Start.index` to `entry_id`. Optional `entry_tag` lets multiple +branches share the same row index without overwriting each other, such as +`soft`, `hard`, or `upscale`. + +Its outputs are: - `collection`: all stored values, or images when no explicit `value` is wired. - `image_batch`: all stored images as one ComfyUI image batch when they share diff --git a/loop_nodes.py b/loop_nodes.py index 37e9b87..bbe4150 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -22,7 +22,7 @@ except Exception: MAX_LOOP_VALUES = 20 MAX_CARRY_VALUES = MAX_LOOP_VALUES - 2 COLLECTION_MODES = ["auto_batch", "list", "image_batch", "latent_batch", "string_lines"] -ACCUMULATOR_ACTIONS = ["replace_by_entry_id", "append", "clear_then_append", "clear", "read"] +ACCUMULATOR_ACTIONS = ["append_variant", "replace_by_entry_id", "append", "clear_then_append", "clear", "read"] ACCUMULATOR_IMAGE_BATCH_MODES = ["same_size_only", "resize_to_first"] ACCUMULATOR_IMAGE_GROUPS = 4 @@ -418,7 +418,7 @@ class SxCPAccumulator: return { "required": { "store_key": ("STRING", {"default": "", "multiline": False}), - "action": (ACCUMULATOR_ACTIONS, {"default": "replace_by_entry_id"}), + "action": (ACCUMULATOR_ACTIONS, {"default": "append_variant"}), "max_items": ("INT", {"default": 32, "min": 1, "max": 10000, "step": 1}), "image_batch_mode": (ACCUMULATOR_IMAGE_BATCH_MODES, {"default": "same_size_only"}), "skip_empty": ("BOOLEAN", {"default": True}), @@ -427,6 +427,7 @@ class SxCPAccumulator: "image": ("IMAGE",), "value": (ANY_TYPE,), "entry_id": (ANY_TYPE,), + "entry_tag": ("STRING", {"default": "", "multiline": False}), }, "hidden": { "unique_id": "UNIQUE_ID", @@ -451,15 +452,16 @@ class SxCPAccumulator: key = str(store_key or "").strip() return key or f"node:{unique_id}" - def _entry_id(self, entry_id: Any, image_index: int, image_count: int) -> str: - if entry_id is None: - return "" - text = str(entry_id).strip() - if not text: - return "" + def _entry_id(self, entry_id: Any, entry_tag: str, image_index: int, image_count: int) -> str: + text = "" if entry_id is None else str(entry_id).strip() + tag = str(entry_tag or "").strip() + if text and tag: + text = f"{text}:{tag}" + elif tag: + text = tag if image_count <= 1: return text - return f"{text}:{image_index + 1}" + return f"{text or 'image'}:{image_index + 1}" def _value_for_image(self, value: Any, image_index: int, image_count: int) -> Any: if image_count <= 1: @@ -468,25 +470,40 @@ class SxCPAccumulator: return value[image_index] return value - def _entry_records(self, image: Any, value: Any, entry_id: Any, skip_empty: bool) -> list[dict[str, Any]]: + def _entry_records(self, image: Any, value: Any, entry_id: Any, entry_tag: str, skip_empty: bool) -> list[dict[str, Any]]: images = _split_image_value(image) if not images: if value is None and skip_empty: return [] - return [{"id": self._entry_id(entry_id, 0, 1), "image": None, "value": value}] + return [{"id": self._entry_id(entry_id, entry_tag, 0, 1), "image": None, "value": value}] image_count = len(images) return [ { - "id": self._entry_id(entry_id, index, image_count), + "id": self._entry_id(entry_id, entry_tag, index, image_count), "image": image_item, "value": self._value_for_image(value, index, image_count), } for index, image_item in enumerate(images) ] + def _variant_entry_id(self, store: list[dict[str, Any]], base_id: str) -> str: + base_id = base_id or "entry" + prefix = f"{base_id}#" + count = 0 + for existing in store: + existing_id = str(existing.get("id") or "") + if existing_id == base_id or existing_id.startswith(prefix): + count += 1 + return f"{base_id}#{count + 1}" + def _append_or_replace(self, store: list[dict[str, Any]], entries: list[dict[str, Any]], action: str) -> None: replace = action == "replace_by_entry_id" for entry in entries: + if action == "append_variant": + entry = dict(entry) + entry["id"] = self._variant_entry_id(store, str(entry.get("id") or "")) + store.append(entry) + continue entry_id = entry.get("id") or "" if replace and entry_id: for index, existing in enumerate(store): @@ -529,17 +546,18 @@ class SxCPAccumulator: image=None, value=None, entry_id=None, + entry_tag="", unique_id=None, ): key = self._store_key(store_key, unique_id) - action = action if action in ACCUMULATOR_ACTIONS else "replace_by_entry_id" + action = action if action in ACCUMULATOR_ACTIONS else "append_variant" store = _ACCUMULATOR_STORES.setdefault(key, []) if action in ("clear", "clear_then_append"): store.clear() if action not in ("clear", "read"): - entries = self._entry_records(image, value, entry_id, bool(skip_empty)) + entries = self._entry_records(image, value, entry_id, entry_tag, bool(skip_empty)) self._append_or_replace(store, entries, action) max_items = max(1, int(max_items))