feat: add simple save image node

This commit is contained in:
LuqP2
2026-06-01 16:46:32 -03:00
parent 9520681482
commit b206843464
6 changed files with 154 additions and 20 deletions
+9 -5
View File
@@ -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`
+5 -3
View File
@@ -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"
}
+13 -8
View File
@@ -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:
+97 -3
View File
@@ -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",
}
+1 -1
View File
@@ -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)
+29
View File
@@ -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"