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.
151 lines
6.3 KiB
Python
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"}
|