├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── example-workflow.json ├── nodes.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Girish Gopaul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Save image with generation metadata on ComfyUI 2 | 3 | All the tools you need to save images with their **generation metadata** on ComfyUI. Compatible with *Civitai* & *Prompthero* geninfo auto-detection. Works with `png`, `jpeg` and `webp`. 4 | 5 | You can find the example workflow file named `example-workflow.json`. 6 | 7 | ![example-workflow](https://github.com/giriss/comfy-image-saver/assets/2811408/e231237b-f91a-4679-b3ae-2618080c8e39) 8 | 9 | ## How to install? 10 | 11 | ### Method 1: Easiest (Recommended) 12 | If you have *ComfyUI-Manager*, you can simply search "**Save Image with Generation Metadata**" and install these custom nodes 🎉 13 | 14 | 15 | ### Method 2: Easy 16 | If you don't have *ComfyUI-Manager*, then: 17 | - Using CLI, go to the ComfyUI folder 18 | - `cd custom_nodes` 19 | - `git clone git@github.com:giriss/comfy-image-saver.git` 20 | - `cd comfy-image-saver` 21 | - `pip install -r requirements.txt` 22 | - Start/restart ComfyUI 🎉 23 | 24 | ## Autodetection in action 25 | 26 | ![Screenshot 2023-08-17 at 13 15 18](https://github.com/giriss/comfy-image-saver/assets/2811408/785f2475-8f9a-45c9-9d38-855161a98495) 27 | 28 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes import NODE_CLASS_MAPPINGS 2 | 3 | __all__ = ['NODE_CLASS_MAPPINGS'] 4 | -------------------------------------------------------------------------------- /example-workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 40, 3 | "last_link_id": 48, 4 | "nodes": [ 5 | { 6 | "id": 28, 7 | "type": "Sampler Selector", 8 | "pos": [ 9 | 548, 10 | -10 11 | ], 12 | "size": { 13 | "0": 263.46875, 14 | "1": 58 15 | }, 16 | "flags": { 17 | "collapsed": true 18 | }, 19 | "order": 0, 20 | "mode": 0, 21 | "outputs": [ 22 | { 23 | "name": "sampler_name", 24 | "type": [ 25 | "euler", 26 | "euler_ancestral", 27 | "heun", 28 | "dpm_2", 29 | "dpm_2_ancestral", 30 | "lms", 31 | "dpm_fast", 32 | "dpm_adaptive", 33 | "dpmpp_2s_ancestral", 34 | "dpmpp_sde", 35 | "dpmpp_sde_gpu", 36 | "dpmpp_2m", 37 | "dpmpp_2m_sde", 38 | "dpmpp_2m_sde_gpu", 39 | "dpmpp_3m_sde", 40 | "dpmpp_3m_sde_gpu", 41 | "ddim", 42 | "uni_pc", 43 | "uni_pc_bh2" 44 | ], 45 | "links": [ 46 | 20, 47 | 41 48 | ], 49 | "shape": 3 50 | } 51 | ], 52 | "properties": { 53 | "Node name for S&R": "Sampler Selector" 54 | }, 55 | "widgets_values": [ 56 | "euler_ancestral" 57 | ] 58 | }, 59 | { 60 | "id": 29, 61 | "type": "Int Literal", 62 | "pos": [ 63 | 608, 64 | 33 65 | ], 66 | "size": { 67 | "0": 210, 68 | "1": 58 69 | }, 70 | "flags": { 71 | "collapsed": true 72 | }, 73 | "order": 1, 74 | "mode": 0, 75 | "outputs": [ 76 | { 77 | "name": "INT", 78 | "type": "INT", 79 | "links": [ 80 | 21, 81 | 38 82 | ], 83 | "shape": 3 84 | } 85 | ], 86 | "title": "Steps", 87 | "properties": { 88 | "Node name for S&R": "Int Literal" 89 | }, 90 | "widgets_values": [ 91 | 20 92 | ] 93 | }, 94 | { 95 | "id": 30, 96 | "type": "Cfg Literal", 97 | "pos": [ 98 | 586, 99 | 73 100 | ], 101 | "size": { 102 | "0": 210, 103 | "1": 58 104 | }, 105 | "flags": { 106 | "collapsed": true 107 | }, 108 | "order": 2, 109 | "mode": 0, 110 | "outputs": [ 111 | { 112 | "name": "FLOAT", 113 | "type": "FLOAT", 114 | "links": [ 115 | 22, 116 | 39 117 | ], 118 | "shape": 3 119 | } 120 | ], 121 | "properties": { 122 | "Node name for S&R": "Cfg Literal" 123 | }, 124 | "widgets_values": [ 125 | 7 126 | ] 127 | }, 128 | { 129 | "id": 3, 130 | "type": "KSampler", 131 | "pos": [ 132 | 863, 133 | 186 134 | ], 135 | "size": { 136 | "0": 315, 137 | "1": 262 138 | }, 139 | "flags": {}, 140 | "order": 14, 141 | "mode": 0, 142 | "inputs": [ 143 | { 144 | "name": "model", 145 | "type": "MODEL", 146 | "link": 1 147 | }, 148 | { 149 | "name": "positive", 150 | "type": "CONDITIONING", 151 | "link": 4 152 | }, 153 | { 154 | "name": "negative", 155 | "type": "CONDITIONING", 156 | "link": 6 157 | }, 158 | { 159 | "name": "latent_image", 160 | "type": "LATENT", 161 | "link": 2 162 | }, 163 | { 164 | "name": "seed", 165 | "type": "INT", 166 | "link": 15, 167 | "widget": { 168 | "name": "seed", 169 | "config": [ 170 | "INT", 171 | { 172 | "default": 0, 173 | "min": 0, 174 | "max": 18446744073709552000 175 | } 176 | ] 177 | } 178 | }, 179 | { 180 | "name": "sampler_name", 181 | "type": "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,dpmpp_3m_sde,dpmpp_3m_sde_gpu,ddim,uni_pc,uni_pc_bh2", 182 | "link": 20, 183 | "widget": { 184 | "name": "sampler_name", 185 | "config": [ 186 | [ 187 | "euler", 188 | "euler_ancestral", 189 | "heun", 190 | "dpm_2", 191 | "dpm_2_ancestral", 192 | "lms", 193 | "dpm_fast", 194 | "dpm_adaptive", 195 | "dpmpp_2s_ancestral", 196 | "dpmpp_sde", 197 | "dpmpp_sde_gpu", 198 | "dpmpp_2m", 199 | "dpmpp_2m_sde", 200 | "dpmpp_2m_sde_gpu", 201 | "dpmpp_3m_sde", 202 | "dpmpp_3m_sde_gpu", 203 | "ddim", 204 | "uni_pc", 205 | "uni_pc_bh2" 206 | ] 207 | ] 208 | }, 209 | "slot_index": 5 210 | }, 211 | { 212 | "name": "steps", 213 | "type": "INT", 214 | "link": 21, 215 | "widget": { 216 | "name": "steps", 217 | "config": [ 218 | "INT", 219 | { 220 | "default": 20, 221 | "min": 1, 222 | "max": 10000 223 | } 224 | ] 225 | }, 226 | "slot_index": 6 227 | }, 228 | { 229 | "name": "cfg", 230 | "type": "FLOAT", 231 | "link": 22, 232 | "widget": { 233 | "name": "cfg", 234 | "config": [ 235 | "FLOAT", 236 | { 237 | "default": 8, 238 | "min": 0, 239 | "max": 100 240 | } 241 | ] 242 | }, 243 | "slot_index": 7 244 | }, 245 | { 246 | "name": "scheduler", 247 | "type": "normal,karras,exponential,sgm_uniform,simple,ddim_uniform", 248 | "link": 23, 249 | "widget": { 250 | "name": "scheduler", 251 | "config": [ 252 | [ 253 | "normal", 254 | "karras", 255 | "exponential", 256 | "sgm_uniform", 257 | "simple", 258 | "ddim_uniform" 259 | ] 260 | ] 261 | }, 262 | "slot_index": 8 263 | } 264 | ], 265 | "outputs": [ 266 | { 267 | "name": "LATENT", 268 | "type": "LATENT", 269 | "links": [ 270 | 7 271 | ], 272 | "slot_index": 0 273 | } 274 | ], 275 | "properties": { 276 | "Node name for S&R": "KSampler" 277 | }, 278 | "widgets_values": [ 279 | 72524088949694, 280 | "randomize", 281 | 20, 282 | 7, 283 | "dpmpp_sde", 284 | "karras", 285 | 1 286 | ] 287 | }, 288 | { 289 | "id": 31, 290 | "type": "Scheduler Selector", 291 | "pos": [ 292 | 539, 293 | 113 294 | ], 295 | "size": { 296 | "0": 210, 297 | "1": 58 298 | }, 299 | "flags": { 300 | "collapsed": true 301 | }, 302 | "order": 3, 303 | "mode": 0, 304 | "outputs": [ 305 | { 306 | "name": "scheduler", 307 | "type": [ 308 | "normal", 309 | "karras", 310 | "exponential", 311 | "sgm_uniform", 312 | "simple", 313 | "ddim_uniform" 314 | ], 315 | "links": [ 316 | 23, 317 | 42 318 | ], 319 | "shape": 3 320 | } 321 | ], 322 | "properties": { 323 | "Node name for S&R": "Scheduler Selector" 324 | }, 325 | "widgets_values": [ 326 | "normal" 327 | ] 328 | }, 329 | { 330 | "id": 7, 331 | "type": "CLIPTextEncode", 332 | "pos": [ 333 | 580, 334 | 345 335 | ], 336 | "size": { 337 | "0": 425.27801513671875, 338 | "1": 180.6060791015625 339 | }, 340 | "flags": { 341 | "collapsed": true 342 | }, 343 | "order": 13, 344 | "mode": 0, 345 | "inputs": [ 346 | { 347 | "name": "clip", 348 | "type": "CLIP", 349 | "link": 5 350 | }, 351 | { 352 | "name": "text", 353 | "type": "STRING", 354 | "link": 17, 355 | "widget": { 356 | "name": "text", 357 | "config": [ 358 | "STRING", 359 | { 360 | "multiline": true 361 | } 362 | ] 363 | }, 364 | "slot_index": 1 365 | } 366 | ], 367 | "outputs": [ 368 | { 369 | "name": "CONDITIONING", 370 | "type": "CONDITIONING", 371 | "links": [ 372 | 6 373 | ], 374 | "slot_index": 0 375 | } 376 | ], 377 | "properties": { 378 | "Node name for S&R": "CLIPTextEncode" 379 | }, 380 | "widgets_values": [ 381 | "lowres, text, error, cropped, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, out of frame, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, blurry, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck, username, watermark, signature, multiple people, 2 or more people, more than 1 person" 382 | ] 383 | }, 384 | { 385 | "id": 6, 386 | "type": "CLIPTextEncode", 387 | "pos": [ 388 | 583, 389 | 293 390 | ], 391 | "size": { 392 | "0": 422.84503173828125, 393 | "1": 164.31304931640625 394 | }, 395 | "flags": { 396 | "collapsed": true 397 | }, 398 | "order": 12, 399 | "mode": 0, 400 | "inputs": [ 401 | { 402 | "name": "clip", 403 | "type": "CLIP", 404 | "link": 3 405 | }, 406 | { 407 | "name": "text", 408 | "type": "STRING", 409 | "link": 16, 410 | "widget": { 411 | "name": "text", 412 | "config": [ 413 | "STRING", 414 | { 415 | "multiline": true 416 | } 417 | ] 418 | }, 419 | "slot_index": 1 420 | } 421 | ], 422 | "outputs": [ 423 | { 424 | "name": "CONDITIONING", 425 | "type": "CONDITIONING", 426 | "links": [ 427 | 4 428 | ], 429 | "slot_index": 0 430 | } 431 | ], 432 | "properties": { 433 | "Node name for S&R": "CLIPTextEncode" 434 | }, 435 | "widgets_values": [ 436 | "Sexy girl wandering alone, Lost in thought, scenic advanced alien mega city, seeking solace, wearing sexy deep pink bra, Finding peace within | centered | stunning visual | intricate | highly detailed| breathtaking beauty| precise lineart| vibrant| comprehensive cinematic| anna dittman, full perfect body, dynamic pose, best quality, 8k, clean focus, carne griffths, beautiful lighting, 1 person, close up portrait, hyperrealistic, hyperrealism, full body view, necklace, daylight" 437 | ] 438 | }, 439 | { 440 | "id": 17, 441 | "type": "Seed Generator", 442 | "pos": [ 443 | 551, 444 | -54 445 | ], 446 | "size": { 447 | "0": 275.2265625, 448 | "1": 82 449 | }, 450 | "flags": { 451 | "collapsed": true 452 | }, 453 | "order": 4, 454 | "mode": 0, 455 | "outputs": [ 456 | { 457 | "name": "INT", 458 | "type": "INT", 459 | "links": [ 460 | 15, 461 | 43 462 | ], 463 | "shape": 3, 464 | "slot_index": 0 465 | } 466 | ], 467 | "properties": { 468 | "Node name for S&R": "Seed Generator" 469 | }, 470 | "widgets_values": [ 471 | 479566252427468, 472 | "randomize" 473 | ] 474 | }, 475 | { 476 | "id": 4, 477 | "type": "CheckpointLoaderSimple", 478 | "pos": [ 479 | 164, 480 | 478 481 | ], 482 | "size": { 483 | "0": 315, 484 | "1": 98 485 | }, 486 | "flags": { 487 | "collapsed": true 488 | }, 489 | "order": 11, 490 | "mode": 0, 491 | "inputs": [ 492 | { 493 | "name": "ckpt_name", 494 | "type": "epicrealism_pureEvolutionV5.safetensors", 495 | "link": 28, 496 | "widget": { 497 | "name": "ckpt_name", 498 | "config": [ 499 | [ 500 | "epicrealism_pureEvolutionV5.safetensors" 501 | ] 502 | ] 503 | } 504 | } 505 | ], 506 | "outputs": [ 507 | { 508 | "name": "MODEL", 509 | "type": "MODEL", 510 | "links": [ 511 | 1 512 | ], 513 | "slot_index": 0 514 | }, 515 | { 516 | "name": "CLIP", 517 | "type": "CLIP", 518 | "links": [ 519 | 3, 520 | 5 521 | ], 522 | "slot_index": 1 523 | }, 524 | { 525 | "name": "VAE", 526 | "type": "VAE", 527 | "links": [ 528 | 8 529 | ], 530 | "slot_index": 2 531 | } 532 | ], 533 | "properties": { 534 | "Node name for S&R": "CheckpointLoaderSimple" 535 | }, 536 | "widgets_values": [ 537 | "epicrealism_pureEvolutionV5.safetensors" 538 | ] 539 | }, 540 | { 541 | "id": 22, 542 | "type": "String Literal", 543 | "pos": [ 544 | 79, 545 | 295 546 | ], 547 | "size": { 548 | "0": 400.109375, 549 | "1": 108.55078125 550 | }, 551 | "flags": {}, 552 | "order": 5, 553 | "mode": 0, 554 | "outputs": [ 555 | { 556 | "name": "STRING", 557 | "type": "STRING", 558 | "links": [ 559 | 17, 560 | 45 561 | ], 562 | "shape": 3 563 | } 564 | ], 565 | "title": "Negative", 566 | "properties": { 567 | "Node name for S&R": "String Literal" 568 | }, 569 | "widgets_values": [ 570 | "(worst quality, low quality, illustration, 3d, 2d), open mouth, tooth,ugly face, old face, abnormal hands, watermark, abnormal fingers, extra limbs, ugly eyes, ugly face," 571 | ], 572 | "color": "#233", 573 | "bgcolor": "#355" 574 | }, 575 | { 576 | "id": 19, 577 | "type": "String Literal", 578 | "pos": [ 579 | 79, 580 | 53 581 | ], 582 | "size": { 583 | "0": 400, 584 | "1": 200 585 | }, 586 | "flags": {}, 587 | "order": 6, 588 | "mode": 0, 589 | "outputs": [ 590 | { 591 | "name": "STRING", 592 | "type": "STRING", 593 | "links": [ 594 | 16, 595 | 44 596 | ], 597 | "shape": 3 598 | } 599 | ], 600 | "title": "Positive", 601 | "properties": { 602 | "Node name for S&R": "String Literal" 603 | }, 604 | "widgets_values": [ 605 | "masterpiece,best quality, renaissance style girl,dark Silver Long waves hair,Bare shoulder,beautiful Bollywood actresse, light skin,DayGlo green translucent saree sari, perty, beauty face, . realistic, perspective, light and shadow, religious or mythological themes, highly detailed, a beautiful painting of the pinnacles, domes and towers of the ancient mayan jungle city, with the night sky with stars above, intricate, elegant, highly detailed, digital painting, artstation, concept art, by krenz cushart and artem demura and alphonse mucha, (colorful) by james jean and by artgerm, by ross tran, ultradetailed, charachter design, concept art, trending on artstation" 606 | ], 607 | "color": "#233", 608 | "bgcolor": "#355" 609 | }, 610 | { 611 | "id": 5, 612 | "type": "EmptyLatentImage", 613 | "pos": [ 614 | 650, 615 | 446 616 | ], 617 | "size": { 618 | "0": 210, 619 | "1": 78 620 | }, 621 | "flags": { 622 | "collapsed": true 623 | }, 624 | "order": 10, 625 | "mode": 0, 626 | "inputs": [ 627 | { 628 | "name": "width", 629 | "type": "INT", 630 | "link": 18, 631 | "widget": { 632 | "name": "width", 633 | "config": [ 634 | "INT", 635 | { 636 | "default": 512, 637 | "min": 64, 638 | "max": 8192, 639 | "step": 8 640 | } 641 | ] 642 | }, 643 | "slot_index": 0 644 | }, 645 | { 646 | "name": "height", 647 | "type": "INT", 648 | "link": 19, 649 | "widget": { 650 | "name": "height", 651 | "config": [ 652 | "INT", 653 | { 654 | "default": 512, 655 | "min": 64, 656 | "max": 8192, 657 | "step": 8 658 | } 659 | ] 660 | }, 661 | "slot_index": 1 662 | } 663 | ], 664 | "outputs": [ 665 | { 666 | "name": "LATENT", 667 | "type": "LATENT", 668 | "links": [ 669 | 2 670 | ], 671 | "slot_index": 0 672 | } 673 | ], 674 | "properties": { 675 | "Node name for S&R": "EmptyLatentImage" 676 | }, 677 | "widgets_values": [ 678 | 512, 679 | 768, 680 | 1 681 | ] 682 | }, 683 | { 684 | "id": 27, 685 | "type": "Width/Height Literal", 686 | "pos": [ 687 | 506, 688 | 406 689 | ], 690 | "size": { 691 | "0": 315, 692 | "1": 58 693 | }, 694 | "flags": { 695 | "collapsed": true 696 | }, 697 | "order": 7, 698 | "mode": 0, 699 | "outputs": [ 700 | { 701 | "name": "INT", 702 | "type": "INT", 703 | "links": [ 704 | 18, 705 | 46 706 | ], 707 | "shape": 3 708 | } 709 | ], 710 | "title": "Width", 711 | "properties": { 712 | "Node name for S&R": "Width/Height Literal" 713 | }, 714 | "widgets_values": [ 715 | 768 716 | ] 717 | }, 718 | { 719 | "id": 26, 720 | "type": "Width/Height Literal", 721 | "pos": [ 722 | 506, 723 | 488 724 | ], 725 | "size": { 726 | "0": 315, 727 | "1": 58 728 | }, 729 | "flags": { 730 | "collapsed": true 731 | }, 732 | "order": 8, 733 | "mode": 0, 734 | "outputs": [ 735 | { 736 | "name": "INT", 737 | "type": "INT", 738 | "links": [ 739 | 19, 740 | 47 741 | ], 742 | "shape": 3 743 | } 744 | ], 745 | "title": "Height", 746 | "properties": { 747 | "Node name for S&R": "Width/Height Literal" 748 | }, 749 | "widgets_values": [ 750 | 1152 751 | ] 752 | }, 753 | { 754 | "id": 35, 755 | "type": "Checkpoint Selector", 756 | "pos": [ 757 | 499, 758 | -155 759 | ], 760 | "size": [ 761 | 382.49933725757705, 762 | 58 763 | ], 764 | "flags": {}, 765 | "order": 9, 766 | "mode": 0, 767 | "outputs": [ 768 | { 769 | "name": "ckpt_name", 770 | "type": [ 771 | "epicrealism_pureEvolutionV5.safetensors" 772 | ], 773 | "links": [ 774 | 28, 775 | 40 776 | ], 777 | "shape": 3, 778 | "slot_index": 0 779 | } 780 | ], 781 | "properties": { 782 | "Node name for S&R": "Checkpoint Selector" 783 | }, 784 | "widgets_values": [ 785 | "epicrealism_pureEvolutionV5.safetensors" 786 | ] 787 | }, 788 | { 789 | "id": 39, 790 | "type": "Save Image w/Metadata", 791 | "pos": [ 792 | 1222, 793 | -154 794 | ], 795 | "size": [ 796 | 349.01775559535145, 797 | 646.6896077006018 798 | ], 799 | "flags": {}, 800 | "order": 16, 801 | "mode": 0, 802 | "inputs": [ 803 | { 804 | "name": "images", 805 | "type": "IMAGE", 806 | "link": 48 807 | }, 808 | { 809 | "name": "steps", 810 | "type": "INT", 811 | "link": 38, 812 | "widget": { 813 | "name": "steps", 814 | "config": [ 815 | "INT", 816 | { 817 | "default": 20, 818 | "min": 1, 819 | "max": 10000 820 | } 821 | ] 822 | }, 823 | "slot_index": 1 824 | }, 825 | { 826 | "name": "cfg", 827 | "type": "FLOAT", 828 | "link": 39, 829 | "widget": { 830 | "name": "cfg", 831 | "config": [ 832 | "FLOAT", 833 | { 834 | "default": 8, 835 | "min": 0, 836 | "max": 100 837 | } 838 | ] 839 | }, 840 | "slot_index": 2 841 | }, 842 | { 843 | "name": "modelname", 844 | "type": "epicrealism_pureEvolutionV5.safetensors", 845 | "link": 40, 846 | "widget": { 847 | "name": "modelname", 848 | "config": [ 849 | [ 850 | "epicrealism_pureEvolutionV5.safetensors" 851 | ] 852 | ] 853 | }, 854 | "slot_index": 3 855 | }, 856 | { 857 | "name": "sampler_name", 858 | "type": "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,dpmpp_3m_sde,dpmpp_3m_sde_gpu,ddim,uni_pc,uni_pc_bh2", 859 | "link": 41, 860 | "widget": { 861 | "name": "sampler_name", 862 | "config": [ 863 | [ 864 | "euler", 865 | "euler_ancestral", 866 | "heun", 867 | "dpm_2", 868 | "dpm_2_ancestral", 869 | "lms", 870 | "dpm_fast", 871 | "dpm_adaptive", 872 | "dpmpp_2s_ancestral", 873 | "dpmpp_sde", 874 | "dpmpp_sde_gpu", 875 | "dpmpp_2m", 876 | "dpmpp_2m_sde", 877 | "dpmpp_2m_sde_gpu", 878 | "dpmpp_3m_sde", 879 | "dpmpp_3m_sde_gpu", 880 | "ddim", 881 | "uni_pc", 882 | "uni_pc_bh2" 883 | ] 884 | ] 885 | }, 886 | "slot_index": 4 887 | }, 888 | { 889 | "name": "scheduler", 890 | "type": "normal,karras,exponential,sgm_uniform,simple,ddim_uniform", 891 | "link": 42, 892 | "widget": { 893 | "name": "scheduler", 894 | "config": [ 895 | [ 896 | "normal", 897 | "karras", 898 | "exponential", 899 | "sgm_uniform", 900 | "simple", 901 | "ddim_uniform" 902 | ] 903 | ] 904 | }, 905 | "slot_index": 5 906 | }, 907 | { 908 | "name": "positive", 909 | "type": "STRING", 910 | "link": 44, 911 | "widget": { 912 | "name": "positive", 913 | "config": [ 914 | "STRING", 915 | { 916 | "default": "unknown", 917 | "multiline": true 918 | } 919 | ] 920 | }, 921 | "slot_index": 6 922 | }, 923 | { 924 | "name": "negative", 925 | "type": "STRING", 926 | "link": 45, 927 | "widget": { 928 | "name": "negative", 929 | "config": [ 930 | "STRING", 931 | { 932 | "default": "unknown", 933 | "multiline": true 934 | } 935 | ] 936 | }, 937 | "slot_index": 7 938 | }, 939 | { 940 | "name": "seed_value", 941 | "type": "INT", 942 | "link": 43, 943 | "widget": { 944 | "name": "seed_value", 945 | "config": [ 946 | "INT", 947 | { 948 | "default": 0, 949 | "min": 0, 950 | "max": 18446744073709552000 951 | } 952 | ] 953 | }, 954 | "slot_index": 8 955 | }, 956 | { 957 | "name": "width", 958 | "type": "INT", 959 | "link": 46, 960 | "widget": { 961 | "name": "width", 962 | "config": [ 963 | "INT", 964 | { 965 | "default": 512, 966 | "min": 1, 967 | "max": 8192, 968 | "step": 8 969 | } 970 | ] 971 | }, 972 | "slot_index": 9 973 | }, 974 | { 975 | "name": "height", 976 | "type": "INT", 977 | "link": 47, 978 | "widget": { 979 | "name": "height", 980 | "config": [ 981 | "INT", 982 | { 983 | "default": 512, 984 | "min": 1, 985 | "max": 8192, 986 | "step": 8 987 | } 988 | ] 989 | }, 990 | "slot_index": 10 991 | } 992 | ], 993 | "properties": { 994 | "Node name for S&R": "Save Image w/Metadata" 995 | }, 996 | "widgets_values": [ 997 | "%time_%seed", 998 | "", 999 | "jpeg", 1000 | 20, 1001 | 8, 1002 | "epicrealism_pureEvolutionV5.safetensors", 1003 | "euler", 1004 | "normal", 1005 | "unknown", 1006 | "unknown", 1007 | 0, 1008 | 512, 1009 | 512, 1010 | true, 1011 | 100, 1012 | 0, 1013 | "%Y-%m-%d-%H%M%S" 1014 | ] 1015 | }, 1016 | { 1017 | "id": 8, 1018 | "type": "VAEDecode", 1019 | "pos": [ 1020 | 971, 1021 | -153 1022 | ], 1023 | "size": { 1024 | "0": 210, 1025 | "1": 46 1026 | }, 1027 | "flags": {}, 1028 | "order": 15, 1029 | "mode": 0, 1030 | "inputs": [ 1031 | { 1032 | "name": "samples", 1033 | "type": "LATENT", 1034 | "link": 7 1035 | }, 1036 | { 1037 | "name": "vae", 1038 | "type": "VAE", 1039 | "link": 8 1040 | } 1041 | ], 1042 | "outputs": [ 1043 | { 1044 | "name": "IMAGE", 1045 | "type": "IMAGE", 1046 | "links": [ 1047 | 48 1048 | ], 1049 | "slot_index": 0 1050 | } 1051 | ], 1052 | "properties": { 1053 | "Node name for S&R": "VAEDecode" 1054 | } 1055 | } 1056 | ], 1057 | "links": [ 1058 | [ 1059 | 1, 1060 | 4, 1061 | 0, 1062 | 3, 1063 | 0, 1064 | "MODEL" 1065 | ], 1066 | [ 1067 | 2, 1068 | 5, 1069 | 0, 1070 | 3, 1071 | 3, 1072 | "LATENT" 1073 | ], 1074 | [ 1075 | 3, 1076 | 4, 1077 | 1, 1078 | 6, 1079 | 0, 1080 | "CLIP" 1081 | ], 1082 | [ 1083 | 4, 1084 | 6, 1085 | 0, 1086 | 3, 1087 | 1, 1088 | "CONDITIONING" 1089 | ], 1090 | [ 1091 | 5, 1092 | 4, 1093 | 1, 1094 | 7, 1095 | 0, 1096 | "CLIP" 1097 | ], 1098 | [ 1099 | 6, 1100 | 7, 1101 | 0, 1102 | 3, 1103 | 2, 1104 | "CONDITIONING" 1105 | ], 1106 | [ 1107 | 7, 1108 | 3, 1109 | 0, 1110 | 8, 1111 | 0, 1112 | "LATENT" 1113 | ], 1114 | [ 1115 | 8, 1116 | 4, 1117 | 2, 1118 | 8, 1119 | 1, 1120 | "VAE" 1121 | ], 1122 | [ 1123 | 15, 1124 | 17, 1125 | 0, 1126 | 3, 1127 | 4, 1128 | "INT" 1129 | ], 1130 | [ 1131 | 16, 1132 | 19, 1133 | 0, 1134 | 6, 1135 | 1, 1136 | "STRING" 1137 | ], 1138 | [ 1139 | 17, 1140 | 22, 1141 | 0, 1142 | 7, 1143 | 1, 1144 | "STRING" 1145 | ], 1146 | [ 1147 | 18, 1148 | 27, 1149 | 0, 1150 | 5, 1151 | 0, 1152 | "INT" 1153 | ], 1154 | [ 1155 | 19, 1156 | 26, 1157 | 0, 1158 | 5, 1159 | 1, 1160 | "INT" 1161 | ], 1162 | [ 1163 | 20, 1164 | 28, 1165 | 0, 1166 | 3, 1167 | 5, 1168 | "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,dpmpp_3m_sde,dpmpp_3m_sde_gpu,ddim,uni_pc,uni_pc_bh2" 1169 | ], 1170 | [ 1171 | 21, 1172 | 29, 1173 | 0, 1174 | 3, 1175 | 6, 1176 | "INT" 1177 | ], 1178 | [ 1179 | 22, 1180 | 30, 1181 | 0, 1182 | 3, 1183 | 7, 1184 | "FLOAT" 1185 | ], 1186 | [ 1187 | 23, 1188 | 31, 1189 | 0, 1190 | 3, 1191 | 8, 1192 | "normal,karras,exponential,sgm_uniform,simple,ddim_uniform" 1193 | ], 1194 | [ 1195 | 28, 1196 | 35, 1197 | 0, 1198 | 4, 1199 | 0, 1200 | "epicrealism_pureEvolutionV5.safetensors" 1201 | ], 1202 | [ 1203 | 38, 1204 | 29, 1205 | 0, 1206 | 39, 1207 | 1, 1208 | "INT" 1209 | ], 1210 | [ 1211 | 39, 1212 | 30, 1213 | 0, 1214 | 39, 1215 | 2, 1216 | "FLOAT" 1217 | ], 1218 | [ 1219 | 40, 1220 | 35, 1221 | 0, 1222 | 39, 1223 | 3, 1224 | "epicrealism_pureEvolutionV5.safetensors" 1225 | ], 1226 | [ 1227 | 41, 1228 | 28, 1229 | 0, 1230 | 39, 1231 | 4, 1232 | "euler,euler_ancestral,heun,dpm_2,dpm_2_ancestral,lms,dpm_fast,dpm_adaptive,dpmpp_2s_ancestral,dpmpp_sde,dpmpp_sde_gpu,dpmpp_2m,dpmpp_2m_sde,dpmpp_2m_sde_gpu,dpmpp_3m_sde,dpmpp_3m_sde_gpu,ddim,uni_pc,uni_pc_bh2" 1233 | ], 1234 | [ 1235 | 42, 1236 | 31, 1237 | 0, 1238 | 39, 1239 | 5, 1240 | "normal,karras,exponential,sgm_uniform,simple,ddim_uniform" 1241 | ], 1242 | [ 1243 | 43, 1244 | 17, 1245 | 0, 1246 | 39, 1247 | 8, 1248 | "INT" 1249 | ], 1250 | [ 1251 | 44, 1252 | 19, 1253 | 0, 1254 | 39, 1255 | 6, 1256 | "STRING" 1257 | ], 1258 | [ 1259 | 45, 1260 | 22, 1261 | 0, 1262 | 39, 1263 | 7, 1264 | "STRING" 1265 | ], 1266 | [ 1267 | 46, 1268 | 27, 1269 | 0, 1270 | 39, 1271 | 9, 1272 | "INT" 1273 | ], 1274 | [ 1275 | 47, 1276 | 26, 1277 | 0, 1278 | 39, 1279 | 10, 1280 | "INT" 1281 | ], 1282 | [ 1283 | 48, 1284 | 8, 1285 | 0, 1286 | 39, 1287 | 0, 1288 | "IMAGE" 1289 | ] 1290 | ], 1291 | "groups": [], 1292 | "config": {}, 1293 | "extra": {}, 1294 | "version": 0.4 1295 | } -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | from datetime import datetime 4 | import json 5 | import piexif 6 | import piexif.helper 7 | from PIL import Image, ExifTags 8 | from PIL.PngImagePlugin import PngInfo 9 | import numpy as np 10 | import folder_paths 11 | import comfy.sd 12 | from nodes import MAX_RESOLUTION 13 | 14 | 15 | def parse_name(ckpt_name): 16 | path = ckpt_name 17 | filename = path.split("/")[-1] 18 | filename = filename.split(".")[:-1] 19 | filename = ".".join(filename) 20 | return filename 21 | 22 | 23 | def calculate_sha256(file_path): 24 | sha256_hash = hashlib.sha256() 25 | 26 | with open(file_path, "rb") as f: 27 | # Read the file in chunks to avoid loading the entire file into memory 28 | for byte_block in iter(lambda: f.read(4096), b""): 29 | sha256_hash.update(byte_block) 30 | 31 | return sha256_hash.hexdigest() 32 | 33 | 34 | def handle_whitespace(string: str): 35 | return string.strip().replace("\n", " ").replace("\r", " ").replace("\t", " ") 36 | 37 | 38 | def get_timestamp(time_format): 39 | now = datetime.now() 40 | try: 41 | timestamp = now.strftime(time_format) 42 | except: 43 | timestamp = now.strftime("%Y-%m-%d-%H%M%S") 44 | 45 | return timestamp 46 | 47 | 48 | def make_pathname(filename, seed, modelname, counter, time_format): 49 | filename = filename.replace("%date", get_timestamp("%Y-%m-%d")) 50 | filename = filename.replace("%time", get_timestamp(time_format)) 51 | filename = filename.replace("%model", modelname) 52 | filename = filename.replace("%seed", str(seed)) 53 | filename = filename.replace("%counter", str(counter)) 54 | return filename 55 | 56 | 57 | def make_filename(filename, seed, modelname, counter, time_format): 58 | filename = make_pathname(filename, seed, modelname, counter, time_format) 59 | 60 | return get_timestamp(time_format) if filename == "" else filename 61 | 62 | 63 | class SeedGenerator: 64 | RETURN_TYPES = ("INT",) 65 | FUNCTION = "get_seed" 66 | CATEGORY = "ImageSaverTools/utils" 67 | 68 | @classmethod 69 | def INPUT_TYPES(cls): 70 | return {"required": {"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})}} 71 | 72 | def get_seed(self, seed): 73 | return (seed,) 74 | 75 | 76 | class StringLiteral: 77 | RETURN_TYPES = ("STRING",) 78 | FUNCTION = "get_string" 79 | CATEGORY = "ImageSaverTools/utils" 80 | 81 | @classmethod 82 | def INPUT_TYPES(cls): 83 | return {"required": {"string": ("STRING", {"default": "", "multiline": True})}} 84 | 85 | def get_string(self, string): 86 | return (string,) 87 | 88 | 89 | class SizeLiteral: 90 | RETURN_TYPES = ("INT",) 91 | FUNCTION = "get_int" 92 | CATEGORY = "ImageSaverTools/utils" 93 | 94 | @classmethod 95 | def INPUT_TYPES(cls): 96 | return {"required": {"int": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8})}} 97 | 98 | def get_int(self, int): 99 | return (int,) 100 | 101 | 102 | class IntLiteral: 103 | RETURN_TYPES = ("INT",) 104 | FUNCTION = "get_int" 105 | CATEGORY = "ImageSaverTools/utils" 106 | 107 | @classmethod 108 | def INPUT_TYPES(cls): 109 | return {"required": {"int": ("INT", {"default": 0, "min": 0, "max": 1000000})}} 110 | 111 | def get_int(self, int): 112 | return (int,) 113 | 114 | 115 | class CfgLiteral: 116 | RETURN_TYPES = ("FLOAT",) 117 | FUNCTION = "get_float" 118 | CATEGORY = "ImageSaverTools/utils" 119 | 120 | @classmethod 121 | def INPUT_TYPES(cls): 122 | return {"required": {"float": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0})}} 123 | 124 | def get_float(self, float): 125 | return (float,) 126 | 127 | 128 | class CheckpointSelector: 129 | CATEGORY = 'ImageSaverTools/utils' 130 | RETURN_TYPES = (folder_paths.get_filename_list("checkpoints"),) 131 | RETURN_NAMES = ("ckpt_name",) 132 | FUNCTION = "get_names" 133 | 134 | @classmethod 135 | def INPUT_TYPES(cls): 136 | return {"required": {"ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),}} 137 | 138 | def get_names(self, ckpt_name): 139 | return (ckpt_name,) 140 | 141 | 142 | class SamplerSelector: 143 | CATEGORY = 'ImageSaverTools/utils' 144 | RETURN_TYPES = (comfy.samplers.KSampler.SAMPLERS,) 145 | RETURN_NAMES = ("sampler_name",) 146 | FUNCTION = "get_names" 147 | 148 | @classmethod 149 | def INPUT_TYPES(cls): 150 | return {"required": {"sampler_name": (comfy.samplers.KSampler.SAMPLERS,)}} 151 | 152 | def get_names(self, sampler_name): 153 | return (sampler_name,) 154 | 155 | 156 | class SchedulerSelector: 157 | CATEGORY = 'ImageSaverTools/utils' 158 | RETURN_TYPES = (comfy.samplers.KSampler.SCHEDULERS,) 159 | RETURN_NAMES = ("scheduler",) 160 | FUNCTION = "get_names" 161 | 162 | @classmethod 163 | def INPUT_TYPES(cls): 164 | return {"required": {"scheduler": (comfy.samplers.KSampler.SCHEDULERS,)}} 165 | 166 | def get_names(self, scheduler): 167 | return (scheduler,) 168 | 169 | 170 | class ImageSaveWithMetadata: 171 | def __init__(self): 172 | self.output_dir = folder_paths.output_directory 173 | 174 | @classmethod 175 | def INPUT_TYPES(cls): 176 | return { 177 | "required": { 178 | "images": ("IMAGE", ), 179 | "filename": ("STRING", {"default": f'%time_%seed', "multiline": False}), 180 | "path": ("STRING", {"default": '', "multiline": False}), 181 | "extension": (['png', 'jpeg', 'webp'],), 182 | "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), 183 | "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), 184 | "modelname": (folder_paths.get_filename_list("checkpoints"),), 185 | "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), 186 | "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), 187 | }, 188 | "optional": { 189 | "positive": ("STRING", {"default": 'unknown', "multiline": True}), 190 | "negative": ("STRING", {"default": 'unknown', "multiline": True}), 191 | "seed_value": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 192 | "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8}), 193 | "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 8}), 194 | "lossless_webp": ("BOOLEAN", {"default": True}), 195 | "quality_jpeg_or_webp": ("INT", {"default": 100, "min": 1, "max": 100}), 196 | "counter": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff }), 197 | "time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}), 198 | }, 199 | "hidden": { 200 | "prompt": "PROMPT", 201 | "extra_pnginfo": "EXTRA_PNGINFO" 202 | }, 203 | } 204 | 205 | RETURN_TYPES = () 206 | FUNCTION = "save_files" 207 | 208 | OUTPUT_NODE = True 209 | 210 | CATEGORY = "ImageSaverTools" 211 | 212 | def save_files(self, images, seed_value, steps, cfg, sampler_name, scheduler, positive, negative, modelname, quality_jpeg_or_webp, 213 | lossless_webp, width, height, counter, filename, path, extension, time_format, prompt=None, extra_pnginfo=None): 214 | filename = make_filename(filename, seed_value, modelname, counter, time_format) 215 | path = make_pathname(path, seed_value, modelname, counter, time_format) 216 | ckpt_path = folder_paths.get_full_path("checkpoints", modelname) 217 | basemodelname = parse_name(modelname) 218 | modelhash = calculate_sha256(ckpt_path)[:10] 219 | comment = f"{handle_whitespace(positive)}\nNegative prompt: {handle_whitespace(negative)}\nSteps: {steps}, Sampler: {sampler_name}{f'_{scheduler}' if scheduler != 'normal' else ''}, CFG Scale: {cfg}, Seed: {seed_value}, Size: {width}x{height}, Model hash: {modelhash}, Model: {basemodelname}, Version: ComfyUI" 220 | output_path = os.path.join(self.output_dir, path) 221 | 222 | if output_path.strip() != '': 223 | if not os.path.exists(output_path.strip()): 224 | print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.') 225 | os.makedirs(output_path, exist_ok=True) 226 | 227 | filenames = self.save_images(images, output_path, filename, comment, extension, quality_jpeg_or_webp, lossless_webp, prompt, extra_pnginfo) 228 | 229 | subfolder = os.path.normpath(path) 230 | return {"ui": {"images": map(lambda filename: {"filename": filename, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'}, filenames)}} 231 | 232 | def save_images(self, images, output_path, filename_prefix, comment, extension, quality_jpeg_or_webp, lossless_webp, prompt=None, extra_pnginfo=None) -> list[str]: 233 | img_count = 1 234 | paths = list() 235 | for image in images: 236 | i = 255. * image.cpu().numpy() 237 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 238 | if images.size()[0] > 1: 239 | filename_prefix += "_{:02d}".format(img_count) 240 | 241 | if extension == 'png': 242 | metadata = PngInfo() 243 | metadata.add_text("parameters", comment) 244 | 245 | if prompt is not None: 246 | metadata.add_text("prompt", json.dumps(prompt)) 247 | if extra_pnginfo is not None: 248 | for x in extra_pnginfo: 249 | metadata.add_text(x, json.dumps(extra_pnginfo[x])) 250 | 251 | filename = f"{filename_prefix}.png" 252 | img.save(os.path.join(output_path, filename), pnginfo=metadata, optimize=True) 253 | else: 254 | filename = f"{filename_prefix}.{extension}" 255 | file = os.path.join(output_path, filename) 256 | img.save(file, optimize=True, quality=quality_jpeg_or_webp, lossless=lossless_webp) 257 | exif_bytes = piexif.dump({ 258 | "Exif": { 259 | piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(comment, encoding="unicode") 260 | }, 261 | }) 262 | piexif.insert(exif_bytes, file) 263 | 264 | paths.append(filename) 265 | img_count += 1 266 | return paths 267 | 268 | 269 | NODE_CLASS_MAPPINGS = { 270 | "Checkpoint Selector": CheckpointSelector, 271 | "Save Image w/Metadata": ImageSaveWithMetadata, 272 | "Sampler Selector": SamplerSelector, 273 | "Scheduler Selector": SchedulerSelector, 274 | "Seed Generator": SeedGenerator, 275 | "String Literal": StringLiteral, 276 | "Width/Height Literal": SizeLiteral, 277 | "Cfg Literal": CfgLiteral, 278 | "Int Literal": IntLiteral, 279 | } 280 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | piexif 3 | --------------------------------------------------------------------------------