feat: add simple save image node
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user