diff --git a/loop_nodes.py b/loop_nodes.py index 1e5786a..c16ad1e 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -190,16 +190,32 @@ def _entry_value_summary(value: Any) -> str: return text[:160] +def _metadata_copy(value: Any) -> Any: + try: + return json.loads(json.dumps(value)) + except Exception: + return value + + +def _entry_metadata(prompt: Any, extra_pnginfo: Any) -> dict[str, Any]: + return { + "prompt": _metadata_copy(prompt), + "extra_pnginfo": _metadata_copy(extra_pnginfo) if isinstance(extra_pnginfo, dict) else {}, + } + + def _entry_infos(store: list[dict[str, Any]]) -> list[dict[str, Any]]: entries = [] for index, entry in enumerate(store, start=1): image = entry.get("image") shape = _image_shape(image) + has_metadata = "prompt" in entry or "extra_pnginfo" in entry entries.append( { "index": index, "id": str(entry.get("id") or ""), "has_image": image is not None, + "has_metadata": has_metadata, "shape": list(shape) if shape is not None else [], "value": _entry_value_summary(entry.get("value")), } @@ -274,12 +290,18 @@ def _metadata(prompt: Any, extra_pnginfo: Any) -> Any: metadata = PngInfo() if prompt is not None: metadata.add_text("prompt", json.dumps(prompt)) - if extra_pnginfo is not None: + if isinstance(extra_pnginfo, dict): for key, value in extra_pnginfo.items(): metadata.add_text(key, json.dumps(value)) return metadata +def _metadata_for_entry(entry: dict[str, Any], fallback_prompt: Any, fallback_extra_pnginfo: Any) -> Any: + prompt = entry["prompt"] if "prompt" in entry else fallback_prompt + extra_pnginfo = entry["extra_pnginfo"] if "extra_pnginfo" in entry else fallback_extra_pnginfo + return _metadata(prompt, extra_pnginfo) + + def _image_to_pil(image: Any) -> Any: _require_image_saving() tensor = image @@ -312,19 +334,26 @@ def _next_save_counter(folder: str, prefix: str) -> int: return counter -def _preview_image_results(images: list[Any], preview_limit: int, prompt: Any, extra_pnginfo: Any) -> list[dict[str, str]]: - if not images: +def _preview_image_results( + entries: list[dict[str, Any]], + preview_limit: int, + prompt: Any, + extra_pnginfo: Any, +) -> list[dict[str, str]]: + image_entries = [entry for entry in entries if entry.get("image") is not None] + if not image_entries: return [] _require_image_saving() output_dir = folder_paths.get_temp_directory() - preview_images = images[: max(1, int(preview_limit))] - first_shape = _image_shape(preview_images[0]) + preview_entries = image_entries[: max(1, int(preview_limit))] + first_shape = _image_shape(preview_entries[0].get("image")) height, width = (first_shape[0], first_shape[1]) if first_shape else (512, 512) prefix = "SxCPAccumulatorPreview_temp_" + "".join(random.choice("abcdefghijklmnopqrstupvxyz") for _ in range(5)) full_output_folder, filename, counter, subfolder, _filename_prefix = folder_paths.get_save_image_path(prefix, output_dir, width, height) - metadata = _metadata(prompt, extra_pnginfo) results = [] - for image in preview_images: + for entry in preview_entries: + image = entry.get("image") + metadata = _metadata_for_entry(entry, prompt, extra_pnginfo) file = f"{filename}_{counter:05}_.png" _image_to_pil(image).save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=1) results.append({"filename": file, "subfolder": subfolder, "type": "temp"}) @@ -343,21 +372,23 @@ def _resolve_save_folder(save_path: str) -> str: def _save_images_to_folder( - images: list[Any], + entries: list[dict[str, Any]], save_path: str, filename_prefix: str, prompt: Any, extra_pnginfo: Any, ) -> list[str]: - if not images: + image_entries = [entry for entry in entries if entry.get("image") is not None] + if not image_entries: return [] folder = _resolve_save_folder(save_path) os.makedirs(folder, exist_ok=True) prefix = _safe_filename_prefix(filename_prefix) counter = _next_save_counter(folder, prefix) - metadata = _metadata(prompt, extra_pnginfo) saved_paths = [] - for image in images: + for entry in image_entries: + image = entry.get("image") + metadata = _metadata_for_entry(entry, prompt, extra_pnginfo) file = f"{prefix}_{counter:05}_.png" path = os.path.join(folder, file) _image_to_pil(image).save(path, pnginfo=metadata, compress_level=4) @@ -642,6 +673,8 @@ class SxCPAccumulator: "entry_tag": ("STRING", {"default": "", "multiline": False}), }, "hidden": { + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID", }, } @@ -681,18 +714,27 @@ class SxCPAccumulator: return value[image_index] return value - def _entry_records(self, image: Any, value: Any, entry_id: Any, entry_tag: str, skip_empty: bool) -> list[dict[str, Any]]: + def _entry_records( + self, + image: Any, + value: Any, + entry_id: Any, + entry_tag: str, + skip_empty: bool, + metadata: dict[str, Any], + ) -> 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, entry_tag, 0, 1), "image": None, "value": value}] + return [{"id": self._entry_id(entry_id, entry_tag, 0, 1), "image": None, "value": value, **metadata}] image_count = len(images) return [ { "id": self._entry_id(entry_id, entry_tag, index, image_count), "image": image_item, "value": self._value_for_image(value, index, image_count), + **metadata, } for index, image_item in enumerate(images) ] @@ -748,6 +790,8 @@ class SxCPAccumulator: value=None, entry_id=None, entry_tag="", + prompt=None, + extra_pnginfo=None, unique_id=None, ): key = self._store_key(store_key, unique_id) @@ -758,7 +802,8 @@ class SxCPAccumulator: store.clear() if action not in ("clear", "read"): - entries = self._entry_records(image, value, entry_id, entry_tag, bool(skip_empty)) + metadata = _entry_metadata(prompt, extra_pnginfo) + entries = self._entry_records(image, value, entry_id, entry_tag, bool(skip_empty), metadata) self._append_or_replace(store, entries, action) max_items = max(1, int(max_items)) @@ -855,14 +900,14 @@ class SxCPAccumulatorPreview: saved_paths: list[str] = [] save_status = "" if bool(save_batch) and bool(finished): - saved_paths = _save_images_to_folder(images, save_path, filename_prefix, prompt, extra_pnginfo) + saved_paths = _save_images_to_folder(store, save_path, filename_prefix, prompt, extra_pnginfo) save_status = f"; saved={len(saved_paths)}" if saved_paths and bool(clear_after_save): store.clear() images = [] save_status += "; cleared_after_save" - preview_images = _preview_image_results(images, preview_limit, prompt, extra_pnginfo) + preview_images = _preview_image_results(store, preview_limit, prompt, extra_pnginfo) entries = _entry_infos(store) status = _accumulator_status(key, store) if removed: diff --git a/web/accumulator_preview.js b/web/accumulator_preview.js index 83f2780..4496621 100644 --- a/web/accumulator_preview.js +++ b/web/accumulator_preview.js @@ -64,7 +64,8 @@ function entryLabel(entry) { const id = entry?.id ? ` ${entry.id}` : ""; const image = entry?.has_image ? " image" : " value"; const shape = Array.isArray(entry?.shape) && entry.shape.length >= 2 ? ` ${entry.shape[1]}x${entry.shape[0]}` : ""; - return `#${index}${id}${image}${shape}`.trim(); + const metadata = entry?.has_metadata ? " metadata" : ""; + return `#${index}${id}${image}${shape}${metadata}`.trim(); } function setStatus(node, status) {