From 70a3aa1a2cb3e1b36eecf6371cf7656a472d2ffc Mon Sep 17 00:00:00 2001 From: TensorKaze Date: Sun, 25 May 2025 04:44:52 +0200 Subject: [PATCH] Add ComfyUI-TkNodes pack with custom nodes, workflow, documentation, and dependencies --- .gitignore | 3 + LICENSE | 21 ++ README.md | 119 +++++++++ __init__.py | 40 +++ ...lux.1 Dev t2i + i2i + LoRA + Upscaler.json | 1 + flux_advanced_sampler.py | 91 +++++++ flux_latent_sampler.py | 62 +++++ load_image_and_scale.py | 114 +++++++++ load_model_and_upscale.py | 79 ++++++ multi_latent_selector.py | 46 ++++ multi_model_loader.py | 242 ++++++++++++++++++ repeat_latent_batch_optional.py | 42 +++ requirements.txt | 4 + vae_encode_optional.py | 45 ++++ 14 files changed, 909 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 example_workflows/Flux.1 Dev t2i + i2i + LoRA + Upscaler.json create mode 100644 flux_advanced_sampler.py create mode 100644 flux_latent_sampler.py create mode 100644 load_image_and_scale.py create mode 100644 load_model_and_upscale.py create mode 100644 multi_latent_selector.py create mode 100644 multi_model_loader.py create mode 100644 repeat_latent_batch_optional.py create mode 100644 requirements.txt create mode 100644 vae_encode_optional.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b96496b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.ruff_cache/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b8ffbd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 TensorKaze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..993bc54 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# ComfyUI-TkNodes + +A collection of custom nodes for ComfyUI by TensorKaze, designed to enhance workflows with advanced sampling, latent manipulation, image processing, and model loading. Includes nodes optimized for Flux.1 as well as general-purpose utilities for ComfyUI. +These nodes aim to streamline complex workflows, offering quality-of-life improvements for both Flux-based and general diffusion tasks. This is a work in progress, with node descriptions available in the ComfyUI interface (click the ? icon on each node for details). +Installation +Clone this repository into the custom_nodes folder of ComfyUI: +bash + +cd ComfyUI/custom_nodes +git clone https://github.com/TensorKaze/ComfyUI-TkNodes + +Install dependencies: +bash + +cd ComfyUI-TkNodes +pip install -r requirements.txt + +If you're using the portable version of ComfyUI, run: +bash + +ComfyUI_windows_portable/python_embeded/python.exe -m pip install -r ComfyUI/custom_nodes/ComfyUI-TkNodes/requirements.txt + +Note: Ensure you have a PyTorch version compatible with your hardware. For example: +bash + +pip install torch --index-url https://download.pytorch.org/whl/cu121 # For CUDA 12.1 +pip install torch # For CPU + +Recommended: PyTorch 2.0.0 or higher for Flux.1 nodes. + +Restart ComfyUI to load the nodes. + +Requirements +ComfyUI updated to the latest version (recommended: as of May 24, 2025). + +Flux.1 model (Dev or Schnell) for Flux-specific nodes (FluxAdvancedSampler, FluxLatentSampler). + +Minimum VRAM: 6 GB (for Flux GGUF) or 8-12 GB (for Flux.1 Dev/Schnell). + +Optional: Upscale models for LoadModelAndUpscaleImage (available via ComfyUI's model downloader or Hugging Face). + +Nodes +FluxAdvancedSampler +Advanced sampling for Flux.1 with customizable guidance, denoising, and scheduler settings. +Inputs: Model, conditioning, sampler name, noise seed, steps, denoise, scheduler, guidance, latent image, VAE. + +Output: Decoded image. + +Category: sampling/flux + +Use case: Generate high-quality images with precise control over Flux sampling parameters. + +FluxLatentSampler +Generates empty latents for Flux.1 with adjustable shift parameters. +Inputs: Model, width, height, batch size, max shift, base shift. + +Outputs: Modified model and latent. + +Category: advanced/model + +Use case: Create latents for Flux workflows with custom resolutions and sampling adjustments. + +LoadImageAndScaleToTotalPixels +Loads an image from the input directory and scales it to a specified total pixel count (in megapixels). +Inputs: Image file, upscale method, target megapixels. + +Outputs: Scaled image and mask (from alpha channel, if present). + +Category: image + +Use case: Preprocess images to desired resolutions for diffusion or other workflows. + +LoadModelAndUpscaleImage +Loads an upscale model and applies it to enhance the resolution of an input image. +Inputs: Image, upscale model name. + +Output: Upscaled image. + +Category: image/upscaling + +Use case: Improve image quality using pretrained upscale models. + +MultiLatentSelector +Selects one of up to four input latents based on a selector, simplifying dynamic workflows. +Inputs: Up to four latents, selector. + +Output: Selected latent. + +Category: utilities/latent + +Use case: Switch between latents without complex rerouting in workflows. + +MultiModelLoader +Loads a diffusion model, dual CLIP models, VAE, and optionally applies a LoRA with customizable strengths. +Inputs: UNET name, weight dtype, CLIP names (two), CLIP type, VAE name, LoRA name, model/CLIP strengths, bypass LoRA option, CLIP device. + +Outputs: Diffusion model, CLIP, VAE. + +Category: loaders + +Use case: Efficiently load and configure models for diffusion workflows, with LoRA support. + +Known Limitations +Flux-specific nodes (FluxAdvancedSampler, FluxLatentSampler) require Flux.1 models, which may need 6-12 GB of VRAM depending on the model. + +LoadModelAndUpscaleImage requires compatible upscale models, downloadable separately. + +Ensure PyTorch is installed with the correct CUDA version for GPU users to avoid compatibility issues. + +Some nodes may not work with outdated ComfyUI versions or incompatible model types. + +Heavy workflows (e.g., large batches or high-resolution images) may require significant computational resources. + +Support +For issues or feature requests, open an issue on this repository: GitHub Issues. + +Check the ? icon on each node in ComfyUI for detailed descriptions. + +Join the ComfyUI community on Reddit or Discord for additional support. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3036275 --- /dev/null +++ b/__init__.py @@ -0,0 +1,40 @@ +from .multi_latent_selector import NODE_CLASS_MAPPINGS as MultiLatentSelectorMappings +from .multi_latent_selector import NODE_DISPLAY_NAME_MAPPINGS as MultiLatentSelectorDisplayNames +from .flux_latent_sampler import NODE_CLASS_MAPPINGS as FluxLatentSamplerMappings +from .flux_latent_sampler import NODE_DISPLAY_NAME_MAPPINGS as FluxLatentSamplerDisplayNames +from .flux_advanced_sampler import NODE_CLASS_MAPPINGS as FluxAdvancedSamplerMappings +from .flux_advanced_sampler import NODE_DISPLAY_NAME_MAPPINGS as FluxAdvancedSamplerDisplayNames +from .multi_model_loader import NODE_CLASS_MAPPINGS as MultiModelLoaderMappings +from .multi_model_loader import NODE_DISPLAY_NAME_MAPPINGS as MultiModelLoaderDisplayNames +from .load_image_and_scale import NODE_CLASS_MAPPINGS as LoadImageAndScaleMappings +from .load_image_and_scale import NODE_DISPLAY_NAME_MAPPINGS as LoadImageAndScaleDisplayNames +from .load_model_and_upscale import NODE_CLASS_MAPPINGS as LoadModelAndUpscaleMappings +from .load_model_and_upscale import NODE_DISPLAY_NAME_MAPPINGS as LoadModelAndUpscaleDisplayNames +from .vae_encode_optional import NODE_CLASS_MAPPINGS as VAEEncodeOptionalMappings +from .vae_encode_optional import NODE_DISPLAY_NAME_MAPPINGS as VAEEncodeOptionalDisplayNames +from .repeat_latent_batch_optional import NODE_CLASS_MAPPINGS as RepeatLatentBatchOptionalMappings +from .repeat_latent_batch_optional import NODE_DISPLAY_NAME_MAPPINGS as RepeatLatentBatchOptionalDisplayNames + +NODE_CLASS_MAPPINGS = { + **MultiLatentSelectorMappings, + **FluxLatentSamplerMappings, + **MultiModelLoaderMappings, + **FluxAdvancedSamplerMappings, + **LoadImageAndScaleMappings, + **LoadModelAndUpscaleMappings, + **VAEEncodeOptionalMappings, + **RepeatLatentBatchOptionalMappings +} + +NODE_DISPLAY_NAME_MAPPINGS = { + **MultiLatentSelectorDisplayNames, + **FluxLatentSamplerDisplayNames, + **MultiModelLoaderDisplayNames, + **FluxAdvancedSamplerDisplayNames, + **LoadImageAndScaleDisplayNames, + **LoadModelAndUpscaleDisplayNames, + **VAEEncodeOptionalDisplayNames, + **RepeatLatentBatchOptionalDisplayNames +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/example_workflows/Flux.1 Dev t2i + i2i + LoRA + Upscaler.json b/example_workflows/Flux.1 Dev t2i + i2i + LoRA + Upscaler.json new file mode 100644 index 0000000..f5d84f2 --- /dev/null +++ b/example_workflows/Flux.1 Dev t2i + i2i + LoRA + Upscaler.json @@ -0,0 +1 @@ +{"id":"ad88c293-98c1-4247-b3e7-5a82e0caab14","revision":0,"last_node_id":41,"last_link_id":42,"nodes":[{"id":12,"type":"Note","pos":[-7429.873046875,1840.5435791015625],"size":[300.4104309082031,88],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Latent selector es muy cómodo para usar diferentes\nmodos sin necesidad de hacer bypass, simplemente\nselecciona latent_1 si quieres usar flux.1 dev normal,\ny selecciona latent_2 si quieres usar image to image."],"color":"#323","bgcolor":"#535","shape":1},{"id":13,"type":"Note","pos":[-7430.44189453125,1700.608154296875],"size":[300.4104309082031,88],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Puedes cargar el LoRA y simplemente seleccionar bypass_lora \"yes\" para ignorarlo, lo que hace que sea muy cómodo."],"color":"#223","bgcolor":"#335","shape":1},{"id":24,"type":"MultiModelLoader","pos":[17.879974365234375,3.0912859439849854],"size":[328.6109619140625,338.974365234375],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null},{"localized_name":"clip_name1","name":"clip_name1","type":"COMBO","widget":{"name":"clip_name1"},"link":null},{"localized_name":"clip_name2","name":"clip_name2","type":"COMBO","widget":{"name":"clip_name2"},"link":null},{"localized_name":"clip_type","name":"clip_type","type":"COMBO","widget":{"name":"clip_type"},"link":null},{"localized_name":"clip_device","name":"clip_device","type":"COMBO","widget":{"name":"clip_device"},"link":null},{"localized_name":"vae_name","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null},{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":null},{"localized_name":"strength_model","name":"strength_model","type":"FLOAT","widget":{"name":"strength_model"},"link":null},{"localized_name":"strength_clip","name":"strength_clip","type":"FLOAT","widget":{"name":"strength_clip"},"link":null},{"localized_name":"bypass_lora","name":"bypass_lora","type":"BOOLEAN","widget":{"name":"bypass_lora"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[26]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[27]},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[28,29]}],"properties":{"Node name for S&R":"MultiModelLoader"},"widgets_values":["flux\\flux1-dev.safetensors","fp8_e4m3fn","flux\\t5xxl_fp16.safetensors","flux\\clip_l.safetensors","flux","default","flux\\flux_ae.safetensors","none",1,1,false],"color":"#223","bgcolor":"#335","shape":1},{"id":19,"type":"RepeatLatentBatchOptional","pos":[18.044692993164062,598.1635131835938],"size":[328.823974609375,58.791378021240234],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":21},{"localized_name":"amount","name":"amount","type":"INT","widget":{"name":"amount"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[22]}],"properties":{"Node name for S&R":"RepeatLatentBatchOptional"},"widgets_values":[1],"color":"#323","bgcolor":"#535","shape":1},{"id":17,"type":"VAEEncodeOptional","pos":[17.96588897705078,708.1612548828125],"size":[328.7763977050781,48.3939208984375],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"VAE","link":28},{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":41}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[21]}],"properties":{"Node name for S&R":"VAEEncodeOptional"},"widgets_values":[],"color":"#322","bgcolor":"#533","shape":1},{"id":9,"type":"CLIPTextEncode","pos":[377.8952331542969,22.90460205078125],"size":[413.1930236816406,138.53173828125],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":27},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","slot_index":0,"links":[8]}],"title":"CLIP Text Encode (Positive Prompt)","properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"CLIPTextEncode"},"widgets_values":["cute anime girl with massive fluffy fennec ears and a big fluffy tail blonde messy long hair blue eyes wearing a maid outfit with a long black gold leaf pattern dress and a white apron mouth open holding a fancy black forest cake with candles on top in the kitchen of an old dark Victorian mansion lit by candlelight with a bright window to the foggy forest and very expensive stuff everywhere"],"color":"#232","bgcolor":"#353","shape":1},{"id":5,"type":"FluxLatentSampler","pos":[378.1914367675781,212.85781860351562],"size":[413.27764892578125,179.42669677734375],"flags":{"collapsed":false},"order":6,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":26},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"max_shift","name":"max_shift","type":"FLOAT","widget":{"name":"max_shift"},"link":null},{"localized_name":"base_shift","name":"base_shift","type":"FLOAT","widget":{"name":"base_shift"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[7]},{"localized_name":"latent","name":"latent","type":"LATENT","links":[12]}],"properties":{"Node name for S&R":"FluxLatentSampler"},"widgets_values":[1024,1024,1,1.15,0.5],"color":"#323","bgcolor":"#535","shape":1},{"id":4,"type":"PreviewImage","pos":[831.5911254882812,62.844722747802734],"size":[1010.4718627929688,1069.106689453125],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":25}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.30","Node name for S&R":"PreviewImage"},"widgets_values":[],"color":"#322","bgcolor":"#533","shape":1},{"id":10,"type":"MultiLatentSelector","pos":[17.9761962890625,392.8150939941406],"size":[329.0559387207031,119.33100128173828],"flags":{},"order":10,"mode":0,"inputs":[{"label":"latent_1 (Text to Image)","localized_name":"latent_1","name":"latent_1","shape":7,"type":"LATENT","link":12},{"label":"latent_2 (Image to Image)","localized_name":"latent_2","name":"latent_2","shape":7,"type":"LATENT","link":22},{"localized_name":"latent_3","name":"latent_3","shape":7,"type":"LATENT","link":null},{"localized_name":"latent_4","name":"latent_4","shape":7,"type":"LATENT","link":null},{"localized_name":"selector","name":"selector","shape":7,"type":"COMBO","widget":{"name":"selector"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[9]}],"title":"Select Flux Mode","properties":{"Node name for S&R":"MultiLatentSelector"},"widgets_values":["latent_1"],"color":"#323","bgcolor":"#535","shape":1},{"id":20,"type":"Note","pos":[377.8642272949219,893.194091796875],"size":[411.83935546875,128.96029663085938],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Select Flux Mode node:\n\n- Choose `latent_1` in \"Select Flux Mode\" for Text-to-Image (Flux.1 dev, denoise=1.0).\n\n- Choose `latent_2` for Image-to-Image (use LoadImageAndScaleToTotalPixels, denoise=0.8-0.9, e.g., 0.85)."],"color":"#432","bgcolor":"#653","shape":1},{"id":21,"type":"Note","pos":[377.8769836425781,1072.78076171875],"size":[412.0301513671875,113.0118408203125],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["In Text-to-Image (t2i), even if you load an image in \nLoad Image And Scale To Total Pixels, VAE Encode Optional and \nRepeat Latent Batch Optional ditch it and return \"none\" with no bypass needed. \n\nIn Image-to-Image (i2i), they use the image."],"color":"#432","bgcolor":"#653","shape":1},{"id":23,"type":"LoadModelAndUpscaleImage","pos":[377.84478759765625,754.2957153320312],"size":[412.4425354003906,82],"flags":{},"order":12,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":24},{"localized_name":"model_name","name":"model_name","type":"COMBO","widget":{"name":"model_name"},"link":null},{"localized_name":"bypass_upscaler","name":"bypass_upscaler","type":"BOOLEAN","widget":{"name":"bypass_upscaler"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[25]}],"properties":{"Node name for S&R":"LoadModelAndUpscaleImage"},"widgets_values":["4xLSDIR\\4xLSDIR.pth",false],"color":"#223","bgcolor":"#335","shape":1},{"id":8,"type":"FluxAdvancedSampler","pos":[377.9621887207031,442.79302978515625],"size":[412.3013916015625,262],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":7},{"localized_name":"conditioning","name":"conditioning","type":"CONDITIONING","link":8},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":9},{"localized_name":"vae","name":"vae","type":"VAE","link":29},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"noise_seed","name":"noise_seed","type":"INT","widget":{"name":"noise_seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"guidance","name":"guidance","type":"FLOAT","widget":{"name":"guidance"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[24]}],"properties":{"Node name for S&R":"FluxAdvancedSampler"},"widgets_values":["euler",204085615555108,"randomize",20,1,"simple",3.5],"color":"#332922","bgcolor":"#593930","shape":1},{"id":40,"type":"LoadImageAndScaleToTotalPixels","pos":[17.755699157714844,807.9051513671875],"size":[329.2864074707031,398],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"upscale_method","name":"upscale_method","type":"COMBO","widget":{"name":"upscale_method"},"link":null},{"localized_name":"megapixels","name":"megapixels","type":"FLOAT","widget":{"name":"megapixels"},"link":null},{"localized_name":"bypass_sttp","name":"bypass_sttp","type":"BOOLEAN","widget":{"name":"bypass_sttp"},"link":null},{"localized_name":"upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[41]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImageAndScaleToTotalPixels"},"widgets_values":["ComfyUI_temp_dtryh_00001_.png","lanczos",1,false,"image"],"color":"#223","bgcolor":"#335","shape":1}],"links":[[7,5,0,8,0,"MODEL"],[8,9,0,8,1,"CONDITIONING"],[9,10,0,8,2,"LATENT"],[12,5,1,10,0,"LATENT"],[21,17,0,19,0,"LATENT"],[22,19,0,10,1,"LATENT"],[24,8,0,23,0,"IMAGE"],[25,23,0,4,0,"IMAGE"],[26,24,0,5,0,"MODEL"],[27,24,1,9,0,"CLIP"],[28,24,2,17,0,"VAE"],[29,24,2,8,3,"VAE"],[41,40,0,17,1,"IMAGE"]],"groups":[{"id":1,"title":"Flux.1 Dev T2I + I2I + LoRA + Upscaler by TensorKaze","bounding":[-2.085364580154419,-77.21869659423828,1858.8880615234375,1297.8870849609375],"color":"#b58b2a","font_size":24,"flags":{}},{"id":2,"title":"I2I","bounding":[6.586372375488281,523.02978515625,350.2113037109375,689.328857421875],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"frontendVersion":"1.18.9","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4} \ No newline at end of file diff --git a/flux_advanced_sampler.py b/flux_advanced_sampler.py new file mode 100644 index 0000000..c586b51 --- /dev/null +++ b/flux_advanced_sampler.py @@ -0,0 +1,91 @@ +import comfy.samplers +import comfy.sample +import latent_preview +import torch +import node_helpers +from comfy.comfy_types import IO + +class Guider_Basic(comfy.samplers.CFGGuider): + def set_conds(self, positive): + self.inner_set_conds({"positive": positive}) + +class FluxAdvancedSampler: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL", {"tooltip": "The diffusion model used for sampling."}), + "conditioning": ("CONDITIONING", {"tooltip": "The conditioning to guide the sampling process."}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The name of the sampler to use for Flux sampling."}), + "noise_seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xffffffffffffffff, + "control_after_generate": True, + "tooltip": "The random seed for noise generation." + }), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "Number of sampling steps."}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Denoising strength."}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"tooltip": "The scheduler for sigma calculation."}), + "guidance": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.1, "tooltip": "Guidance scale for conditioning."}), + "latent_image": ("LATENT", {"tooltip": "The input latent image to sample."}), + "vae": (IO.VAE, {"tooltip": "The VAE model used to decode the latent image."}), + } + } + + RETURN_TYPES = (IO.IMAGE,) + RETURN_NAMES = ("IMAGE",) + OUTPUT_TOOLTIPS = ("The decoded image from the sampled latent.",) + FUNCTION = "sample_and_decode" + + CATEGORY = "sampling/flux" + DESCRIPTION = "Samples a latent image using Flux and decodes it to an image using a VAE." + + def sample_and_decode(self, model, conditioning, sampler_name, noise_seed, steps, denoise, scheduler, guidance, latent_image, vae): + # Modificar condicionamiento con guidance + conditioning = node_helpers.conditioning_set_values(conditioning, {"guidance": guidance}) + + # Crear guider con Guider_Basic + guider = Guider_Basic(model) + guider.set_conds(conditioning) + + # Obtener sampler + sampler = comfy.samplers.sampler_object(sampler_name) + + # Generar ruido + noise = comfy.sample.prepare_noise(latent_image["samples"], noise_seed, latent_image.get("batch_index", None)) + + # Calcular sigmas + total_steps = steps + if denoise < 1.0: + if denoise <= 0.0: + sigmas = torch.FloatTensor([]) + else: + total_steps = int(steps / denoise) + sigmas = comfy.samplers.calculate_sigmas(model.get_model_object("model_sampling"), scheduler, total_steps).cpu() + sigmas = sigmas[-(steps + 1):] + + # Realizar muestreo + latent = latent_image.copy() + latent_image_samples = comfy.sample.fix_empty_latent_channels(model, latent_image["samples"]) + latent["samples"] = latent_image_samples + noise_mask = latent.get("noise_mask", None) + x0_output = {} + callback = latent_preview.prepare_callback(model, sigmas.shape[-1] - 1, x0_output) + disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED + samples = guider.sample(noise, latent_image_samples, sampler, sigmas, denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=noise_seed) + samples = samples.to(comfy.model_management.intermediate_device()) + + # Decodificar el latente con VAE + decoded_samples = vae.decode(samples.to(vae.vae_dtype)) + + return (decoded_samples,) + +# Mapeo de nodos +NODE_CLASS_MAPPINGS = { + "FluxAdvancedSampler": FluxAdvancedSampler +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FluxAdvancedSampler": "Flux Advanced Sampler" +} \ No newline at end of file diff --git a/flux_latent_sampler.py b/flux_latent_sampler.py new file mode 100644 index 0000000..ce4b423 --- /dev/null +++ b/flux_latent_sampler.py @@ -0,0 +1,62 @@ +import torch +import comfy.model_management +import comfy.model_sampling +import nodes + +class FluxLatentSampler: + def __init__(self): + self.device = comfy.model_management.intermediate_device() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "width": ("INT", {"default": 1024, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 1024, "min": 16, "max": nodes.MAX_RESOLUTION, "step": 8}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), + "max_shift": ("FLOAT", {"default": 1.15, "min": 0.0, "max": 100.0, "step": 0.01}), + "base_shift": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 100.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("MODEL", "LATENT") + RETURN_NAMES = ("model", "latent") + FUNCTION = "sample" + CATEGORY = "advanced/model" + + def sample(self, model, max_shift, base_shift, width, height, batch_size): + # Generar el latente vacío + latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=self.device) + latent_output = {"samples": latent} + + # Ajustar parámetros de muestreo para Flux + m = model.clone() + + # Calcular shift + x1 = 256 + x2 = 4096 + mm = (max_shift - base_shift) / (x2 - x1) + b = base_shift - mm * x1 + shift = (width * height / (8 * 8 * 2 * 2)) * mm + b + + # Crear la clase de muestreo + sampling_base = comfy.model_sampling.ModelSamplingFlux + sampling_type = comfy.model_sampling.CONST + + class ModelSamplingAdvanced(sampling_base, sampling_type): + pass + + model_sampling = ModelSamplingAdvanced(model.model.model_config) + model_sampling.set_parameters(shift=shift) + m.add_object_patch("model_sampling", model_sampling) + + return (m, latent_output) + +NODE_CLASS_MAPPINGS = { + "FluxLatentSampler": FluxLatentSampler +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FluxLatentSampler": "Flux Latent Sampler" +} \ No newline at end of file diff --git a/load_image_and_scale.py b/load_image_and_scale.py new file mode 100644 index 0000000..9edb723 --- /dev/null +++ b/load_image_and_scale.py @@ -0,0 +1,114 @@ +import os +import numpy as np +import torch +import math +from PIL import Image, ImageSequence +import folder_paths +import comfy +from comfy.comfy_types import IO + +class LoadImageAndScaleToTotalPixels: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + + @classmethod + def INPUT_TYPES(cls): + input_dir = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] + files = folder_paths.filter_files_content_types(files, ["image"]) + return { + "required": { + "image": (["none"] + sorted(files), {"image_upload": True, "tooltip": "The image file to load from the input directory. Select 'none' to skip loading an image."}), + "upscale_method": (cls.upscale_methods, {"tooltip": "The method used for scaling the image."}), + "megapixels": ("FLOAT", { + "default": 1.0, + "min": 0.01, + "max": 16.0, + "step": 0.01, + "tooltip": "Target total pixels in megapixels (e.g., 1.0 for 1,048,576 pixels)." + }), + "bypass_sttp": ("BOOLEAN", { + "default": False, + "tooltip": "If true, skips scaling and returns the original image and mask." + }), + } + } + + RETURN_TYPES = (IO.IMAGE, IO.MASK) + RETURN_NAMES = ("IMAGE", "MASK") + OUTPUT_TOOLTIPS = ( + "The loaded and scaled image (or original if bypassed), or None if no image is provided.", + "The mask generated from the alpha channel (scaled to match the image or original if bypassed), or None if no image is provided." + ) + FUNCTION = "load_and_scale" + + CATEGORY = "image" + DESCRIPTION = "Loads an image from the input directory, scales it to a specified total number of pixels (unless bypassed), and returns the scaled mask. If 'none' is selected, returns None for both outputs." + + def load_and_scale(self, image, upscale_method, megapixels, bypass_sttp): + # Si se selecciona "none", devolver None para ambas salidas + if image == "none": + return (None, None) + + # Cargar la imagen (exactamente como LoadImage en nodes.py) + image_path = os.path.join(folder_paths.get_input_directory(), image) + i = None + try: + for img in ImageSequence.Iterator(Image.open(image_path)): + i = img.convert("RGBA") + break + if i is None: + raise ValueError(f"Failed to load image: {image_path}") + except Exception: + return (None, None) + + img = np.array(i).astype(np.float32) / 255.0 + img = torch.from_numpy(img)[None,] + + if img.shape[-1] == 4: + mask = img[0, :, :, 3].clone() # (H, W), float32 + else: + mask = torch.ones((img.shape[1], img.shape[2]), dtype=torch.float32, device=img.device) + image_tensor = img[:, :, :, :3] # (1, H, W, 3) + + # Si bypass_sttp está activado, devolver imagen y máscara originales + if bypass_sttp: + return (image_tensor, mask) + + # Escalar la imagen + samples = image_tensor.movedim(-1, 1) # (1, H, W, 3) -> (1, 3, H, W) + total = int(megapixels * 1024 * 1024) + scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2])) + width = round(samples.shape[3] * scale_by) + height = round(samples.shape[2] * scale_by) + + scaled_samples = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled") + scaled_samples = scaled_samples.movedim(1, -1) # (1, 3, H', W') -> (1, H', W', 3) + + # Escalar la máscara + mask_samples = mask[None, None, :, :] # (H, W) -> (1, 1, H, W) + # Usar bicubic para la máscara si upscale_method es lanczos, si no, usar el mismo método + mask_upscale_method = "bicubic" if upscale_method == "lanczos" else upscale_method + scaled_mask = comfy.utils.common_upscale(mask_samples, width, height, mask_upscale_method, "disabled") + scaled_mask = scaled_mask[0, 0, :, :] # (1, 1, H', W') -> (H', W') + + return (scaled_samples, scaled_mask) + + @classmethod + def IS_CHANGED(cls, image, upscale_method, megapixels, bypass_sttp, **kwargs): + if image == "none": + return None + image_path = os.path.join(folder_paths.get_input_directory(), image) + try: + hash_value = comfy.utils.calculate_file_hash(image_path) + return f"{hash_value}_{bypass_sttp}" + except AttributeError: + return f"{image_path}_{bypass_sttp}" + +# Mapeo de nodos +NODE_CLASS_MAPPINGS = { + "LoadImageAndScaleToTotalPixels": LoadImageAndScaleToTotalPixels +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LoadImageAndScaleToTotalPixels": "Load Image & Scale to Total Pixels" +} \ No newline at end of file diff --git a/load_model_and_upscale.py b/load_model_and_upscale.py new file mode 100644 index 0000000..45dfec3 --- /dev/null +++ b/load_model_and_upscale.py @@ -0,0 +1,79 @@ +import os +import torch +import folder_paths +import comfy +import comfy.utils +from comfy.comfy_types import IO +from spandrel import ModelLoader + +class LoadModelAndUpscaleImage: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {"tooltip": "The image to upscale."}), + "model_name": (folder_paths.get_filename_list("upscale_models"), {"tooltip": "The upscale model to use."}), + "bypass_upscaler": ("BOOLEAN", {"default": False, "toggle": True, "label_on": "yes", "label_off": "no", "tooltip": "Bypass upscaling and return the original image if yes."}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "load_and_upscale" + CATEGORY = "image/upscaling" + DESCRIPTION = "Loads an upscale model and upscales the input image using the model, unless bypass_upscaler is yes." + + def load_and_upscale(self, image, model_name, bypass_upscaler): + # Si bypass_upscaler es True ("yes"), devolver la imagen original + if bypass_upscaler: + return (image,) + + # Cargar el modelo de escalado (exactamente como UpscaleModelLoader en nodes_upscale_model.py) + model_path = folder_paths.get_full_path_or_raise("upscale_models", model_name) + sd = comfy.utils.load_torch_file(model_path, safe_load=True) + if "module.layers.0.residual_group.blocks.0.norm1.weight" in sd: + sd = comfy.utils.state_dict_prefix_replace(sd, {"module.":""}) + upscale_model = ModelLoader().load_from_state_dict(sd).eval() + + # Escalar la imagen con el modelo (exactamente como ImageUpscaleWithModel en nodes_upscale_model.py) + device = comfy.model_management.get_torch_device() + memory_required = comfy.model_management.module_size(upscale_model.model) + memory_required += (512 * 512 * 3) * image.element_size() * max(upscale_model.scale, 1.0) * 384.0 + memory_required += image.nelement() * image.element_size() + comfy.model_management.free_memory(memory_required, device) + + upscale_model.to(device) + in_img = image.movedim(-1, -3).to(device) # (B, H, W, C) -> (B, C, H, W) + + tile = 512 + overlap = 32 + oom = True + while oom: + try: + steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, tile_y=tile, overlap=overlap) + pbar = comfy.utils.ProgressBar(steps) + s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, upscale_amount=upscale_model.scale, pbar=pbar) + oom = False + except comfy.model_management.OOM_EXCEPTION as e: + tile //= 2 + if tile < 128: + raise e + + upscale_model.to("cpu") + scaled_image = torch.clamp(s.movedim(-3, -1), min=0, max=1.0) # (B, C, H', W') -> (B, H', W', C) + + # Devolver la imagen escalada + return (scaled_image,) + + @classmethod + def IS_CHANGED(cls, image, model_name, bypass_upscaler, **kwargs): + model_path = folder_paths.get_full_path_or_raise("upscale_models", model_name) + return comfy.utils.calculate_file_hash(model_path) + +# Mapeo de nodos +NODE_CLASS_MAPPINGS = { + "LoadModelAndUpscaleImage": LoadModelAndUpscaleImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LoadModelAndUpscaleImage": "Load Model & Upscale Image" +} \ No newline at end of file diff --git a/multi_latent_selector.py b/multi_latent_selector.py new file mode 100644 index 0000000..398d1e8 --- /dev/null +++ b/multi_latent_selector.py @@ -0,0 +1,46 @@ +class MultiLatentSelector: + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "latent_1": ("LATENT",), + "latent_2": ("LATENT",), + "latent_3": ("LATENT",), + "latent_4": ("LATENT",), + "selector": (["latent_1", "latent_2", "latent_3", "latent_4"], {"default": "latent_1"}), + }, + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "select_latent" + CATEGORY = "utilities/latent" + DISPLAY_NAME = "Multi-Latent Selector" + + def select_latent(self, latent_1=None, latent_2=None, latent_3=None, latent_4=None, selector="latent_1"): + if selector == "latent_1": + if latent_1 is None: + raise ValueError("latent_1 is not connected but was selected in the selector") + return (latent_1,) + elif selector == "latent_2": + if latent_2 is None: + raise ValueError("latent_2 is not connected but was selected in the selector") + return (latent_2,) + elif selector == "latent_3": + if latent_3 is None: + raise ValueError("latent_3 is not connected but was selected in the selector") + return (latent_3,) + elif selector == "latent_4": + if latent_4 is None: + raise ValueError("latent_4 is not connected but was selected in the selector") + return (latent_4,) + else: + raise ValueError(f"Invalid selector value: {selector}") + +NODE_CLASS_MAPPINGS = { + "MultiLatentSelector": MultiLatentSelector +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MultiLatentSelector": "Multi-Latent Selector" +} \ No newline at end of file diff --git a/multi_model_loader.py b/multi_model_loader.py new file mode 100644 index 0000000..fc62d52 --- /dev/null +++ b/multi_model_loader.py @@ -0,0 +1,242 @@ +import torch +import comfy.sd +import comfy.utils +import folder_paths +from comfy.comfy_types import IO, InputTypeDict + +class MultiModelLoader: + def __init__(self): + # Cache para almacenar modelos y LoRA + self.cached_model = None + self.cached_clip = None + self.cached_vae = None + self.cached_lora = None + self.last_unet_name = None + self.last_weight_dtype = None + self.last_clip_name1 = None + self.last_clip_name2 = None + self.last_clip_type = None + self.last_clip_device = None + self.last_vae_name = None + self.last_lora_name = None + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + loras = ["none"] + folder_paths.get_filename_list("loras") + return { + "required": { + "unet_name": (folder_paths.get_filename_list("diffusion_models"), { + "tooltip": "The name of the diffusion model (UNET) to load." + }), + "weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"], { + "tooltip": "The weight dtype for the diffusion model." + }), + "clip_name1": (folder_paths.get_filename_list("text_encoders"), { + "tooltip": "The name of the first CLIP text encoder to load." + }), + "clip_name2": (folder_paths.get_filename_list("text_encoders"), { + "tooltip": "The name of the second CLIP text encoder to load." + }), + "clip_type": (["sdxl", "sd3", "flux", "hunyuan_video", "hidream"], { + "tooltip": "The type of CLIP configuration (e.g., flux: clip-l, t5)." + }), + "clip_device": (["default", "cpu"], { + "advanced": True, + "tooltip": "Device for loading CLIP models." + }), + "vae_name": (cls.vae_list(), { + "tooltip": "The name of the VAE model to load." + }), + "lora_name": (loras, { + "tooltip": "The name of the LoRA to load (select 'none' to skip loading)." + }), + "strength_model": ("FLOAT", { + "default": 1.0, + "min": -100.0, + "max": 100.0, + "step": 0.01, + "tooltip": "Strength of the LoRA applied to the diffusion model." + }), + "strength_clip": ("FLOAT", { + "default": 1.0, + "min": -100.0, + "max": 100.0, + "step": 0.01, + "tooltip": "Strength of the LoRA applied to the CLIP model." + }), + "bypass_lora": ("BOOLEAN", { + "default": False, + "toggle": True, + "label_on": "yes", + "label_off": "no", + "tooltip": "Bypass LoRA application to MODEL and CLIP if yes, but keep it loaded." + }), + }, + } + + @staticmethod + def vae_list(): + vaes = folder_paths.get_filename_list("vae") + approx_vaes = folder_paths.get_filename_list("vae_approx") + sdxl_taesd_enc = False + sdxl_taesd_dec = False + sd1_taesd_enc = False + sd1_taesd_dec = False + sd3_taesd_enc = False + sd3_taesd_dec = False + f1_taesd_enc = False + f1_taesd_dec = False + + for v in approx_vaes: + if v.startswith("taesd_decoder."): + sd1_taesd_dec = True + elif v.startswith("taesd_encoder."): + sd1_taesd_enc = True + elif v.startswith("taesdxl_decoder."): + sdxl_taesd_dec = True + elif v.startswith("taesdxl_encoder."): + sdxl_taesd_enc = True + elif v.startswith("taesd3_decoder."): + sd3_taesd_dec = True + elif v.startswith("taesd3_encoder."): + sd3_taesd_enc = True + elif v.startswith("taef1_encoder."): + f1_taesd_dec = True + elif v.startswith("taef1_decoder."): + f1_taesd_enc = True + if sd1_taesd_dec and sd1_taesd_enc: + vaes.append("taesd") + if sdxl_taesd_dec and sdxl_taesd_enc: + vaes.append("taesdxl") + if sd3_taesd_dec and sd3_taesd_enc: + vaes.append("taesd3") + if f1_taesd_dec and f1_taesd_enc: + vaes.append("taef1") + return vaes + + @staticmethod + def load_taesd(name): + sd = {} + approx_vaes = folder_paths.get_filename_list("vae_approx") + encoder = next(filter(lambda a: a.startswith(f"{name}_encoder."), approx_vaes)) + decoder = next(filter(lambda a: a.startswith(f"{name}_decoder."), approx_vaes)) + enc = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", encoder)) + for k in enc: + sd[f"taesd_encoder.{k}"] = enc[k] + dec = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", decoder)) + for k in dec: + sd[f"taesd_decoder.{k}"] = dec[k] + if name == "taesd": + sd["vae_scale"] = torch.tensor(0.18215) + sd["vae_shift"] = torch.tensor(0.0) + elif name == "taesdxl": + sd["vae_scale"] = torch.tensor(0.13025) + sd["vae_shift"] = torch.tensor(0.0) + elif name == "taesd3": + sd["vae_scale"] = torch.tensor(1.5305) + sd["vae_shift"] = torch.tensor(0.0609) + elif name == "taef1": + sd["vae_scale"] = torch.tensor(0.3611) + sd["vae_shift"] = torch.tensor(0.1159) + return sd + + RETURN_TYPES = (IO.MODEL, IO.CLIP, IO.VAE) + RETURN_NAMES = ("MODEL", "CLIP", "VAE") + OUTPUT_TOOLTIPS = ( + "The diffusion model (modified by LoRA if loaded and not bypassed).", + "The CLIP model (modified by LoRA if loaded and not bypassed).", + "The VAE model used for encoding/decoding latents." + ) + FUNCTION = "load_unified" + + CATEGORY = "loaders" + DESCRIPTION = "Loads a diffusion model, dual CLIP models, VAE, and optionally applies a loaded LoRA to the model and CLIP with bypass option." + + def load_unified(self, unet_name, weight_dtype, clip_name1, clip_name2, clip_type, clip_device, vae_name, lora_name, strength_model, strength_clip, bypass_lora): + # Cargar modelo de difusión si no está cacheado o cambió + if (self.cached_model is None or + unet_name != self.last_unet_name or + weight_dtype != self.last_weight_dtype): + model_options = {} + if weight_dtype == "fp8_e4m3fn": + model_options["dtype"] = torch.float8_e4m3fn + elif weight_dtype == "fp8_e4m3fn_fast": + model_options["dtype"] = torch.float8_e4m3fn + model_options["fp8_optimizations"] = True + elif weight_dtype == "fp8_e5m2": + model_options["dtype"] = torch.float8_e5m2 + + unet_path = folder_paths.get_full_path_or_raise("diffusion_models", unet_name) + self.cached_model = comfy.sd.load_diffusion_model(unet_path, model_options=model_options) + self.last_unet_name = unet_name + self.last_weight_dtype = weight_dtype + + model = self.cached_model + + # Cargar CLIPs si no están cacheados o cambiaron + if (self.cached_clip is None or + clip_name1 != self.last_clip_name1 or + clip_name2 != self.last_clip_name2 or + clip_type != self.last_clip_type or + clip_device != self.last_clip_device): + clip_type_enum = getattr(comfy.sd.CLIPType, clip_type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) + clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1) + clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2) + clip_options = {} + if clip_device == "cpu": + clip_options["load_device"] = clip_options["offload_device"] = torch.device("cpu") + self.cached_clip = comfy.sd.load_clip( + ckpt_paths=[clip_path1, clip_path2], + embedding_directory=folder_paths.get_folder_paths("embeddings"), + clip_type=clip_type_enum, + model_options=clip_options + ) + self.last_clip_name1 = clip_name1 + self.last_clip_name2 = clip_name2 + self.last_clip_type = clip_type + self.last_clip_device = clip_device + + clip = self.cached_clip + + # Cargar VAE si no está cacheado o cambió + if self.cached_vae is None or vae_name != self.last_vae_name: + if vae_name in ["taesd", "taesdxl", "taesd3", "taef1"]: + sd = self.load_taesd(vae_name) + else: + vae_path = folder_paths.get_full_path_or_raise("vae", vae_name) + sd = comfy.utils.load_torch_file(vae_path) + self.cached_vae = comfy.sd.VAE(sd=sd) + self.cached_vae.throw_exception_if_invalid() + self.last_vae_name = vae_name + + vae = self.cached_vae + + # Cargar LoRA solo si lora_name no es "none" y no está cacheado o cambió + if lora_name != "none" and (self.cached_lora is None or lora_name != self.last_lora_name): + lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) + self.cached_lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + self.last_lora_name = lora_name + elif lora_name == "none": + self.cached_lora = None + self.last_lora_name = None + + # Aplicar LoRA solo si está cargado, bypass_lora es False ("no"), y las intensidades no son 0 + if self.cached_lora is not None and not bypass_lora and (strength_model != 0 or strength_clip != 0): + # Crear copias para no modificar el cache + model = comfy.sd.load_lora_for_models(self.cached_model, self.cached_clip, self.cached_lora, strength_model, strength_clip)[0] + clip = comfy.sd.load_lora_for_models(self.cached_model, self.cached_clip, self.cached_lora, strength_model, strength_clip)[1] + else: + # Usar cache directamente si bypass_lora es True ("yes") o no hay LoRA + model = self.cached_model + clip = self.cached_clip + + return (model, clip, vae) + +# Mapeo de nodos +NODE_CLASS_MAPPINGS = { + "MultiModelLoader": MultiModelLoader +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "MultiModelLoader": "Multi-Model Loader" +} \ No newline at end of file diff --git a/repeat_latent_batch_optional.py b/repeat_latent_batch_optional.py new file mode 100644 index 0000000..e09623c --- /dev/null +++ b/repeat_latent_batch_optional.py @@ -0,0 +1,42 @@ +import torch +from comfy.comfy_types import IO + +class RepeatLatentBatchOptional: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "samples": ("LATENT", {"tooltip": "The latent to repeat. If None, returns None."}), + "amount": ("INT", {"default": 1, "min": 1, "max": 64, "tooltip": "Number of times to repeat the latent."}), + } + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("LATENT",) + FUNCTION = "repeat" + CATEGORY = "latent/batch" + + def repeat(self, samples, amount): + if samples is None: + return (None,) + + s = samples.copy() + s_in = samples["samples"] + s["samples"] = s_in.repeat((amount, 1, 1, 1)) + if "noise_mask" in samples and samples["noise_mask"].shape[0] > 1: + masks = samples["noise_mask"] + if masks.shape[0] < s_in.shape[0]: + masks = masks.repeat(math.ceil(s_in.shape[0] / masks.shape[0]), 1, 1, 1)[:s_in.shape[0]] + s["noise_mask"] = samples["noise_mask"].repeat((amount, 1, 1, 1)) + if "batch_index" in s: + offset = max(s["batch_index"]) - min(s["batch_index"]) + 1 + s["batch_index"] = s["batch_index"] + [x + (i * offset) for i in range(1, amount) for x in s["batch_index"]] + return (s,) + +NODE_CLASS_MAPPINGS = { + "RepeatLatentBatchOptional": RepeatLatentBatchOptional +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RepeatLatentBatchOptional": "Repeat Latent Batch (Optional)" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a04b858 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +torch +numpy +Pillow +spandrel \ No newline at end of file diff --git a/vae_encode_optional.py b/vae_encode_optional.py new file mode 100644 index 0000000..4041b64 --- /dev/null +++ b/vae_encode_optional.py @@ -0,0 +1,45 @@ +import torch +from comfy.comfy_types import IO + +class VAEEncodeOptional: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "vae": ("VAE", {"tooltip": "The VAE model used for encoding the image to latent space."}), + }, + "optional": { + "image": ("IMAGE", {"tooltip": "The image to encode to latent space. If not provided, returns None."}), + } + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("LATENT",) + OUTPUT_TOOLTIPS = ("The encoded latent image, or None if no image is provided.",) + FUNCTION = "encode" + + CATEGORY = "latent" + DESCRIPTION = "Encodes an image to latent space using a VAE model. If no image is provided, acts as a bypass and returns None." + + def encode(self, vae, image=None): + # Modo bypass: si no hay imagen, devolver None + if image is None: + return (None,) + + # Codificar la imagen con el VAE + try: + # Asegurarse de que la imagen solo use los canales RGB (ignorar alfa si existe) + latent = vae.encode(image[:,:,:,:3]) + return ({"samples": latent},) + except Exception as e: + # En caso de error (por ejemplo, dimensiones inválidas), devolver None + return (None,) + +# Mapeo de nodos +NODE_CLASS_MAPPINGS = { + "VAEEncodeOptional": VAEEncodeOptional +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VAEEncodeOptional": "VAE Encode (Optional)" +} \ No newline at end of file