From 8a260b9729defa71fbd702b858d2b48afeeab99f Mon Sep 17 00:00:00 2001 From: Alexopus Date: Mon, 9 Jun 2025 09:09:47 +0200 Subject: [PATCH] Enable Simple+Metadata --- __init__.py | 8 +- example-workflow.json | 1560 +++++++++++++++++----------------- nodes.py | 333 +++++--- nodes_literals.py | 2 +- nodes_selectors.py | 2 +- prompt_metadata_extractor.py | 2 +- saver/saver.py | 14 +- utils.py | 7 +- utils_civitai.py | 4 +- 9 files changed, 986 insertions(+), 946 deletions(-) diff --git a/__init__.py b/__init__.py index cda2631..f249bba 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,17 @@ -from .nodes import ImageSaver +from typing import Any + +from .nodes import ImageSaver, ImageSaverSimple, ImageSaverMetadata from .nodes_literals import SeedGenerator, StringLiteral, SizeLiteral, IntLiteral, FloatLiteral, CfgLiteral from .nodes_loaders import CheckpointLoaderWithName, UNETLoaderWithName from .nodes_selectors import SamplerSelector, SchedulerSelector, SchedulerSelectorComfy, SchedulerToString, SamplerToString, SchedulerComfyToString, InputParameters from .civitai_nodes import CivitaiHashFetcher -NODE_CLASS_MAPPINGS = { +NODE_CLASS_MAPPINGS: dict[str, Any] = { "Checkpoint Loader with Name (Image Saver)": CheckpointLoaderWithName, "UNet loader with Name (Image Saver)": UNETLoaderWithName, "Image Saver": ImageSaver, + "Image Saver Simple": ImageSaverSimple, + "Image Saver Metadata": ImageSaverMetadata, "Sampler Selector (Image Saver)": SamplerSelector, "Scheduler Selector (Image Saver)": SchedulerSelector, "Scheduler Selector (Comfy) (Image Saver)": SchedulerSelectorComfy, diff --git a/example-workflow.json b/example-workflow.json index de1f898..34a09ff 100644 --- a/example-workflow.json +++ b/example-workflow.json @@ -1,20 +1,460 @@ { - "last_node_id": 30, - "last_link_id": 82, + "id": "e4602d36-6fc0-42ed-b90b-c3ee838a55a8", + "revision": 0, + "last_node_id": 54, + "last_link_id": 134, "nodes": [ + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1110, + -140 + ], + "size": [ + 140, + 50 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 45 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 107 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.40", + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 24, + "type": "CLIPTextEncode", + "pos": [ + 630, + -120 + ], + "size": [ + 210.26666259765625, + 88 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 44 + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 34 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 36 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.40", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 630, + 10 + ], + "size": [ + 210.26666259765625, + 88 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 43 + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 35 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 6 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.40", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 22, + "type": "String Literal (Image Saver)", + "pos": [ + 310, + -150 + ], + "size": [ + 300, + 90 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "slot_index": 0, + "links": [ + 34, + 111 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "String Literal (Image Saver)" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, purple galaxy bottle" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 23, + "type": "String Literal (Image Saver)", + "pos": [ + 310, + -20 + ], + "size": [ + 300, + 90 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "slot_index": 0, + "links": [ + 35, + 112 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "String Literal (Image Saver)" + }, + "widgets_values": [ + "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 25, + "type": "Checkpoint Loader with Name (Image Saver)", + "pos": [ + 270, + 110 + ], + "size": [ + 344.3999938964844, + 118 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 42 + ] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [ + 43, + 44 + ] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 45 + ] + }, + { + "name": "model_name", + "type": "STRING", + "slot_index": 3, + "links": [ + 110 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Checkpoint Loader with Name (Image Saver)" + }, + "widgets_values": [ + "1.5/theAllysMixIII_v10.safetensors" + ] + }, + { + "id": 53, + "type": "Input Parameters (Image Saver)", + "pos": [ + 350, + 270 + ], + "size": [ + 260, + 342 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "seed", + "type": "INT", + "links": [ + 121, + 122 + ] + }, + { + "name": "steps", + "type": "INT", + "links": [ + 123, + 124 + ] + }, + { + "name": "cfg", + "type": "FLOAT", + "links": [ + 125, + 126 + ] + }, + { + "name": "sampler", + "type": "COMBO", + "links": [ + 134 + ] + }, + { + "name": "sampler_name", + "type": "STRING", + "links": [ + 128 + ] + }, + { + "name": "scheduler", + "type": "COMBO", + "links": [ + 133 + ] + }, + { + "name": "scheduler_name", + "type": "STRING", + "links": [ + 130 + ] + }, + { + "name": "denoise", + "type": "FLOAT", + "links": [ + 131, + 132 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Input Parameters (Image Saver)" + }, + "widgets_values": [ + 582528133228867, + "randomize", + 20, + 7, + "euler", + "normal", + 1 + ] + }, + { + "id": 12, + "type": "Width/Height Literal (Image Saver)", + "pos": [ + 320, + 650 + ], + "size": [ + 290, + 60 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "size", + "type": "INT", + "slot_index": 0, + "links": [ + 20, + 113 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Width/Height Literal (Image Saver)" + }, + "widgets_values": [ + 512 + ] + }, + { + "id": 13, + "type": "Width/Height Literal (Image Saver)", + "pos": [ + 320, + 750 + ], + "size": [ + 290, + 60 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "size", + "type": "INT", + "slot_index": 0, + "links": [ + 21, + 114 + ] + } + ], + "properties": { + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Width/Height Literal (Image Saver)" + }, + "widgets_values": [ + 512 + ] + }, { "id": 3, "type": "KSampler", - "pos": { - "0": 810, - "1": -140 - }, - "size": { - "0": 210, - "1": 230 - }, + "pos": [ + 880, + -140 + ], + "size": [ + 220, + 262 + ], "flags": {}, - "order": 14, + "order": 10, "mode": 0, "inputs": [ { @@ -37,70 +477,72 @@ "type": "LATENT", "link": 2 }, - { - "name": "sampler_name", - "type": "COMBO", - "link": 10, - "widget": { - "name": "sampler_name" - } - }, - { - "name": "scheduler", - "type": "COMBO", - "link": 57, - "widget": { - "name": "scheduler" - } - }, { "name": "seed", "type": "INT", - "link": 18, "widget": { "name": "seed" - } + }, + "link": 121 }, { "name": "steps", "type": "INT", - "link": 25, "widget": { "name": "steps" - } + }, + "link": 123 }, { "name": "cfg", "type": "FLOAT", - "link": 28, "widget": { "name": "cfg" - } + }, + "link": 125 + }, + { + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": 134 + }, + { + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": 133 }, { "name": "denoise", "type": "FLOAT", - "link": 31, "widget": { "name": "denoise" - } + }, + "link": 132 } ], "outputs": [ { "name": "LATENT", "type": "LATENT", + "slot_index": 0, "links": [ 7 - ], - "slot_index": 0 + ] } ], "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.40", "Node name for S&R": "KSampler" }, "widgets_values": [ - 680957322507703, + 453912465950566, "randomize", 20, 8, @@ -110,257 +552,207 @@ ] }, { - "id": 7, - "type": "CLIPTextEncode", - "pos": { - "0": 520, - "1": -40 - }, - "size": { - "0": 210, - "1": 54 - }, + "id": 35, + "type": "Image Saver Metadata", + "pos": [ + 880, + 160 + ], + "size": [ + 222, + 494 + ], "flags": {}, - "order": 12, + "order": 9, "mode": 0, "inputs": [ { - "name": "clip", - "type": "CLIP", - "link": 43 - }, - { - "name": "text", + "name": "modelname", + "shape": 7, "type": "STRING", - "link": 35, "widget": { - "name": "text" - } - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 6 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" - ], - "color": "#322", - "bgcolor": "#533" - }, - { - "id": 24, - "type": "CLIPTextEncode", - "pos": { - "0": 520, - "1": -170 - }, - "size": { - "0": 210, - "1": 54 - }, - "flags": {}, - "order": 13, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 44 + "name": "modelname" + }, + "link": 110 }, { - "name": "text", + "name": "positive", + "shape": 7, "type": "STRING", - "link": 34, "widget": { - "name": "text" - } - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 36 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" - ], - "color": "#232", - "bgcolor": "#353" - }, - { - "id": 8, - "type": "VAEDecode", - "pos": { - "0": 1030, - "1": -140 - }, - "size": { - "0": 140, - "1": 50 - }, - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "name": "samples", - "type": "LATENT", - "link": 7 + "name": "positive" + }, + "link": 111 }, { - "name": "vae", - "type": "VAE", - "link": 45 - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 72 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "VAEDecode" - }, - "widgets_values": [] - }, - { - "id": 22, - "type": "String Literal (Image Saver)", - "pos": { - "0": 200, - "1": -150 - }, - "size": { - "0": 300, - "1": 90 - }, - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", + "name": "negative", + "shape": 7, "type": "STRING", + "widget": { + "name": "negative" + }, + "link": 112 + }, + { + "name": "width", + "shape": 7, + "type": "INT", + "widget": { + "name": "width" + }, + "link": 113 + }, + { + "name": "height", + "shape": 7, + "type": "INT", + "widget": { + "name": "height" + }, + "link": 114 + }, + { + "name": "seed_value", + "shape": 7, + "type": "INT", + "widget": { + "name": "seed_value" + }, + "link": 122 + }, + { + "name": "steps", + "shape": 7, + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 124 + }, + { + "name": "cfg", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 126 + }, + { + "name": "sampler_name", + "shape": 7, + "type": "STRING", + "widget": { + "name": "sampler_name" + }, + "link": 128 + }, + { + "name": "scheduler_name", + "shape": 7, + "type": "STRING", + "widget": { + "name": "scheduler_name" + }, + "link": 130 + }, + { + "name": "denoise", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": 131 + } + ], + "outputs": [ + { + "name": "metadata", + "type": "METADATA", "links": [ - 34, - 73 - ], - "slot_index": 0, - "shape": 3 + 92 + ] + }, + { + "name": "hashes", + "type": "STRING", + "links": [] + }, + { + "name": "a1111_params", + "type": "STRING", + "links": [] } ], "properties": { - "Node name for S&R": "String Literal (Image Saver)" + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Image Saver Metadata" }, "widgets_values": [ - "beautiful scenery nature glass bottle landscape, purple galaxy bottle" - ], - "color": "#232", - "bgcolor": "#353" - }, - { - "id": 23, - "type": "String Literal (Image Saver)", - "pos": { - "0": 200, - "1": -20 - }, - "size": { - "0": 300, - "1": 90 - }, - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "STRING", - "type": "STRING", - "links": [ - 35, - 74 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "String Literal (Image Saver)" - }, - "widgets_values": [ - "(worst quality, low quality, bad quality:1.3), embedding:ng_deepnegative_v1_75t, embedding:EasyNegative, embedding:badhandv4" - ], - "color": "#322", - "bgcolor": "#533" + "", + "unknown", + "unknown", + 512, + 512, + 0, + 20, + 7, + "", + "normal", + 1, + 0, + "", + false, + false + ] }, { "id": 5, "type": "EmptyLatentImage", - "pos": { - "0": 620, - "1": 660 - }, - "size": { - "0": 210, - "1": 80 - }, + "pos": [ + 620, + 670 + ], + "size": [ + 210, + 106 + ], "flags": {}, - "order": 11, + "order": 8, "mode": 0, "inputs": [ { "name": "width", "type": "INT", - "link": 20, "widget": { "name": "width" - } + }, + "link": 20 }, { "name": "height", "type": "INT", - "link": 21, "widget": { "name": "height" - } + }, + "link": 21 } ], "outputs": [ { "name": "LATENT", "type": "LATENT", + "slot_index": 0, "links": [ 2 - ], - "slot_index": 0 + ] } ], "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.40", "Node name for S&R": "EmptyLatentImage" }, "widgets_values": [ @@ -370,491 +762,60 @@ ] }, { - "id": 21, - "type": "Float Literal (Image Saver)", - "pos": { - "0": 350, - "1": 920 - }, - "size": { - "0": 230, - "1": 60 - }, - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 31, - 81 - ], - "slot_index": 0, - "shape": 3 - } + "id": 34, + "type": "Image Saver Simple", + "pos": [ + 1260, + -140 ], - "properties": { - "Node name for S&R": "Float Literal (Image Saver)" - }, - "widgets_values": [ - 1 - ] - }, - { - "id": 19, - "type": "Int Literal (Image Saver)", - "pos": { - "0": 370, - "1": 820 - }, - "size": { - "0": 210, - "1": 60 - }, - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 25, - 75 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Int Literal (Image Saver)" - }, - "widgets_values": [ - 30 - ] - }, - { - "id": 13, - "type": "Width/Height Literal (Image Saver)", - "pos": { - "0": 290, - "1": 720 - }, - "size": { - "0": 290, - "1": 60 - }, - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "size", - "type": "INT", - "links": [ - 21, - 80 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Width/Height Literal (Image Saver)" - }, - "widgets_values": [ - 512 - ] - }, - { - "id": 12, - "type": "Width/Height Literal (Image Saver)", - "pos": { - "0": 290, - "1": 620 - }, - "size": { - "0": 290, - "1": 60 - }, - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "size", - "type": "INT", - "links": [ - 20, - 79 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Width/Height Literal (Image Saver)" - }, - "widgets_values": [ - 512 - ] - }, - { - "id": 20, - "type": "Float Literal (Image Saver)", - "pos": { - "0": 350, - "1": 520 - }, - "size": { - "0": 230, - "1": 60 - }, - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "FLOAT", - "type": "FLOAT", - "links": [ - 28, - 76 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Float Literal (Image Saver)" - }, - "widgets_values": [ - 7 - ] - }, - { - "id": 18, - "type": "Seed Generator (Image Saver)", - "pos": { - "0": 340, - "1": 400 - }, - "size": { - "0": 240, - "1": 82 - }, - "flags": {}, - "order": 7, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 18, - 78 - ], - "slot_index": 0, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Seed Generator (Image Saver)" - }, - "widgets_values": [ - 42, - "fixed" - ] - }, - { - "id": 27, - "type": "Scheduler Selector (Comfy) (Image Saver)", - "pos": { - "0": 240, - "1": 280 - }, - "size": { - "0": 340, - "1": 80 - }, - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "scheduler", - "type": "COMBO", - "links": [ - 57 - ], - "slot_index": 0, - "shape": 3 - }, - { - "name": "scheduler_name", - "type": "STRING", - "links": [ - 77 - ], - "slot_index": 1, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Scheduler Selector (Comfy) (Image Saver)" - }, - "widgets_values": [ - "normal" - ] - }, - { - "id": 16, - "type": "Sampler Selector (Image Saver)", - "pos": { - "0": 330, - "1": 160 - }, - "size": { - "0": 252, - "1": 78 - }, - "flags": {}, - "order": 9, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "sampler", - "type": "COMBO", - "links": [ - 10 - ], - "slot_index": 0, - "shape": 3 - }, - { - "name": "sampler_name", - "type": "STRING", - "links": [ - 82 - ], - "slot_index": 1, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Sampler Selector (Image Saver)" - }, - "widgets_values": [ - "dpmpp_2m_sde" - ] - }, - { - "id": 25, - "type": "Checkpoint Loader with Name (Image Saver)", - "pos": { - "0": -40, - "1": 110 - }, - "size": { - "0": 344.3999938964844, - "1": 118 - }, - "flags": {}, - "order": 10, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "MODEL", - "type": "MODEL", - "links": [ - 42 - ], - "slot_index": 0, - "shape": 3 - }, - { - "name": "CLIP", - "type": "CLIP", - "links": [ - 43, - 44 - ], - "slot_index": 1, - "shape": 3 - }, - { - "name": "VAE", - "type": "VAE", - "links": [ - 45 - ], - "slot_index": 2, - "shape": 3 - }, - { - "name": "model_name", - "type": "STRING", - "links": [], - "slot_index": 3, - "shape": 3 - } - ], - "properties": { - "Node name for S&R": "Checkpoint Loader with Name (Image Saver)" - }, - "widgets_values": [ - "1.5/theAllysMixIII_v10.safetensors" - ] - }, - { - "id": 30, - "type": "Image Saver", - "pos": { - "0": 1250, - "1": 190 - }, "size": [ - 310, - 734 + 260, + 520 ], "flags": {}, - "order": 16, + "order": 12, "mode": 0, "inputs": [ { "name": "images", "type": "IMAGE", - "link": 72 + "link": 107 }, { - "name": "positive", + "name": "metadata", + "shape": 7, + "type": "METADATA", + "link": 92 + } + ], + "outputs": [ + { + "name": "hashes", "type": "STRING", - "link": 73, - "widget": { - "name": "positive" - }, - "shape": 7 + "links": [] }, { - "name": "negative", + "name": "a1111_params", "type": "STRING", - "link": 74, - "widget": { - "name": "negative" - }, - "shape": 7 - }, - { - "name": "steps", - "type": "INT", - "link": 75, - "widget": { - "name": "steps" - }, - "shape": 7 - }, - { - "name": "cfg", - "type": "FLOAT", - "link": 76, - "widget": { - "name": "cfg" - }, - "shape": 7 - }, - { - "name": "scheduler", - "type": "STRING", - "link": 77, - "widget": { - "name": "scheduler" - }, - "shape": 7 - }, - { - "name": "seed_value", - "type": "INT", - "link": 78, - "widget": { - "name": "seed_value" - }, - "shape": 7 - }, - { - "name": "width", - "type": "INT", - "link": 79, - "widget": { - "name": "width" - }, - "shape": 7 - }, - { - "name": "height", - "type": "INT", - "link": 80, - "widget": { - "name": "height" - }, - "shape": 7 - }, - { - "name": "denoise", - "type": "FLOAT", - "link": 81, - "widget": { - "name": "denoise" - }, - "shape": 7 - }, - { - "name": "sampler_name", - "type": "STRING", - "link": 82, - "widget": { - "name": "sampler_name" - }, - "shape": 7 + "links": [] } ], - "outputs": [], "properties": { - "Node name for S&R": "Image Saver" + "cnr_id": "comfyui-image-saver", + "ver": "90bd72c89ab37942d0269a40344cd142562c0851", + "Node name for S&R": "Image Saver Simple" }, "widgets_values": [ - "%time_%basemodelname_%seed", + "%time_%seed", "", - "png", - 20, - 7, - "", - "", - "normal", - "unknown", - "unknown", - 0, - 512, - 512, + "jpg", true, 100, false, - 0, - 1, - 0, - "%Y-%m-%d-%H%M%S", false, - true + false, + 0, + "%Y-%m-%d-%H%M%S" ] } ], @@ -883,22 +844,6 @@ 0, "LATENT" ], - [ - 10, - 16, - 0, - 3, - 4, - "COMBO" - ], - [ - 18, - 18, - 0, - 3, - 6, - "INT" - ], [ 20, 12, @@ -915,30 +860,6 @@ 1, "INT" ], - [ - 25, - 19, - 0, - 3, - 7, - "INT" - ], - [ - 28, - 20, - 0, - 3, - 8, - "FLOAT" - ], - [ - 31, - 21, - 0, - 3, - 9, - "FLOAT" - ], [ 34, 22, @@ -996,100 +917,156 @@ "VAE" ], [ - 57, - 27, + 92, + 35, 0, - 3, - 5, - "COMBO" + 34, + 1, + "METADATA" ], [ - 72, + 107, 8, 0, - 30, + 34, 0, "IMAGE" ], [ - 73, + 110, + 25, + 3, + 35, + 0, + "STRING" + ], + [ + 111, 22, 0, - 30, + 35, 1, "STRING" ], [ - 74, + 112, 23, 0, - 30, + 35, 2, "STRING" ], [ - 75, - 19, + 113, + 12, 0, - 30, + 35, 3, "INT" ], [ - 76, - 20, + 114, + 13, 0, - 30, + 35, 4, - "FLOAT" + "INT" ], [ - 77, - 27, - 1, - 30, - 5, - "STRING" - ], - [ - 78, - 18, + 121, + 53, 0, - 30, + 3, + 4, + "INT" + ], + [ + 122, + 53, + 0, + 35, + 5, + "INT" + ], + [ + 123, + 53, + 1, + 3, + 5, + "INT" + ], + [ + 124, + 53, + 1, + 35, 6, "INT" ], [ - 79, - 12, - 0, - 30, + 125, + 53, + 2, + 3, + 6, + "FLOAT" + ], + [ + 126, + 53, + 2, + 35, 7, - "INT" + "FLOAT" ], [ - 80, - 13, - 0, - 30, + 128, + 53, + 4, + 35, 8, - "INT" + "STRING" ], [ - 81, - 21, - 0, - 30, + 130, + 53, + 6, + 35, + 9, + "STRING" + ], + [ + 131, + 53, + 7, + 35, + 10, + "FLOAT" + ], + [ + 132, + 53, + 7, + 3, 9, "FLOAT" ], [ - 82, - 16, - 1, - 30, - 10, - "STRING" + 133, + 53, + 5, + 3, + 8, + "COMBO" + ], + [ + 134, + 53, + 3, + 3, + 7, + "COMBO" ] ], "groups": [], @@ -1101,7 +1078,8 @@ 739.0102049088814, 603.3338883928535 ] - } + }, + "frontendVersion": "1.21.7" }, "version": 0.4 } \ No newline at end of file diff --git a/nodes.py b/nodes.py index aab438d..6428768 100644 --- a/nodes.py +++ b/nodes.py @@ -33,7 +33,7 @@ def get_timestamp(time_format: str) -> str: return timestamp -def save_json(image_info: dict[str, Any], filename: str) -> None: +def save_json(image_info: dict[str, Any] | None, filename: str) -> None: try: workflow = (image_info or {}).get('workflow') if workflow is None: @@ -44,7 +44,7 @@ def save_json(image_info: dict[str, Any], filename: str) -> None: except Exception as e: print(f'Failed to save workflow as json due to: {e}, proceeding with the remainder of saving execution') -def make_pathname(filename: str, width: int, height: int, seed: int, modelname: str, counter: int, time_format: str, sampler_name: str, steps: int, cfg: float, scheduler: str, denoise: float, clip_skip: int) -> str: +def make_pathname(filename: str, width: int, height: int, seed: int, modelname: str, counter: int, time_format: str, sampler_name: str, steps: int, cfg: float, scheduler_name: str, denoise: float, clip_skip: int) -> str: filename = filename.replace("%date", get_timestamp("%Y-%m-%d")) filename = filename.replace("%time", get_timestamp(time_format)) filename = filename.replace("%model", parse_checkpoint_name(modelname)) @@ -55,14 +55,14 @@ def make_pathname(filename: str, width: int, height: int, seed: int, modelname: filename = filename.replace("%sampler_name", sampler_name) filename = filename.replace("%steps", str(steps)) filename = filename.replace("%cfg", str(cfg)) - filename = filename.replace("%scheduler", scheduler) + filename = filename.replace("%scheduler_name", scheduler_name) filename = filename.replace("%basemodelname", parse_checkpoint_name_without_extension(modelname)) filename = filename.replace("%denoise", str(denoise)) filename = filename.replace("%clip_skip", str(clip_skip)) return filename -def make_filename(filename: str, width: int, height: int, seed: int, modelname: str, counter: int, time_format: str, sampler_name: str, steps: int, cfg: float, scheduler: str, denoise: float, clip_skip: int) -> str: - filename = make_pathname(filename, width, height, seed, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip) +def make_filename(filename: str, width: int, height: int, seed: int, modelname: str, counter: int, time_format: str, sampler_name: str, steps: int, cfg: float, scheduler_name: str, denoise: float, clip_skip: int) -> str: + filename = make_pathname(filename, width, height, seed, modelname, counter, time_format, sampler_name, steps, cfg, scheduler_name, denoise, clip_skip) return get_timestamp(time_format) if filename == "" else filename @dataclass @@ -76,10 +76,13 @@ class Metadata: steps: int cfg: float sampler_name: str - scheduler: str + scheduler_name: str denoise: float clip_skip: int additional_hashes: str + ckpt_path: str + a111_params: str + final_hashes: str class ImageSaverMetadata: @classmethod @@ -95,7 +98,7 @@ class ImageSaverMetadata: "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "number of steps"}), "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "tooltip": "CFG value"}), "sampler_name": ("STRING", {"default": '', "multiline": False, "tooltip": "sampler name (as string)"}), - "scheduler": ("STRING", {"default": 'normal', "multiline": False, "tooltip": "scheduler name (as string)"}), + "scheduler_name": ("STRING", {"default": 'normal', "multiline": False, "tooltip": "scheduler name (as string)"}), "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "tooltip": "denoise value"}), "clip_skip": ("INT", {"default": 0, "min": -24, "max": 24, "tooltip": "skip last CLIP layers (positive or negative value, 0 for no skip)"}), "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')"}), @@ -104,34 +107,81 @@ class ImageSaverMetadata: }, } - RETURN_TYPES = ("METADATA",) - RETURN_NAMES = ("metadata",) - OUTPUT_TOOLTIPS = ("metadata for Image Saver Simple",) + RETURN_TYPES = ("METADATA","STRING","STRING") + RETURN_NAMES = ("metadata","hashes","a1111_params") + OUTPUT_TOOLTIPS = ("metadata for Image Saver Simple","Comma-separated list of the hashes to chain with other Image Saver additional_hashes","Written parameters to the image metadata") FUNCTION = "get_metadata" CATEGORY = "ImageSaver" DESCRIPTION = "Prepare metadata for Image Saver Simple" def get_metadata( self, - modelname: str, - positive: str, - negative: str, - width: int, - height: int, - seed_value: int, - steps: int, - cfg: float, - sampler_name: str, - scheduler: str, - denoise: float, - clip_skip: int, + modelname: str = "", + positive: str = "unknown", + negative: str = "unknown", + width: int = 512, + height: int = 512, + seed_value: int = 0, + steps: int = 20, + cfg: float = 7.0, + sampler_name: str = "", + scheduler_name: str = "", + denoise: float = 1.0, + clip_skip: int = 0, additional_hashes: str = "", download_civitai_data: bool = True, easy_remix: bool = True, - ): + ) -> tuple[Metadata, str, str]: + metadata = ImageSaverMetadata.make_metadata(modelname, positive, negative, width, height, seed_value, steps, cfg, sampler_name, scheduler_name, denoise, clip_skip, additional_hashes, download_civitai_data, easy_remix) + return (metadata, metadata.final_hashes, metadata.a111_params) + + @staticmethod + def make_metadata(modelname: str, positive: str, negative: str, width: int, height: int, seed_value: int, steps: int, cfg: float, sampler_name: str, scheduler_name: str, denoise: float, clip_skip: int, additional_hashes: str, download_civitai_data: bool, easy_remix: bool) -> Metadata: modelname, additional_hashes = ImageSaver.get_multiple_models(modelname, additional_hashes) - metadata = Metadata(modelname, positive, negative, width, height, seed_value, steps, cfg, sampler_name, scheduler, denoise, clip_skip, additional_hashes) - return (metadata,) + + ckpt_path = full_checkpoint_path_for(modelname) + if ckpt_path: + modelhash = get_sha256(ckpt_path)[:10] + else: + modelhash = "" + + metadata_extractor = PromptMetadataExtractor([positive, negative]) + embeddings = metadata_extractor.get_embeddings() + loras = metadata_extractor.get_loras() + civitai_sampler_name = get_civitai_sampler_name(sampler_name.replace('_gpu', ''), scheduler_name) + basemodelname = parse_checkpoint_name_without_extension(modelname) + + # Get existing hashes from model, loras, and embeddings + existing_hashes = {modelhash.lower()} | {t[2].lower() for t in loras.values()} | {t[2].lower() for t in embeddings.values()} + # Parse manual hashes + manual_entries = ImageSaver.parse_manual_hashes(additional_hashes, existing_hashes, download_civitai_data) + # Get Civitai metadata + civitai_resources, hashes, add_model_hash = get_civitai_metadata(modelname, ckpt_path, modelhash, loras, embeddings, manual_entries, download_civitai_data) + + if easy_remix: + positive = ImageSaver.clean_prompt(positive, metadata_extractor) + negative = ImageSaver.clean_prompt(negative, metadata_extractor) + + positive_a111_params = positive.strip() + negative_a111_params = f"\nNegative prompt: {negative.strip()}" + clip_skip_str = f", Clip skip: {abs(clip_skip)}" if clip_skip != 0 else "" + model_hash_str = f", Model hash: {add_model_hash}" if add_model_hash else "" + hashes_str = f", Hashes: {json.dumps(hashes, separators=(',', ':'))}" if hashes else "" + + a111_params = ( + f"{positive_a111_params}{negative_a111_params}\n" + f"Steps: {steps}, Sampler: {civitai_sampler_name}, CFG scale: {cfg}, Seed: {seed_value}, " + f"Size: {width}x{height}{clip_skip_str}{model_hash_str}, Model: {basemodelname}{hashes_str}, Version: ComfyUI" + ) + + # Add Civitai resource listing + if download_civitai_data and civitai_resources: + a111_params += f", Civitai resources: {json.dumps(civitai_resources, separators=(',', ':'))}" + + final_hashes = ",".join(f"{Path(name.split(':')[-1]).stem + ':' if name else ''}{hash}{':' + str(weight) if weight is not None and download_civitai_data else ''}" for name, (_, weight, hash) in ({ modelname: ( ckpt_path, None, modelhash ) } | loras | embeddings | manual_entries).items()) + + metadata = Metadata(modelname, positive, negative, width, height, seed_value, steps, cfg, sampler_name, scheduler_name, denoise, clip_skip, additional_hashes, ckpt_path, a111_params, final_hashes) + return metadata class ImageSaverSimple: @classmethod @@ -139,7 +189,7 @@ class ImageSaverSimple: return { "required": { "images": ("IMAGE", { "tooltip": "image(s) to save"}), - "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)"}), + "filename": ("STRING", {"default": '%time_%basemodelname_%seed', "multiline": False, "tooltip": "filename (available variables: %date, %time, %model, %width, %height, %seed, %counter, %sampler_name, %steps, %cfg, %scheduler_name, %basemodelname, %denoise, %clip_skip)"}), "path": ("STRING", {"default": '', "multiline": False, "tooltip": "path to save the images (under Comfy's save directory)"}), "extension": (['png', 'jpeg', 'jpg', 'webp'], { "tooltip": "file extension/type to save image as"}), "lossless_webp": ("BOOLEAN", {"default": True, "tooltip": "if True, saved WEBP files will be lossless"}), @@ -162,13 +212,41 @@ class ImageSaverSimple: RETURN_TYPES = ("STRING","STRING") RETURN_NAMES = ("hashes","a1111_params") OUTPUT_TOOLTIPS = ("Comma-separated list of the hashes to chain with other Image Saver additional_hashes","Written parameters to the image metadata") - FUNCTION = "save_files" + FUNCTION = "save_images" OUTPUT_NODE = True CATEGORY = "ImageSaver" DESCRIPTION = "Save images with civitai-compatible generation metadata" + def save_images(self, + images: list[torch.Tensor], + filename: str, + path: str, + extension: str, + lossless_webp: bool, + quality_jpeg_or_webp: int, + optimize_png: bool, + embed_workflow: bool = True, + save_workflow_as_json: bool = False, + metadata: Metadata | None = None, + counter: int = 0, + time_format: str = "%Y-%m-%d-%H%M%S", + prompt: dict[str, Any] | None = None, + extra_pnginfo: dict[str, Any] | None = None, + ) -> dict[str, Any]: + if metadata is None: + metadata = Metadata('', '', '', 512, 512, 0, 20, 7.0, '', 'normal', 1.0, 0, '', '', '', '') + + filenames = ImageSaver.save_images(images, filename, extension, path, quality_jpeg_or_webp, lossless_webp, optimize_png, prompt, extra_pnginfo, save_workflow_as_json, embed_workflow, counter, time_format, metadata) + + subfolder = os.path.normpath(path) + + return { + "result": (metadata.final_hashes, metadata.a111_params), + "ui": {"images": map(lambda filename: {"filename": filename, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'}, filenames)}, + } + class ImageSaver: @classmethod def INPUT_TYPES(cls) -> dict[str, Any]: @@ -184,7 +262,7 @@ class ImageSaver: "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "tooltip": "CFG value"}), "modelname": ("STRING", {"default": '', "multiline": False, "tooltip": "model name (can be multiple, separated by commas)"}), "sampler_name": ("STRING", {"default": '', "multiline": False, "tooltip": "sampler name (as string)"}), - "scheduler": ("STRING", {"default": 'normal', "multiline": False, "tooltip": "scheduler name (as string)"}), + "scheduler_name": ("STRING", {"default": 'normal', "multiline": False, "tooltip": "scheduler name (as string)"}), "positive": ("STRING", {"default": 'unknown', "multiline": True, "tooltip": "positive prompt"}), "negative": ("STRING", {"default": 'unknown', "multiline": True, "tooltip": "negative prompt"}), "seed_value": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "seed"}), @@ -219,6 +297,92 @@ class ImageSaver: CATEGORY = "ImageSaver" DESCRIPTION = "Save images with civitai-compatible generation metadata" + def save_files( + self, + images: list[torch.Tensor], + filename: str, + path: str, + extension: str, + steps: int = 20, + cfg: float = 7.0, + modelname: str = "", + sampler_name: str = "", + scheduler_name: str = "normal", + positive: str = "unknown", + negative: str = "unknown", + seed_value: int = 0, + width: int = 512, + height: int = 512, + lossless_webp: bool = True, + quality_jpeg_or_webp: int = 100, + optimize_png: bool = False, + counter: int = 0, + denoise: float = 1.0, + clip_skip: int = 0, + time_format: str = "%Y-%m-%d-%H%M%S", + save_workflow_as_json: bool = False, + embed_workflow: bool = True, + additional_hashes: str = "", + download_civitai_data: bool = True, + easy_remix: bool = True, + prompt: dict[str, Any] | None = None, + extra_pnginfo: dict[str, Any] | None = None, + ) -> dict[str, Any]: + metadata = ImageSaverMetadata.make_metadata(modelname, positive, negative, width, height, seed_value, steps, cfg, sampler_name, scheduler_name, denoise, clip_skip, additional_hashes, download_civitai_data, easy_remix) + + filenames = ImageSaver.save_images(images, filename, extension, path, quality_jpeg_or_webp, lossless_webp, optimize_png, prompt, extra_pnginfo, save_workflow_as_json, embed_workflow, counter, time_format, metadata) + + subfolder = os.path.normpath(path) + + return { + "result": (metadata.final_hashes, metadata.a111_params), + "ui": {"images": map(lambda filename: {"filename": filename, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'}, filenames)}, + } + + @staticmethod + def save_images( + images: list[torch.Tensor], + filename_pattern: str, + extension: str, + path_pattern: str, + quality_jpeg_or_webp: int, + lossless_webp: bool, + optimize_png: bool, + prompt: dict[str, Any] | None, + extra_pnginfo: dict[str, Any] | None, + save_workflow_as_json: bool, + embed_workflow: bool, + counter: int, + time_format: str, + metadata: Metadata + ) -> list[str]: + filename_prefix = make_filename(filename_pattern, metadata.width, metadata.height, metadata.seed, metadata.modelname, counter, time_format, metadata.sampler_name, metadata.steps, metadata.cfg, metadata.scheduler_name, metadata.denoise, metadata.clip_skip) + path = make_pathname(path_pattern, metadata.width, metadata.height, metadata.seed, metadata.modelname, counter, time_format, metadata.sampler_name, metadata.steps, metadata.cfg, metadata.scheduler_name, metadata.denoise, metadata.clip_skip) + + output_path = os.path.join(folder_paths.output_directory, path) + + if output_path.strip() != '': + if not os.path.exists(output_path.strip()): + print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.') + os.makedirs(output_path, exist_ok=True) + + result_paths: list[str] = list() + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + current_filename_prefix = ImageSaver.get_unique_filename(output_path, filename_prefix, extension) + final_filename = f"{current_filename_prefix}.{extension}" + filepath = os.path.join(output_path, final_filename) + + save_image(img, filepath, extension, quality_jpeg_or_webp, lossless_webp, optimize_png, metadata.a111_params, prompt, extra_pnginfo, embed_workflow) + + if save_workflow_as_json: + save_json(extra_pnginfo, os.path.join(output_path, current_filename_prefix)) + + result_paths.append(final_filename) + return result_paths + # Match 'anything' or 'anything:anything' with trimmed white space re_manual_hash = re.compile(r'^\s*([^:]+?)(?:\s*:\s*([^\s:][^:]*?))?\s*$') # Match 'anything', 'anything:anything' or 'anything:anything:number' with trimmed white space @@ -240,117 +404,6 @@ class ImageSaver: additional_hashes += f"{additional_model}:{additional_modelhash}" return modelname, additional_hashes - def save_files( - self, - images: list[torch.Tensor], - filename: str, - path: str, - extension: str, - steps: int, - cfg: float, - modelname: str, - sampler_name: str, - scheduler: str, - positive: str, - negative: str, - seed_value: int, - width: int, - height: int, - lossless_webp: bool, - quality_jpeg_or_webp: int, - optimize_png: bool, - counter: int, - denoise: float, - clip_skip: int, - time_format: str, - save_workflow_as_json: bool = False, - embed_workflow: bool = True, - additional_hashes: str = "", - download_civitai_data: bool = True, - easy_remix: bool = True, - prompt: dict[str, Any] | None = None, - extra_pnginfo: dict[str, Any] | None = None, - ) -> dict[str, Any]: - modelname, additional_hashes = ImageSaver.get_multiple_models(modelname, additional_hashes) - filename = make_filename(filename, width, height, seed_value, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip) - path = make_pathname(path, width, height, seed_value, modelname, counter, time_format, sampler_name, steps, cfg, scheduler, denoise, clip_skip) - - ckpt_path = full_checkpoint_path_for(modelname) - if ckpt_path: - modelhash = get_sha256(ckpt_path)[:10] - else: - modelhash = "" - - metadata_extractor = PromptMetadataExtractor([positive, negative]) - embeddings = metadata_extractor.get_embeddings() - loras = metadata_extractor.get_loras() - civitai_sampler_name = get_civitai_sampler_name(sampler_name.replace('_gpu', ''), scheduler) - basemodelname = parse_checkpoint_name_without_extension(modelname) - - # Get existing hashes from model, loras, and embeddings - existing_hashes = {modelhash.lower()} | {t[2].lower() for t in loras.values()} | {t[2].lower() for t in embeddings.values()} - # Parse manual hashes - manual_entries = ImageSaver.parse_manual_hashes(additional_hashes, existing_hashes, download_civitai_data) - # Get Civitai metadata - civitai_resources, hashes, add_model_hash = get_civitai_metadata(modelname, ckpt_path, modelhash, loras, embeddings, manual_entries, download_civitai_data) - - if easy_remix: - positive = ImageSaver.clean_prompt(positive, metadata_extractor) - negative = ImageSaver.clean_prompt(negative, metadata_extractor) - - positive_a111_params = positive.strip() - negative_a111_params = f"\nNegative prompt: {negative.strip()}" - clip_skip_str = f", Clip skip: {abs(clip_skip)}" if clip_skip != 0 else "" - model_hash_str = f", Model hash: {add_model_hash}" if add_model_hash else "" - hashes_str = f", Hashes: {json.dumps(hashes, separators=(',', ':'))}" if hashes else "" - - a111_params = ( - f"{positive_a111_params}{negative_a111_params}\n" - f"Steps: {steps}, Sampler: {civitai_sampler_name}, CFG scale: {cfg}, Seed: {seed_value}, " - f"Size: {width}x{height}{clip_skip_str}{model_hash_str}, Model: {basemodelname}{hashes_str}, Version: ComfyUI" - ) - - # Add Civitai resource listing - if download_civitai_data and civitai_resources: - a111_params += f", Civitai resources: {json.dumps(civitai_resources, separators=(',', ':'))}" - - output_path = os.path.join(folder_paths.output_directory, path) - - if output_path.strip() != '': - if not os.path.exists(output_path.strip()): - print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.') - os.makedirs(output_path, exist_ok=True) - - filenames = ImageSaver.save_images(images, output_path, filename, a111_params, extension, quality_jpeg_or_webp, lossless_webp, optimize_png, prompt, extra_pnginfo, save_workflow_as_json, embed_workflow) - - subfolder = os.path.normpath(path) - - final_hashes = ",".join(f"{Path(name.split(':')[-1]).stem + ':' if name else ''}{hash}{':' + str(weight) if weight is not None and download_civitai_data else ''}" for name, (_, weight, hash) in ({ modelname: ( ckpt_path, None, modelhash ) } | loras | embeddings | manual_entries).items()) - - return { - "result": (final_hashes, a111_params), - "ui": {"images": map(lambda filename: {"filename": filename, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'}, filenames)}, - } - - @staticmethod - def save_images(images: list[torch.Tensor], output_path: str, filename_prefix: str, a111_params: str, extension: str, quality_jpeg_or_webp: int, lossless_webp: bool, optimize_png: bool, prompt: dict[str, Any] | None, extra_pnginfo: dict[str, Any] | None, save_workflow_as_json: bool, embed_workflow: bool) -> list[str]: - paths: list[str] = list() - for image in images: - i = 255. * image.cpu().numpy() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - - current_filename_prefix = ImageSaver.get_unique_filename(output_path, filename_prefix, extension) - filename = f"{current_filename_prefix}.{extension}" - filepath = os.path.join(output_path, filename) - - save_image(img, filepath, extension, quality_jpeg_or_webp, lossless_webp, optimize_png, a111_params, prompt, extra_pnginfo, embed_workflow) - - if save_workflow_as_json: - save_json(extra_pnginfo, os.path.join(output_path, current_filename_prefix)) - - paths.append(filename) - return paths - @staticmethod def parse_manual_hashes(additional_hashes: str, existing_hashes: set[str], download_civitai_data: bool) -> dict[str, tuple[str | None, float | None, str]]: """Process additional_hashes input (a string) by normalizing, removing extra spaces/newlines, and splitting by comma""" @@ -423,7 +476,7 @@ class ImageSaver: if not existing_files: return f"{filename_prefix}" - suffixes = [] + suffixes: list[int] = [] for f in existing_files: name, _ = os.path.splitext(f) parts = name.split('_') diff --git a/nodes_literals.py b/nodes_literals.py index e7777f2..7b38208 100644 --- a/nodes_literals.py +++ b/nodes_literals.py @@ -111,7 +111,7 @@ class CfgLiteral: def INPUT_TYPES(cls): return { "required": { - "float": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "tooltip": "CFG as a floating point number"}), + "float": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "tooltip": "CFG as a floating point number"}), } } diff --git a/nodes_selectors.py b/nodes_selectors.py index 4bc3739..311bcff 100644 --- a/nodes_selectors.py +++ b/nodes_selectors.py @@ -144,7 +144,7 @@ class InputParameters: "required": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "The number of steps used in the denoising process."}), - "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality."}), + "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality."}), "sampler": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The algorithm used when sampling, this can affect the quality, speed, and style of the generated output."}), "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"tooltip": "The scheduler controls how noise is gradually removed to form the image."}), "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The amount of denoising applied, lower values will maintain the structure of the initial image allowing for image to image sampling."}), diff --git a/prompt_metadata_extractor.py b/prompt_metadata_extractor.py index c3627fa..f411873 100644 --- a/prompt_metadata_extractor.py +++ b/prompt_metadata_extractor.py @@ -1,5 +1,5 @@ import re -from typing import List, Dict, Tuple, Optional +from typing import List, Dict, Tuple from comfy.sd1_clip import escape_important, unescape_important, token_weights from .utils import full_embedding_path_for, full_lora_path_for, get_sha256 diff --git a/saver/saver.py b/saver/saver.py index 10cc1d2..0142556 100644 --- a/saver/saver.py +++ b/saver/saver.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, cast from PIL.PngImagePlugin import PngInfo from PIL.Image import Image @@ -9,7 +9,8 @@ import piexif.helper def save_image(image: Image, filepath: str, extension: str, quality_jpeg_or_webp: int, lossless_webp: bool, optimize_png: bool, a111_params: str, prompt: dict[str, Any] | None, extra_pnginfo: dict[str, Any] | None, embed_workflow: bool) -> None: if extension == 'png': metadata = PngInfo() - metadata.add_text("parameters", a111_params) + if a111_params: + metadata.add_text("parameters", a111_params) if embed_workflow: if extra_pnginfo is not None: @@ -33,13 +34,14 @@ def save_image(image: Image, filepath: str, extension: str, quality_jpeg_or_webp prompt_json = {piexif.ImageIFD.Model: f"prompt:{json.dumps(prompt, separators=(',', ':'))}"} def get_exif_bytes() -> bytes: - return piexif.dump(({ + exif_dict = ({ "0th": pnginfo_json | prompt_json - } if pnginfo_json or prompt_json else {}) | { + } if pnginfo_json or prompt_json else {}) | ({ "Exif": { - piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(a111_params, encoding="unicode") + piexif.ExifIFD.UserComment: cast(bytes, piexif.helper.UserComment.dump(a111_params, encoding="unicode")) }, - }) + } if a111_params else {}) + return cast(bytes, piexif.dump(exif_dict)) exif_bytes = get_exif_bytes() diff --git a/utils.py b/utils.py index 86fd277..67923fd 100644 --- a/utils.py +++ b/utils.py @@ -71,7 +71,10 @@ def __list_loras() -> list[str]: def __list_embeddings() -> list[str]: return folder_paths.get_filename_list("embeddings") -def full_checkpoint_path_for(model_name: str) -> Optional[str]: +def full_checkpoint_path_for(model_name: str) -> str: + if not model_name: + return '' + last_dot_position = model_name.rfind('.') extension = model_name[last_dot_position:] if last_dot_position != -1 else "" if extension not in folder_paths.supported_pt_extensions: @@ -86,7 +89,7 @@ def full_checkpoint_path_for(model_name: str) -> Optional[str]: return folder_paths.get_full_path("diffusion_models", matching_model) print(f'Could not find full path to checkpoint "{model_name}"') - return None + return '' def __list_checkpoints() -> list[str]: return folder_paths.get_filename_list("checkpoints") diff --git a/utils_civitai.py b/utils_civitai.py index 933d4c4..0dfb8b3 100644 --- a/utils_civitai.py +++ b/utils_civitai.py @@ -61,7 +61,7 @@ def get_civitai_sampler_name(sampler_name: str, scheduler: str) -> str: def get_civitai_metadata( modelname: str, - ckpt_path: str | None, + ckpt_path: str, modelhash: str, loras: Dict[str, Tuple[str, float, str]], embeddings: Dict[str, Tuple[str, float, str]], @@ -73,7 +73,7 @@ def get_civitai_metadata( add_model_hash = None if download_civitai_data: - for name, (filepath, weight, hash) in ({ modelname: ( ckpt_path if ckpt_path is not None else '', None, modelhash ) } | loras | embeddings | manual_entries).items(): + for name, (filepath, weight, hash) in ({ modelname: ( ckpt_path, None, modelhash ) } | loras | embeddings | manual_entries).items(): civitai_info = get_civitai_info(filepath, hash) if civitai_info is not None: resource_data: Dict[str, str | float] = {}