From b206843464a395b51261fdc7bb11cabf82dc4019 Mon Sep 17 00:00:00 2001 From: LuqP2 Date: Mon, 1 Jun 2026 16:46:32 -0300 Subject: [PATCH] feat: add simple save image node --- README.md | 14 +++-- __init__.py | 8 +-- metadata_utils.py | 21 +++++--- metahub_save_node.py | 100 +++++++++++++++++++++++++++++++++-- pyproject.toml | 2 +- tests/test_metadata_utils.py | 29 ++++++++++ 6 files changed, 154 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b0234f0..a5beaf3 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ Restart ComfyUI after installation. 3. Generate! The node auto-detects and saves metadata. For most workflows, `images` is the only connection you need. The visible -`seed`, `steps`, `cfg`, `model_name`, `positive`, `negative`, `vae_name`, and -similar sockets are optional overrides. Leave them disconnected unless you want -to replace the value that the node detected from the workflow. +simple node fields are for filenames and Image MetaHub organization only. Use +`tags`, `notes`, and `project_name` when you want the generated files to arrive +organized in Image MetaHub. Auto-detected fields: - seed, steps, cfg @@ -98,6 +98,10 @@ connect only the missing override sockets you care about, such as `positive`, ### Override (Optional) +The full node is still available as **MetaHub Save Image Advanced**. Use it only +when you need manual metadata overrides, benchmarking inputs, or compatibility +with an existing workflow that already uses the older full node. + **Generation Parameters:** Connect any override input to replace auto-detected values: - `seed`, `steps`, `cfg` @@ -135,8 +139,8 @@ For custom benchmarking, connect these hidden inputs: - `generation_time_override` - Manual generation time (or from Timer node) All generation-parameter sockets are optional. They use `forceInput: True` so -ComfyUI shows them as connection inputs, but they are not required for normal -saving. A good default setup is: +ComfyUI shows them as connection inputs on the Advanced node, but they are not +required for normal saving. A good default setup is: - `VAEDecode.IMAGE` -> `MetaHub Save Image.images` - optional `MetaHub Timer.elapsed_time` -> `generation_time_override` diff --git a/__init__.py b/__init__.py index 9771e64..82e099b 100644 --- a/__init__.py +++ b/__init__.py @@ -4,22 +4,24 @@ Entry point for node registration """ try: - from .metahub_save_node import MetaHubSaveNode + from .metahub_save_node import MetaHubSaveImage, MetaHubSaveNode from .metahub_save_video_node import MetaHubSaveVideoNode from .timer_node import MetaHubTimerNode except ImportError: - from metahub_save_node import MetaHubSaveNode + from metahub_save_node import MetaHubSaveImage, MetaHubSaveNode from metahub_save_video_node import MetaHubSaveVideoNode from timer_node import MetaHubTimerNode NODE_CLASS_MAPPINGS = { + "MetaHubSaveImage": MetaHubSaveImage, "MetaHubSaveNode": MetaHubSaveNode, "MetaHubSaveVideoNode": MetaHubSaveVideoNode, "MetaHubTimerNode": MetaHubTimerNode } NODE_DISPLAY_NAME_MAPPINGS = { - "MetaHubSaveNode": "MetaHub Save Image", + "MetaHubSaveImage": "MetaHub Save Image", + "MetaHubSaveNode": "MetaHub Save Image Advanced", "MetaHubSaveVideoNode": "MetaHub Save Video", "MetaHubTimerNode": "MetaHub Timer" } diff --git a/metadata_utils.py b/metadata_utils.py index 416b096..e3c4a1d 100644 --- a/metadata_utils.py +++ b/metadata_utils.py @@ -651,9 +651,14 @@ def ensure_prompt_in_workflow(workflow_json: dict, prompt_data: Optional[dict]) return workflow_json -def ensure_metahub_save_node(workflow_json: dict, save_node_id: Optional[str]) -> None: +def ensure_metahub_save_node( + workflow_json: dict, + save_node_id: Optional[str], + class_type: str = "MetaHubSaveNode", + display_name: str = "MetaHub Save Image Advanced", +) -> None: """ - Ensures saved workflow keeps MetaHubSaveNode instead of SaveImage. + Ensures saved workflow keeps the active MetaHub save node instead of SaveImage. """ if not isinstance(workflow_json, dict): return @@ -672,7 +677,7 @@ def ensure_metahub_save_node(workflow_json: dict, save_node_id: Optional[str]) - if target_id and target_id in prompt: node = prompt.get(target_id) if isinstance(node, dict): - node["class_type"] = "MetaHubSaveNode" + node["class_type"] = class_type else: save_nodes = [ node_id @@ -680,7 +685,7 @@ def ensure_metahub_save_node(workflow_json: dict, save_node_id: Optional[str]) - if isinstance(node, dict) and node.get("class_type") == "SaveImage" ] if len(save_nodes) == 1: - prompt[save_nodes[0]]["class_type"] = "MetaHubSaveNode" + prompt[save_nodes[0]]["class_type"] = class_type workflow = workflow_json.get("workflow") if isinstance(workflow, str): @@ -698,14 +703,14 @@ def ensure_metahub_save_node(workflow_json: dict, save_node_id: Optional[str]) - def update_workflow_node(node: dict) -> None: if "type" in node: - node["type"] = "MetaHubSaveNode" + node["type"] = class_type if "class_type" in node: - node["class_type"] = "MetaHubSaveNode" + node["class_type"] = class_type if node.get("title") in ("Save Image", "SaveImage"): - node["title"] = "MetaHub Save Image" + node["title"] = display_name props = node.get("properties") if isinstance(props, dict) and props.get("node_name") in ("SaveImage", "Save Image"): - props["node_name"] = "MetaHub Save Image" + props["node_name"] = display_name if target_id: for node in nodes: diff --git a/metahub_save_node.py b/metahub_save_node.py index 1e2150a..5934bab 100644 --- a/metahub_save_node.py +++ b/metahub_save_node.py @@ -16,6 +16,87 @@ except ImportError: _HINT_SHOWN = False +class MetaHubSaveImage: + """ + Simple entry-point node. Keeps the UI focused on normal saving while + delegating all metadata logic to MetaHubSaveNode. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + }, + "optional": { + "filename_pattern": ("STRING", { + "default": "ComfyUI_%counter%", + "tooltip": "Filename pattern with placeholders" + }), + "file_format": (["PNG", "JPEG", "WebP"], { + "default": "PNG", + "tooltip": "File format" + }), + "output_path": ("STRING", { + "default": "", + "tooltip": "Custom output directory (empty = ComfyUI default)" + }), + "tags": ("STRING", { + "default": "", + "tooltip": "Optional Image MetaHub tags (comma-separated)" + }), + "notes": ("STRING", { + "multiline": True, + "default": "", + "tooltip": "Optional Image MetaHub notes" + }), + "project_name": ("STRING", { + "default": "", + "tooltip": "Optional Image MetaHub project name" + }), + }, + "hidden": { + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + "unique_id": "UNIQUE_ID" + }, + } + + RETURN_TYPES = () + FUNCTION = "save_images" + OUTPUT_NODE = True + CATEGORY = "image/save" + DESCRIPTION = "Save images with Image MetaHub metadata. Connect images and generate." + + def save_images( + self, + images, + filename_pattern="ComfyUI_%counter%", + file_format="PNG", + output_path="", + tags="", + notes="", + project_name="", + prompt=None, + extra_pnginfo=None, + unique_id=None, + ): + return MetaHubSaveNode().save_images( + images=images, + filename_pattern=filename_pattern, + file_format=file_format, + output_path=output_path, + user_tags=tags, + notes=notes, + project_name=project_name, + prompt=prompt, + extra_pnginfo=extra_pnginfo, + unique_id=unique_id, + save_node_class_type="MetaHubSaveImage", + save_node_display_name="MetaHub Save Image", + ) + + class MetaHubSaveNode: """ ComfyUI custom node for saving images with comprehensive metadata. @@ -190,6 +271,8 @@ class MetaHubSaveNode: prompt=None, extra_pnginfo=None, unique_id=None, + save_node_class_type="MetaHubSaveNode", + save_node_display_name="MetaHub Save Image Advanced", ): global _HINT_SHOWN @@ -204,7 +287,12 @@ class MetaHubSaveNode: workflow_json = utils.ensure_prompt_in_workflow(workflow_json, prompt_data) save_node_id = str(unique_id) if unique_id is not None else None - utils.ensure_metahub_save_node(workflow_json, save_node_id) + utils.ensure_metahub_save_node( + workflow_json, + save_node_id, + class_type=save_node_class_type, + display_name=save_node_display_name, + ) extractor = WorkflowExtractor(prompt_data) extracted, missing_fields = extractor.extract( @@ -430,5 +518,11 @@ class MetaHubSaveNode: raise RuntimeError(f"MetaHub Save Image failed: {e}") from e -NODE_CLASS_MAPPINGS = {"MetaHubSaveNode": MetaHubSaveNode} -NODE_DISPLAY_NAME_MAPPINGS = {"MetaHubSaveNode": "MetaHub Save Image"} +NODE_CLASS_MAPPINGS = { + "MetaHubSaveImage": MetaHubSaveImage, + "MetaHubSaveNode": MetaHubSaveNode, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "MetaHubSaveImage": "MetaHub Save Image", + "MetaHubSaveNode": "MetaHub Save Image Advanced", +} diff --git a/pyproject.toml b/pyproject.toml index 9f925f2..96f1624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "imagemetahub-comfyui-save" description = "Official companion node for Image MetaHub. Saves A1111/Civitai-compatible parameters plus extended Image MetaHub metadata (workflow JSON, SHA256 model hashes, VRAM peak/total, GPU device, generation time, and steps/sec) for reproducibility and benchmarking." -version = "1.0.8" +version = "1.0.9" license = {file = "LICENSE"} # classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/tests/test_metadata_utils.py b/tests/test_metadata_utils.py index c2fbbc1..3c5e4f4 100644 --- a/tests/test_metadata_utils.py +++ b/tests/test_metadata_utils.py @@ -201,6 +201,35 @@ def test_ensure_metahub_save_node_updates_workflow_node_by_id(): node = workflow["workflow"]["nodes"][0] assert node["type"] == "MetaHubSaveNode" + assert node["title"] == "MetaHub Save Image Advanced" + assert node["properties"]["node_name"] == "MetaHub Save Image Advanced" + + +def test_ensure_metahub_save_node_can_preserve_simple_node_type(): + workflow = { + "prompt": {"5": {"class_type": "SaveImage", "inputs": {}}}, + "workflow": { + "nodes": [ + { + "id": 5, + "type": "SaveImage", + "title": "Save Image", + "properties": {"node_name": "Save Image"}, + } + ] + }, + } + + ensure_metahub_save_node( + workflow, + "5", + class_type="MetaHubSaveImage", + display_name="MetaHub Save Image", + ) + + node = workflow["workflow"]["nodes"][0] + assert workflow["prompt"]["5"]["class_type"] == "MetaHubSaveImage" + assert node["type"] == "MetaHubSaveImage" assert node["title"] == "MetaHub Save Image" assert node["properties"]["node_name"] == "MetaHub Save Image"