Files
LuqP2-ImageMetaHub-ComfyUI-…/metahub_save_node.py
T
LuqP2 238e458de6 feat: Implement MetaHub Save Image node for ComfyUI
Complete implementation of advanced image saving node with dual metadata:
- A1111/Civitai compatible metadata (tEXt chunk "parameters")
- Image MetaHub metadata (iTXt chunk "imagemetahub_data")
- Auto-detection of LoRAs from workflow (UI and API formats)
- SHA256 hash calculation (AutoV2/Civitai format) with caching
- Graceful degradation (never interrupts generation)
- IMH Pro fields support (user_tags, notes, project_name)
- Auto-increment filenames with customizable output paths

Files added:
- metahub_save_node.py: Main node class with INPUT_TYPES
- metadata_utils.py: Hash calculation, metadata formatting, PNG injection
- __init__.py: Node registration for ComfyUI
- requirements.txt: Dependencies (Pillow, numpy)
- README.md: Installation and usage documentation

100% compatible with Image MetaHub comfyUIParser.ts specification.
2025-12-19 20:59:47 -03:00

151 lines
6.3 KiB
Python

"""
MetaHub Save Image Node for ComfyUI
Advanced image saving with dual metadata support (A1111/Civitai + Image MetaHub)
"""
from typing import Dict, Any
from . import metadata_utils as utils
class MetaHubSaveNode:
"""
ComfyUI custom node for saving images with comprehensive metadata.
Features:
- A1111/Civitai compatible metadata (tEXt chunk "parameters")
- Image MetaHub metadata (iTXt chunk "imagemetahub_data")
- Auto-detection of LoRAs from workflow
- SHA256 model hashes (AutoV2/Civitai format)
- Graceful degradation (never interrupts generation)
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"positive": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Positive prompt"
}),
"negative": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Negative prompt"
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"tooltip": "Generation seed"
}),
"steps": ("INT", {
"default": 20,
"min": 1,
"max": 10000,
"tooltip": "Sampling steps"
}),
"cfg": ("FLOAT", {
"default": 7.0,
"min": 0.0,
"max": 100.0,
"step": 0.1,
"tooltip": "CFG scale"
}),
"sampler_name": ("STRING", {
"default": "euler",
"tooltip": "Sampler name"
}),
"scheduler": ("STRING", {
"default": "normal",
"tooltip": "Scheduler"
}),
"model_name": ("STRING", {
"default": "",
"tooltip": "Model filename (e.g., model.safetensors)"
}),
},
"optional": {
"vae_name": ("STRING", {
"default": "",
"tooltip": "VAE filename"
}),
"denoise": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Denoise strength"
}),
"upscale_model": ("STRING", {
"default": "",
"tooltip": "Upscale model name"
}),
"generation_time": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"tooltip": "Generation time in seconds"
}),
"user_tags": ("STRING", {
"default": "",
"tooltip": "IMH Pro: User tags (comma-separated)"
}),
"notes": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "IMH Pro: Notes"
}),
"project_name": ("STRING", {
"default": "",
"tooltip": "IMH Pro: Project name"
}),
"filename_prefix": ("STRING", {
"default": "ComfyUI",
"tooltip": "Filename prefix"
}),
"output_path": ("STRING", {
"default": "",
"tooltip": "Custom output directory (empty = ComfyUI default)"
}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "image/save"
DESCRIPTION = "Save images with A1111/Civitai and Image MetaHub metadata"
def save_images(self, images, positive, negative, seed, steps, cfg, sampler_name, scheduler, model_name, vae_name="", denoise=1.0, upscale_model="", generation_time=0.0, user_tags="", notes="", project_name="", filename_prefix="ComfyUI", output_path="", prompt=None, extra_pnginfo=None):
print(f"[MetaHub] Starting save_images for batch of {len(images)} image(s)")
workflow_json = utils.get_workflow_json(extra_pnginfo)
lora_list = utils.extract_loras_from_workflow(workflow_json)
print(f"[MetaHub] Detected {len(lora_list)} LoRA(s) from workflow")
print(f"[MetaHub] Calculating hash for model: {model_name}")
model_hash = utils.calculate_model_hash(model_name, model_type="checkpoint")
print(f"[MetaHub] Calculating hashes for LoRAs...")
lora_hashes = utils.calculate_lora_hashes(lora_list)
height, width = images[0].shape[0], images[0].shape[1]
print(f"[MetaHub] Image dimensions: {width}x{height}")
output_dir = utils.get_output_directory(output_path)
print(f"[MetaHub] Saving to: {output_dir}")
saved_paths = utils.save_image_batch(images, output_dir, filename_prefix)
print(f"[MetaHub] Saved {len(saved_paths)} image(s)")
for idx, file_path in enumerate(saved_paths):
print(f"[MetaHub] Injecting metadata into: {file_path.name}")
params = {'positive': positive, 'negative': negative, 'steps': steps, 'sampler': sampler_name, 'scheduler': scheduler, 'cfg': cfg, 'seed': seed, 'width': width, 'height': height, 'model_name': model_name, 'model_hash': model_hash, 'vae_name': vae_name, 'denoise': denoise, 'upscale_model': upscale_model, 'generation_time': generation_time, 'user_tags': user_tags, 'notes': notes, 'project_name': project_name, 'lora_list': lora_list, 'lora_hashes': lora_hashes}
a1111_metadata = utils.build_a1111_metadata(params)
imh_metadata = utils.build_imh_metadata(params, workflow_json)
utils.inject_metadata_chunks(str(file_path), a1111_metadata, imh_metadata)
print(f"[MetaHub] ✓ All images saved successfully with metadata")
return {}
NODE_CLASS_MAPPINGS = {"MetaHubSaveNode": MetaHubSaveNode}
NODE_DISPLAY_NAME_MAPPINGS = {"MetaHubSaveNode": "MetaHub Save Image"}