|
1 | 1 | import os
|
2 | 2 | from datetime import datetime
|
| 3 | +from dataclasses import dataclass |
3 | 4 | from pathlib import Path
|
4 | 5 | import json
|
5 | 6 | import numpy as np
|
@@ -62,6 +63,110 @@ def make_filename(filename, width, height, seed, modelname, counter, time_format
|
62 | 63 | filename = make_pathname(filename, width, height, seed, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip)
|
63 | 64 | return get_timestamp(time_format) if filename == "" else filename
|
64 | 65 |
|
| 66 | +@dataclass |
| 67 | +class Metadata: |
| 68 | + modelname: str |
| 69 | + positive: str |
| 70 | + negative: str |
| 71 | + width: int |
| 72 | + height: int |
| 73 | + seed: int |
| 74 | + steps: int |
| 75 | + cfg: float |
| 76 | + sampler_name: str |
| 77 | + scheduler: str |
| 78 | + denoise: float |
| 79 | + clip_skip: int |
| 80 | + additional_hashes: str |
| 81 | + |
| 82 | +class ImageSaverMetadata: |
| 83 | + @classmethod |
| 84 | + def INPUT_TYPES(cls): |
| 85 | + return { |
| 86 | + "optional": { |
| 87 | + "modelname": ("STRING", {"default": '', "multiline": False, "tooltip": "model name (can be multiple, separated by commas)"}), |
| 88 | + "positive": ("STRING", {"default": 'unknown', "multiline": True, "tooltip": "positive prompt"}), |
| 89 | + "negative": ("STRING", {"default": 'unknown', "multiline": True, "tooltip": "negative prompt"}), |
| 90 | + "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "tooltip": "image width"}), |
| 91 | + "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "tooltip": "image height"}), |
| 92 | + "seed_value": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "seed"}), |
| 93 | + "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "number of steps"}), |
| 94 | + "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "tooltip": "CFG value"}), |
| 95 | + "sampler_name": ("STRING", {"default": '', "multiline": False, "tooltip": "sampler name (as string)"}), |
| 96 | + "scheduler": ("STRING", {"default": 'normal', "multiline": False, "tooltip": "scheduler name (as string)"}), |
| 97 | + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "tooltip": "denoise value"}), |
| 98 | + "clip_skip": ("INT", {"default": 0, "min": -24, "max": 24, "tooltip": "skip last CLIP layers (positive or negative value, 0 for no skip)"}), |
| 99 | + "additional_hashes": ("STRING", {"default": "", "multiline": False, "tooltip": "hashes separated by commas, optionally with names. 'Name:HASH' (e.g., 'MyLoRA:FF735FF83F98')\nWith download_civitai_data set to true, weights can be added as well. (e.g., 'HASH:Weight', 'Name:HASH:Weight')"}), |
| 100 | + "download_civitai_data": ("BOOLEAN", {"default": True, "tooltip": "Download and cache data from civitai.com to save correct metadata. Allows LoRA weights to be saved to the metadata."}), |
| 101 | + "easy_remix": ("BOOLEAN", {"default": True, "tooltip": "Strip LoRAs and simplify 'embedding:path' from the prompt to make the Remix option on civitai.com more seamless."}), |
| 102 | + }, |
| 103 | + } |
| 104 | + |
| 105 | + RETURN_TYPES = ("METADATA",) |
| 106 | + RETURN_NAMES = ("metadata",) |
| 107 | + OUTPUT_TOOLTIPS = ("metadata for Image Saver Simple",) |
| 108 | + FUNCTION = "get_metadata" |
| 109 | + CATEGORY = "ImageSaver" |
| 110 | + DESCRIPTION = "Prepare metadata for Image Saver Simple" |
| 111 | + |
| 112 | + def get_metadata( |
| 113 | + self, |
| 114 | + modelname, |
| 115 | + positive, |
| 116 | + negative, |
| 117 | + width, |
| 118 | + height, |
| 119 | + seed_value, |
| 120 | + steps, |
| 121 | + cfg, |
| 122 | + sampler_name, |
| 123 | + scheduler, |
| 124 | + denoise, |
| 125 | + clip_skip, |
| 126 | + additional_hashes="", |
| 127 | + download_civitai_data=True, |
| 128 | + easy_remix=True, |
| 129 | + ): |
| 130 | + modelname, additional_hashes = ImageSaver.get_multiple_models(modelname, additional_hashes) |
| 131 | + metadata = Metadata(modelname, positive, negative, width, height, seed_value, steps, cfg, sampler_name, scheduler, denoise, clip_skip, additional_hashes) |
| 132 | + return (metadata,) |
| 133 | + |
| 134 | +class ImageSaverSimple: |
| 135 | + @classmethod |
| 136 | + def INPUT_TYPES(cls): |
| 137 | + return { |
| 138 | + "required": { |
| 139 | + "images": ("IMAGE", { "tooltip": "image(s) to save"}), |
| 140 | + "filename": ("STRING", {"default": '%time_%basemodelname_%seed', "multiline": False, "tooltip": "filename (available variables: %date, %time, %model, %width, %height, %seed, %counter, %sampler_name, %steps, %cfg, %scheduler, %basemodelname, %denoise, %clip_skip)"}), |
| 141 | + "path": ("STRING", {"default": '', "multiline": False, "tooltip": "path to save the images (under Comfy's save directory)"}), |
| 142 | + "extension": (['png', 'jpeg', 'jpg', 'webp'], { "tooltip": "file extension/type to save image as"}), |
| 143 | + "lossless_webp": ("BOOLEAN", {"default": True, "tooltip": "if True, saved WEBP files will be lossless"}), |
| 144 | + "quality_jpeg_or_webp": ("INT", {"default": 100, "min": 1, "max": 100, "tooltip": "quality setting of JPEG/WEBP"}), |
| 145 | + "optimize_png": ("BOOLEAN", {"default": False, "tooltip": "if True, saved PNG files will be optimized (can reduce file size but is slower)"}), |
| 146 | + "embed_workflow": ("BOOLEAN", {"default": True, "tooltip": "if True, embeds the workflow in the saved image files.\nStable for PNG, experimental for WEBP.\nJPEG experimental and only if metadata size is below 65535 bytes"}), |
| 147 | + "save_workflow_as_json": ("BOOLEAN", {"default": False, "tooltip": "if True, also saves the workflow as a separate JSON file"}), |
| 148 | + }, |
| 149 | + "optional": { |
| 150 | + "metadata": ("METADATA", {"default": None, "tooltip": "metadata to embed in the image"}), |
| 151 | + "counter": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "counter"}), |
| 152 | + "time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False, "tooltip": "timestamp format"}), |
| 153 | + }, |
| 154 | + "hidden": { |
| 155 | + "prompt": "PROMPT", |
| 156 | + "extra_pnginfo": "EXTRA_PNGINFO", |
| 157 | + }, |
| 158 | + } |
| 159 | + |
| 160 | + RETURN_TYPES = ("STRING","STRING") |
| 161 | + RETURN_NAMES = ("hashes","a1111_params") |
| 162 | + OUTPUT_TOOLTIPS = ("Comma-separated list of the hashes to chain with other Image Saver additional_hashes","Written parameters to the image metadata") |
| 163 | + FUNCTION = "save_files" |
| 164 | + |
| 165 | + OUTPUT_NODE = True |
| 166 | + |
| 167 | + CATEGORY = "ImageSaver" |
| 168 | + DESCRIPTION = "Save images with civitai-compatible generation metadata" |
| 169 | + |
65 | 170 | class ImageSaver:
|
66 | 171 | @classmethod
|
67 | 172 | def INPUT_TYPES(cls):
|
@@ -117,6 +222,22 @@ def INPUT_TYPES(cls):
|
117 | 222 | # Match 'anything', 'anything:anything' or 'anything:anything:number' with trimmed white space
|
118 | 223 | re_manual_hash_weights = re.compile(r'^\s*([^:]+?)(?:\s*:\s*([^\s:][^:]*?))?(?:\s*:\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+)))?\s*$')
|
119 | 224 |
|
| 225 | + @staticmethod |
| 226 | + def get_multiple_models(modelname, additional_hashes): |
| 227 | + model_names = [m.strip() for m in modelname.split(',')] |
| 228 | + modelname = model_names[0] # Use the first model as the primary one |
| 229 | + |
| 230 | + # Process additional model names and add to additional_hashes |
| 231 | + for additional_model in model_names[1:]: |
| 232 | + additional_ckpt_path = full_checkpoint_path_for(additional_model) |
| 233 | + if additional_ckpt_path: |
| 234 | + additional_modelhash = get_sha256(additional_ckpt_path)[:10] |
| 235 | + # Add to additional_hashes in "name:HASH" format |
| 236 | + if additional_hashes: |
| 237 | + additional_hashes += "," |
| 238 | + additional_hashes += f"{additional_model}:{additional_modelhash}" |
| 239 | + return modelname, additional_hashes |
| 240 | + |
120 | 241 | def save_files(
|
121 | 242 | self,
|
122 | 243 | images,
|
@@ -148,19 +269,7 @@ def save_files(
|
148 | 269 | prompt=None,
|
149 | 270 | extra_pnginfo=None,
|
150 | 271 | ):
|
151 |
| - model_names = [m.strip() for m in modelname.split(',')] |
152 |
| - modelname = model_names[0] # Use the first model as the primary one |
153 |
| - |
154 |
| - # Process additional model names and add to additional_hashes |
155 |
| - for additional_model in model_names[1:]: |
156 |
| - additional_ckpt_path = full_checkpoint_path_for(additional_model) |
157 |
| - if additional_ckpt_path: |
158 |
| - additional_modelhash = get_sha256(additional_ckpt_path)[:10] |
159 |
| - # Add to additional_hashes in "name:HASH" format |
160 |
| - if additional_hashes: |
161 |
| - additional_hashes += "," |
162 |
| - additional_hashes += f"{additional_model}:{additional_modelhash}" |
163 |
| - |
| 272 | + modelname, additional_hashes = ImageSaver.get_multiple_models(modelname, additional_hashes) |
164 | 273 | filename = make_filename(filename, width, height, seed_value, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip)
|
165 | 274 | path = make_pathname(path, width, height, seed_value, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip)
|
166 | 275 |
|
|
0 commit comments