fred-dev/comfy_ui_ali
0
1from __future__ import annotations2import torch3 4import os5import sys6import json7import hashlib8import traceback9import math10import time11import random12import logging13 14from PIL import Image, ImageOps, ImageSequence15from PIL.PngImagePlugin import PngInfo16 17import numpy as np18import safetensors.torch19 20sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))21 22import comfy.diffusers_load23import comfy.samplers24import comfy.sample25import comfy.sd26import comfy.utils27import comfy.controlnet28from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict, FileLocator29 30import comfy.clip_vision31 32import comfy.model_management33from comfy.cli_args import args34 35import importlib36 37import folder_paths38import latent_preview39import node_helpers40 41def before_node_execution():42 comfy.model_management.throw_exception_if_processing_interrupted()43 44def interrupt_processing(value=True):45 comfy.model_management.interrupt_current_processing(value)46 47MAX_RESOLUTION=1638448 49class CLIPTextEncode(ComfyNodeABC):50 @classmethod51 def INPUT_TYPES(s) -> InputTypeDict:52 return {53 "required": {54 "text": (IO.STRING, {"multiline": True, "dynamicPrompts": True, "tooltip": "The text to be encoded."}),55 "clip": (IO.CLIP, {"tooltip": "The CLIP model used for encoding the text."})56 }57 }58 RETURN_TYPES = (IO.CONDITIONING,)59 OUTPUT_TOOLTIPS = ("A conditioning containing the embedded text used to guide the diffusion model.",)60 FUNCTION = "encode"61 62 CATEGORY = "conditioning"63 DESCRIPTION = "Encodes a text prompt using a CLIP model into an embedding that can be used to guide the diffusion model towards generating specific images."64 65 def encode(self, clip, text):66 if clip is None:67 raise RuntimeError("ERROR: clip input is invalid: None\n\nIf the clip is from a checkpoint loader node your checkpoint does not contain a valid clip or text encoder model.")68 tokens = clip.tokenize(text)69 return (clip.encode_from_tokens_scheduled(tokens), )70 71 72class ConditioningCombine:73 @classmethod74 def INPUT_TYPES(s):75 return {"required": {"conditioning_1": ("CONDITIONING", ), "conditioning_2": ("CONDITIONING", )}}76 RETURN_TYPES = ("CONDITIONING",)77 FUNCTION = "combine"78 79 CATEGORY = "conditioning"80 81 def combine(self, conditioning_1, conditioning_2):82 return (conditioning_1 + conditioning_2, )83 84class ConditioningAverage :85 @classmethod86 def INPUT_TYPES(s):87 return {"required": {"conditioning_to": ("CONDITIONING", ), "conditioning_from": ("CONDITIONING", ),88 "conditioning_to_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})89 }}90 RETURN_TYPES = ("CONDITIONING",)91 FUNCTION = "addWeighted"92 93 CATEGORY = "conditioning"94 95 def addWeighted(self, conditioning_to, conditioning_from, conditioning_to_strength):96 out = []97 98 if len(conditioning_from) > 1:99 logging.warning("Warning: ConditioningAverage conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.")100 101 cond_from = conditioning_from[0][0]102 pooled_output_from = conditioning_from[0][1].get("pooled_output", None)103 104 for i in range(len(conditioning_to)):105 t1 = conditioning_to[i][0]106 pooled_output_to = conditioning_to[i][1].get("pooled_output", pooled_output_from)107 t0 = cond_from[:,:t1.shape[1]]108 if t0.shape[1] < t1.shape[1]:109 t0 = torch.cat([t0] + [torch.zeros((1, (t1.shape[1] - t0.shape[1]), t1.shape[2]))], dim=1)110 111 tw = torch.mul(t1, conditioning_to_strength) + torch.mul(t0, (1.0 - conditioning_to_strength))112 t_to = conditioning_to[i][1].copy()113 if pooled_output_from is not None and pooled_output_to is not None:114 t_to["pooled_output"] = torch.mul(pooled_output_to, conditioning_to_strength) + torch.mul(pooled_output_from, (1.0 - conditioning_to_strength))115 elif pooled_output_from is not None:116 t_to["pooled_output"] = pooled_output_from117 118 n = [tw, t_to]119 out.append(n)120 return (out, )121 122class ConditioningConcat:123 @classmethod124 def INPUT_TYPES(s):125 return {"required": {126 "conditioning_to": ("CONDITIONING",),127 "conditioning_from": ("CONDITIONING",),128 }}129 RETURN_TYPES = ("CONDITIONING",)130 FUNCTION = "concat"131 132 CATEGORY = "conditioning"133 134 def concat(self, conditioning_to, conditioning_from):135 out = []136 137 if len(conditioning_from) > 1:138 logging.warning("Warning: ConditioningConcat conditioning_from contains more than 1 cond, only the first one will actually be applied to conditioning_to.")139 140 cond_from = conditioning_from[0][0]141 142 for i in range(len(conditioning_to)):143 t1 = conditioning_to[i][0]144 tw = torch.cat((t1, cond_from),1)145 n = [tw, conditioning_to[i][1].copy()]146 out.append(n)147 148 return (out, )149 150class ConditioningSetArea:151 @classmethod152 def INPUT_TYPES(s):153 return {"required": {"conditioning": ("CONDITIONING", ),154 "width": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 8}),155 "height": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 8}),156 "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),157 "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),158 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),159 }}160 RETURN_TYPES = ("CONDITIONING",)161 FUNCTION = "append"162 163 CATEGORY = "conditioning"164 165 def append(self, conditioning, width, height, x, y, strength):166 c = node_helpers.conditioning_set_values(conditioning, {"area": (height // 8, width // 8, y // 8, x // 8),167 "strength": strength,168 "set_area_to_bounds": False})169 return (c, )170 171class ConditioningSetAreaPercentage:172 @classmethod173 def INPUT_TYPES(s):174 return {"required": {"conditioning": ("CONDITIONING", ),175 "width": ("FLOAT", {"default": 1.0, "min": 0, "max": 1.0, "step": 0.01}),176 "height": ("FLOAT", {"default": 1.0, "min": 0, "max": 1.0, "step": 0.01}),177 "x": ("FLOAT", {"default": 0, "min": 0, "max": 1.0, "step": 0.01}),178 "y": ("FLOAT", {"default": 0, "min": 0, "max": 1.0, "step": 0.01}),179 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),180 }}181 RETURN_TYPES = ("CONDITIONING",)182 FUNCTION = "append"183 184 CATEGORY = "conditioning"185 186 def append(self, conditioning, width, height, x, y, strength):187 c = node_helpers.conditioning_set_values(conditioning, {"area": ("percentage", height, width, y, x),188 "strength": strength,189 "set_area_to_bounds": False})190 return (c, )191 192class ConditioningSetAreaStrength:193 @classmethod194 def INPUT_TYPES(s):195 return {"required": {"conditioning": ("CONDITIONING", ),196 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),197 }}198 RETURN_TYPES = ("CONDITIONING",)199 FUNCTION = "append"200 201 CATEGORY = "conditioning"202 203 def append(self, conditioning, strength):204 c = node_helpers.conditioning_set_values(conditioning, {"strength": strength})205 return (c, )206 207 208class ConditioningSetMask:209 @classmethod210 def INPUT_TYPES(s):211 return {"required": {"conditioning": ("CONDITIONING", ),212 "mask": ("MASK", ),213 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),214 "set_cond_area": (["default", "mask bounds"],),215 }}216 RETURN_TYPES = ("CONDITIONING",)217 FUNCTION = "append"218 219 CATEGORY = "conditioning"220 221 def append(self, conditioning, mask, set_cond_area, strength):222 set_area_to_bounds = False223 if set_cond_area != "default":224 set_area_to_bounds = True225 if len(mask.shape) < 3:226 mask = mask.unsqueeze(0)227 228 c = node_helpers.conditioning_set_values(conditioning, {"mask": mask,229 "set_area_to_bounds": set_area_to_bounds,230 "mask_strength": strength})231 return (c, )232 233class ConditioningZeroOut:234 @classmethod235 def INPUT_TYPES(s):236 return {"required": {"conditioning": ("CONDITIONING", )}}237 RETURN_TYPES = ("CONDITIONING",)238 FUNCTION = "zero_out"239 240 CATEGORY = "advanced/conditioning"241 242 def zero_out(self, conditioning):243 c = []244 for t in conditioning:245 d = t[1].copy()246 pooled_output = d.get("pooled_output", None)247 if pooled_output is not None:248 d["pooled_output"] = torch.zeros_like(pooled_output)249 n = [torch.zeros_like(t[0]), d]250 c.append(n)251 return (c, )252 253class ConditioningSetTimestepRange:254 @classmethod255 def INPUT_TYPES(s):256 return {"required": {"conditioning": ("CONDITIONING", ),257 "start": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),258 "end": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})259 }}260 RETURN_TYPES = ("CONDITIONING",)261 FUNCTION = "set_range"262 263 CATEGORY = "advanced/conditioning"264 265 def set_range(self, conditioning, start, end):266 c = node_helpers.conditioning_set_values(conditioning, {"start_percent": start,267 "end_percent": end})268 return (c, )269 270class VAEDecode:271 @classmethod272 def INPUT_TYPES(s):273 return {274 "required": {275 "samples": ("LATENT", {"tooltip": "The latent to be decoded."}),276 "vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."})277 }278 }279 RETURN_TYPES = ("IMAGE",)280 OUTPUT_TOOLTIPS = ("The decoded image.",)281 FUNCTION = "decode"282 283 CATEGORY = "latent"284 DESCRIPTION = "Decodes latent images back into pixel space images."285 286 def decode(self, vae, samples):287 images = vae.decode(samples["samples"])288 if len(images.shape) == 5: #Combine batches289 images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])290 return (images, )291 292class VAEDecodeTiled:293 @classmethod294 def INPUT_TYPES(s):295 return {"required": {"samples": ("LATENT", ), "vae": ("VAE", ),296 "tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),297 "overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),298 "temporal_size": ("INT", {"default": 64, "min": 8, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to decode at a time."}),299 "temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),300 }}301 RETURN_TYPES = ("IMAGE",)302 FUNCTION = "decode"303 304 CATEGORY = "_for_testing"305 306 def decode(self, vae, samples, tile_size, overlap=64, temporal_size=64, temporal_overlap=8):307 if tile_size < overlap * 4:308 overlap = tile_size // 4309 if temporal_size < temporal_overlap * 2:310 temporal_overlap = temporal_overlap // 2311 temporal_compression = vae.temporal_compression_decode()312 if temporal_compression is not None:313 temporal_size = max(2, temporal_size // temporal_compression)314 temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression))315 else:316 temporal_size = None317 temporal_overlap = None318 319 compression = vae.spacial_compression_decode()320 images = vae.decode_tiled(samples["samples"], tile_x=tile_size // compression, tile_y=tile_size // compression, overlap=overlap // compression, tile_t=temporal_size, overlap_t=temporal_overlap)321 if len(images.shape) == 5: #Combine batches322 images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])323 return (images, )324 325class VAEEncode:326 @classmethod327 def INPUT_TYPES(s):328 return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", )}}329 RETURN_TYPES = ("LATENT",)330 FUNCTION = "encode"331 332 CATEGORY = "latent"333 334 def encode(self, vae, pixels):335 t = vae.encode(pixels[:,:,:,:3])336 return ({"samples":t}, )337 338class VAEEncodeTiled:339 @classmethod340 def INPUT_TYPES(s):341 return {"required": {"pixels": ("IMAGE", ), "vae": ("VAE", ),342 "tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),343 "overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),344 "temporal_size": ("INT", {"default": 64, "min": 8, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to encode at a time."}),345 "temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),346 }}347 RETURN_TYPES = ("LATENT",)348 FUNCTION = "encode"349 350 CATEGORY = "_for_testing"351 352 def encode(self, vae, pixels, tile_size, overlap, temporal_size=64, temporal_overlap=8):353 t = vae.encode_tiled(pixels[:,:,:,:3], tile_x=tile_size, tile_y=tile_size, overlap=overlap, tile_t=temporal_size, overlap_t=temporal_overlap)354 return ({"samples": t}, )355 356class VAEEncodeForInpaint:357 @classmethod358 def INPUT_TYPES(s):359 return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ), "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),}}360 RETURN_TYPES = ("LATENT",)361 FUNCTION = "encode"362 363 CATEGORY = "latent/inpaint"364 365 def encode(self, vae, pixels, mask, grow_mask_by=6):366 x = (pixels.shape[1] // vae.downscale_ratio) * vae.downscale_ratio367 y = (pixels.shape[2] // vae.downscale_ratio) * vae.downscale_ratio368 mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")369 370 pixels = pixels.clone()371 if pixels.shape[1] != x or pixels.shape[2] != y:372 x_offset = (pixels.shape[1] % vae.downscale_ratio) // 2373 y_offset = (pixels.shape[2] % vae.downscale_ratio) // 2374 pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]375 mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]376 377 #grow mask by a few pixels to keep things seamless in latent space378 if grow_mask_by == 0:379 mask_erosion = mask380 else:381 kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))382 padding = math.ceil((grow_mask_by - 1) / 2)383 384 mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1)385 386 m = (1.0 - mask.round()).squeeze(1)387 for i in range(3):388 pixels[:,:,:,i] -= 0.5389 pixels[:,:,:,i] *= m390 pixels[:,:,:,i] += 0.5391 t = vae.encode(pixels)392 393 return ({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, )394 395 396class InpaintModelConditioning:397 @classmethod398 def INPUT_TYPES(s):399 return {"required": {"positive": ("CONDITIONING", ),400 "negative": ("CONDITIONING", ),401 "vae": ("VAE", ),402 "pixels": ("IMAGE", ),403 "mask": ("MASK", ),404 "noise_mask": ("BOOLEAN", {"default": True, "tooltip": "Add a noise mask to the latent so sampling will only happen within the mask. Might improve results or completely break things depending on the model."}),405 }}406 407 RETURN_TYPES = ("CONDITIONING","CONDITIONING","LATENT")408 RETURN_NAMES = ("positive", "negative", "latent")409 FUNCTION = "encode"410 411 CATEGORY = "conditioning/inpaint"412 413 def encode(self, positive, negative, pixels, vae, mask, noise_mask=True):414 x = (pixels.shape[1] // 8) * 8415 y = (pixels.shape[2] // 8) * 8416 mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")417 418 orig_pixels = pixels419 pixels = orig_pixels.clone()420 if pixels.shape[1] != x or pixels.shape[2] != y:421 x_offset = (pixels.shape[1] % 8) // 2422 y_offset = (pixels.shape[2] % 8) // 2423 pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]424 mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]425 426 m = (1.0 - mask.round()).squeeze(1)427 for i in range(3):428 pixels[:,:,:,i] -= 0.5429 pixels[:,:,:,i] *= m430 pixels[:,:,:,i] += 0.5431 concat_latent = vae.encode(pixels)432 orig_latent = vae.encode(orig_pixels)433 434 out_latent = {}435 436 out_latent["samples"] = orig_latent437 if noise_mask:438 out_latent["noise_mask"] = mask439 440 out = []441 for conditioning in [positive, negative]:442 c = node_helpers.conditioning_set_values(conditioning, {"concat_latent_image": concat_latent,443 "concat_mask": mask})444 out.append(c)445 return (out[0], out[1], out_latent)446 447 448class SaveLatent:449 def __init__(self):450 self.output_dir = folder_paths.get_output_directory()451 452 @classmethod453 def INPUT_TYPES(s):454 return {"required": { "samples": ("LATENT", ),455 "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})},456 "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},457 }458 RETURN_TYPES = ()459 FUNCTION = "save"460 461 OUTPUT_NODE = True462 463 CATEGORY = "_for_testing"464 465 def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):466 full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)467 468 # support save metadata for latent sharing469 prompt_info = ""470 if prompt is not None:471 prompt_info = json.dumps(prompt)472 473 metadata = None474 if not args.disable_metadata:475 metadata = {"prompt": prompt_info}476 if extra_pnginfo is not None:477 for x in extra_pnginfo:478 metadata[x] = json.dumps(extra_pnginfo[x])479 480 file = f"{filename}_{counter:05}_.latent"481 482 results: list[FileLocator] = []483 results.append({484 "filename": file,485 "subfolder": subfolder,486 "type": "output"487 })488 489 file = os.path.join(full_output_folder, file)490 491 output = {}492 output["latent_tensor"] = samples["samples"].contiguous()493 output["latent_format_version_0"] = torch.tensor([])494 495 comfy.utils.save_torch_file(output, file, metadata=metadata)496 return { "ui": { "latents": results } }497 498 499class LoadLatent:500 @classmethod501 def INPUT_TYPES(s):502 input_dir = folder_paths.get_input_directory()503 files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".latent")]504 return {"required": {"latent": [sorted(files), ]}, }505 506 CATEGORY = "_for_testing"507 508 RETURN_TYPES = ("LATENT", )509 FUNCTION = "load"510 511 def load(self, latent):512 latent_path = folder_paths.get_annotated_filepath(latent)513 latent = safetensors.torch.load_file(latent_path, device="cpu")514 multiplier = 1.0515 if "latent_format_version_0" not in latent:516 multiplier = 1.0 / 0.18215517 samples = {"samples": latent["latent_tensor"].float() * multiplier}518 return (samples, )519 520 @classmethod521 def IS_CHANGED(s, latent):522 image_path = folder_paths.get_annotated_filepath(latent)523 m = hashlib.sha256()524 with open(image_path, 'rb') as f:525 m.update(f.read())526 return m.digest().hex()527 528 @classmethod529 def VALIDATE_INPUTS(s, latent):530 if not folder_paths.exists_annotated_filepath(latent):531 return "Invalid latent file: {}".format(latent)532 return True533 534 535class CheckpointLoader:536 @classmethod537 def INPUT_TYPES(s):538 return {"required": { "config_name": (folder_paths.get_filename_list("configs"), ),539 "ckpt_name": (folder_paths.get_filename_list("checkpoints"), )}}540 RETURN_TYPES = ("MODEL", "CLIP", "VAE")541 FUNCTION = "load_checkpoint"542 543 CATEGORY = "advanced/loaders"544 DEPRECATED = True545 546 def load_checkpoint(self, config_name, ckpt_name):547 config_path = folder_paths.get_full_path("configs", config_name)548 ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)549 return comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))550 551class CheckpointLoaderSimple:552 @classmethod553 def INPUT_TYPES(s):554 return {555 "required": {556 "ckpt_name": (folder_paths.get_filename_list("checkpoints"), {"tooltip": "The name of the checkpoint (model) to load."}),557 }558 }559 RETURN_TYPES = ("MODEL", "CLIP", "VAE")560 OUTPUT_TOOLTIPS = ("The model used for denoising latents.",561 "The CLIP model used for encoding text prompts.",562 "The VAE model used for encoding and decoding images to and from latent space.")563 FUNCTION = "load_checkpoint"564 565 CATEGORY = "loaders"566 DESCRIPTION = "Loads a diffusion model checkpoint, diffusion models are used to denoise latents."567 568 def load_checkpoint(self, ckpt_name):569 ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)570 out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))571 return out[:3]572 573class DiffusersLoader:574 @classmethod575 def INPUT_TYPES(cls):576 paths = []577 for search_path in folder_paths.get_folder_paths("diffusers"):578 if os.path.exists(search_path):579 for root, subdir, files in os.walk(search_path, followlinks=True):580 if "model_index.json" in files:581 paths.append(os.path.relpath(root, start=search_path))582 583 return {"required": {"model_path": (paths,), }}584 RETURN_TYPES = ("MODEL", "CLIP", "VAE")585 FUNCTION = "load_checkpoint"586 587 CATEGORY = "advanced/loaders/deprecated"588 589 def load_checkpoint(self, model_path, output_vae=True, output_clip=True):590 for search_path in folder_paths.get_folder_paths("diffusers"):591 if os.path.exists(search_path):592 path = os.path.join(search_path, model_path)593 if os.path.exists(path):594 model_path = path595 break596 597 return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))598 599 600class unCLIPCheckpointLoader:601 @classmethod602 def INPUT_TYPES(s):603 return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),604 }}605 RETURN_TYPES = ("MODEL", "CLIP", "VAE", "CLIP_VISION")606 FUNCTION = "load_checkpoint"607 608 CATEGORY = "loaders"609 610 def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):611 ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)612 out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, output_clipvision=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))613 return out614 615class CLIPSetLastLayer:616 @classmethod617 def INPUT_TYPES(s):618 return {"required": { "clip": ("CLIP", ),619 "stop_at_clip_layer": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),620 }}621 RETURN_TYPES = ("CLIP",)622 FUNCTION = "set_last_layer"623 624 CATEGORY = "conditioning"625 626 def set_last_layer(self, clip, stop_at_clip_layer):627 clip = clip.clone()628 clip.clip_layer(stop_at_clip_layer)629 return (clip,)630 631class LoraLoader:632 def __init__(self):633 self.loaded_lora = None634 635 @classmethod636 def INPUT_TYPES(s):637 return {638 "required": {639 "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),640 "clip": ("CLIP", {"tooltip": "The CLIP model the LoRA will be applied to."}),641 "lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}),642 "strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative."}),643 "strength_clip": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "tooltip": "How strongly to modify the CLIP model. This value can be negative."}),644 }645 }646 647 RETURN_TYPES = ("MODEL", "CLIP")648 OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.")649 FUNCTION = "load_lora"650 651 CATEGORY = "loaders"652 DESCRIPTION = "LoRAs are used to modify diffusion and CLIP models, altering the way in which latents are denoised such as applying styles. Multiple LoRA nodes can be linked together."653 654 def load_lora(self, model, clip, lora_name, strength_model, strength_clip):655 if strength_model == 0 and strength_clip == 0:656 return (model, clip)657 658 lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)659 lora = None660 if self.loaded_lora is not None:661 if self.loaded_lora[0] == lora_path:662 lora = self.loaded_lora[1]663 else:664 self.loaded_lora = None665 666 if lora is None:667 lora = comfy.utils.load_torch_file(lora_path, safe_load=True)668 self.loaded_lora = (lora_path, lora)669 670 model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)671 return (model_lora, clip_lora)672 673class LoraLoaderModelOnly(LoraLoader):674 @classmethod675 def INPUT_TYPES(s):676 return {"required": { "model": ("MODEL",),677 "lora_name": (folder_paths.get_filename_list("loras"), ),678 "strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),679 }}680 RETURN_TYPES = ("MODEL",)681 FUNCTION = "load_lora_model_only"682 683 def load_lora_model_only(self, model, lora_name, strength_model):684 return (self.load_lora(model, None, lora_name, strength_model, 0)[0],)685 686class VAELoader:687 @staticmethod688 def vae_list():689 vaes = folder_paths.get_filename_list("vae")690 approx_vaes = folder_paths.get_filename_list("vae_approx")691 sdxl_taesd_enc = False692 sdxl_taesd_dec = False693 sd1_taesd_enc = False694 sd1_taesd_dec = False695 sd3_taesd_enc = False696 sd3_taesd_dec = False697 f1_taesd_enc = False698 f1_taesd_dec = False699 700 for v in approx_vaes:701 if v.startswith("taesd_decoder."):702 sd1_taesd_dec = True703 elif v.startswith("taesd_encoder."):704 sd1_taesd_enc = True705 elif v.startswith("taesdxl_decoder."):706 sdxl_taesd_dec = True707 elif v.startswith("taesdxl_encoder."):708 sdxl_taesd_enc = True709 elif v.startswith("taesd3_decoder."):710 sd3_taesd_dec = True711 elif v.startswith("taesd3_encoder."):712 sd3_taesd_enc = True713 elif v.startswith("taef1_encoder."):714 f1_taesd_dec = True715 elif v.startswith("taef1_decoder."):716 f1_taesd_enc = True717 if sd1_taesd_dec and sd1_taesd_enc:718 vaes.append("taesd")719 if sdxl_taesd_dec and sdxl_taesd_enc:720 vaes.append("taesdxl")721 if sd3_taesd_dec and sd3_taesd_enc:722 vaes.append("taesd3")723 if f1_taesd_dec and f1_taesd_enc:724 vaes.append("taef1")725 return vaes726 727 @staticmethod728 def load_taesd(name):729 sd = {}730 approx_vaes = folder_paths.get_filename_list("vae_approx")731 732 encoder = next(filter(lambda a: a.startswith("{}_encoder.".format(name)), approx_vaes))733 decoder = next(filter(lambda a: a.startswith("{}_decoder.".format(name)), approx_vaes))734 735 enc = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", encoder))736 for k in enc:737 sd["taesd_encoder.{}".format(k)] = enc[k]738 739 dec = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", decoder))740 for k in dec:741 sd["taesd_decoder.{}".format(k)] = dec[k]742 743 if name == "taesd":744 sd["vae_scale"] = torch.tensor(0.18215)745 sd["vae_shift"] = torch.tensor(0.0)746 elif name == "taesdxl":747 sd["vae_scale"] = torch.tensor(0.13025)748 sd["vae_shift"] = torch.tensor(0.0)749 elif name == "taesd3":750 sd["vae_scale"] = torch.tensor(1.5305)751 sd["vae_shift"] = torch.tensor(0.0609)752 elif name == "taef1":753 sd["vae_scale"] = torch.tensor(0.3611)754 sd["vae_shift"] = torch.tensor(0.1159)755 return sd756 757 @classmethod758 def INPUT_TYPES(s):759 return {"required": { "vae_name": (s.vae_list(), )}}760 RETURN_TYPES = ("VAE",)761 FUNCTION = "load_vae"762 763 CATEGORY = "loaders"764 765 #TODO: scale factor?766 def load_vae(self, vae_name):767 if vae_name in ["taesd", "taesdxl", "taesd3", "taef1"]:768 sd = self.load_taesd(vae_name)769 else:770 vae_path = folder_paths.get_full_path_or_raise("vae", vae_name)771 sd = comfy.utils.load_torch_file(vae_path)772 vae = comfy.sd.VAE(sd=sd)773 vae.throw_exception_if_invalid()774 return (vae,)775 776class ControlNetLoader:777 @classmethod778 def INPUT_TYPES(s):779 return {"required": { "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}780 781 RETURN_TYPES = ("CONTROL_NET",)782 FUNCTION = "load_controlnet"783 784 CATEGORY = "loaders"785 786 def load_controlnet(self, control_net_name):787 controlnet_path = folder_paths.get_full_path_or_raise("controlnet", control_net_name)788 controlnet = comfy.controlnet.load_controlnet(controlnet_path)789 return (controlnet,)790 791class DiffControlNetLoader:792 @classmethod793 def INPUT_TYPES(s):794 return {"required": { "model": ("MODEL",),795 "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}796 797 RETURN_TYPES = ("CONTROL_NET",)798 FUNCTION = "load_controlnet"799 800 CATEGORY = "loaders"801 802 def load_controlnet(self, model, control_net_name):803 controlnet_path = folder_paths.get_full_path_or_raise("controlnet", control_net_name)804 controlnet = comfy.controlnet.load_controlnet(controlnet_path, model)805 return (controlnet,)806 807 808class ControlNetApply:809 @classmethod810 def INPUT_TYPES(s):811 return {"required": {"conditioning": ("CONDITIONING", ),812 "control_net": ("CONTROL_NET", ),813 "image": ("IMAGE", ),814 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})815 }}816 RETURN_TYPES = ("CONDITIONING",)817 FUNCTION = "apply_controlnet"818 819 DEPRECATED = True820 CATEGORY = "conditioning/controlnet"821 822 def apply_controlnet(self, conditioning, control_net, image, strength):823 if strength == 0:824 return (conditioning, )825 826 c = []827 control_hint = image.movedim(-1,1)828 for t in conditioning:829 n = [t[0], t[1].copy()]830 c_net = control_net.copy().set_cond_hint(control_hint, strength)831 if 'control' in t[1]:832 c_net.set_previous_controlnet(t[1]['control'])833 n[1]['control'] = c_net834 n[1]['control_apply_to_uncond'] = True835 c.append(n)836 return (c, )837 838 839class ControlNetApplyAdvanced:840 @classmethod841 def INPUT_TYPES(s):842 return {"required": {"positive": ("CONDITIONING", ),843 "negative": ("CONDITIONING", ),844 "control_net": ("CONTROL_NET", ),845 "image": ("IMAGE", ),846 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),847 "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),848 "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})849 },850 "optional": {"vae": ("VAE", ),851 }852 }853 854 RETURN_TYPES = ("CONDITIONING","CONDITIONING")855 RETURN_NAMES = ("positive", "negative")856 FUNCTION = "apply_controlnet"857 858 CATEGORY = "conditioning/controlnet"859 860 def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent, vae=None, extra_concat=[]):861 if strength == 0:862 return (positive, negative)863 864 control_hint = image.movedim(-1,1)865 cnets = {}866 867 out = []868 for conditioning in [positive, negative]:869 c = []870 for t in conditioning:871 d = t[1].copy()872 873 prev_cnet = d.get('control', None)874 if prev_cnet in cnets:875 c_net = cnets[prev_cnet]876 else:877 c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent), vae=vae, extra_concat=extra_concat)878 c_net.set_previous_controlnet(prev_cnet)879 cnets[prev_cnet] = c_net880 881 d['control'] = c_net882 d['control_apply_to_uncond'] = False883 n = [t[0], d]884 c.append(n)885 out.append(c)886 return (out[0], out[1])887 888 889class UNETLoader:890 @classmethod891 def INPUT_TYPES(s):892 return {"required": { "unet_name": (folder_paths.get_filename_list("diffusion_models"), ),893 "weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],)894 }}895 RETURN_TYPES = ("MODEL",)896 FUNCTION = "load_unet"897 898 CATEGORY = "advanced/loaders"899 900 def load_unet(self, unet_name, weight_dtype):901 model_options = {}902 if weight_dtype == "fp8_e4m3fn":903 model_options["dtype"] = torch.float8_e4m3fn904 elif weight_dtype == "fp8_e4m3fn_fast":905 model_options["dtype"] = torch.float8_e4m3fn906 model_options["fp8_optimizations"] = True907 elif weight_dtype == "fp8_e5m2":908 model_options["dtype"] = torch.float8_e5m2909 910 unet_path = folder_paths.get_full_path_or_raise("diffusion_models", unet_name)911 model = comfy.sd.load_diffusion_model(unet_path, model_options=model_options)912 return (model,)913 914class CLIPLoader:915 @classmethod916 def INPUT_TYPES(s):917 return {"required": { "clip_name": (folder_paths.get_filename_list("text_encoders"), ),918 "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan"], ),919 },920 "optional": {921 "device": (["default", "cpu"], {"advanced": True}),922 }}923 RETURN_TYPES = ("CLIP",)924 FUNCTION = "load_clip"925 926 CATEGORY = "advanced/loaders"927 928 DESCRIPTION = "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 xxl/ clip-g / clip-l\nstable_audio: t5 base\nmochi: t5 xxl\ncosmos: old t5 xxl\nlumina2: gemma 2 2B\nwan: umt5 xxl"929 930 def load_clip(self, clip_name, type="stable_diffusion", device="default"):931 if type == "stable_cascade":932 clip_type = comfy.sd.CLIPType.STABLE_CASCADE933 elif type == "sd3":934 clip_type = comfy.sd.CLIPType.SD3935 elif type == "stable_audio":936 clip_type = comfy.sd.CLIPType.STABLE_AUDIO937 elif type == "mochi":938 clip_type = comfy.sd.CLIPType.MOCHI939 elif type == "ltxv":940 clip_type = comfy.sd.CLIPType.LTXV941 elif type == "pixart":942 clip_type = comfy.sd.CLIPType.PIXART943 elif type == "cosmos":944 clip_type = comfy.sd.CLIPType.COSMOS945 elif type == "lumina2":946 clip_type = comfy.sd.CLIPType.LUMINA2947 elif type == "wan":948 clip_type = comfy.sd.CLIPType.WAN949 else:950 clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION951 952 model_options = {}953 if device == "cpu":954 model_options["load_device"] = model_options["offload_device"] = torch.device("cpu")955 956 clip_path = folder_paths.get_full_path_or_raise("text_encoders", clip_name)957 clip = comfy.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type, model_options=model_options)958 return (clip,)959 960class DualCLIPLoader:961 @classmethod962 def INPUT_TYPES(s):963 return {"required": { "clip_name1": (folder_paths.get_filename_list("text_encoders"), ),964 "clip_name2": (folder_paths.get_filename_list("text_encoders"), ),965 "type": (["sdxl", "sd3", "flux", "hunyuan_video"], ),966 },967 "optional": {968 "device": (["default", "cpu"], {"advanced": True}),969 }}970 RETURN_TYPES = ("CLIP",)971 FUNCTION = "load_clip"972 973 CATEGORY = "advanced/loaders"974 975 DESCRIPTION = "[Recipes]\n\nsdxl: clip-l, clip-g\nsd3: clip-l, clip-g / clip-l, t5 / clip-g, t5\nflux: clip-l, t5"976 977 def load_clip(self, clip_name1, clip_name2, type, device="default"):978 clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)979 clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)980 if type == "sdxl":981 clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION982 elif type == "sd3":983 clip_type = comfy.sd.CLIPType.SD3984 elif type == "flux":985 clip_type = comfy.sd.CLIPType.FLUX986 elif type == "hunyuan_video":987 clip_type = comfy.sd.CLIPType.HUNYUAN_VIDEO988 989 model_options = {}990 if device == "cpu":991 model_options["load_device"] = model_options["offload_device"] = torch.device("cpu")992 993 clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type, model_options=model_options)994 return (clip,)995 996class CLIPVisionLoader:997 @classmethod998 def INPUT_TYPES(s):999 return {"required": { "clip_name": (folder_paths.get_filename_list("clip_vision"), ),1000 }}1001 RETURN_TYPES = ("CLIP_VISION",)1002 FUNCTION = "load_clip"1003 1004 CATEGORY = "loaders"1005 1006 def load_clip(self, clip_name):1007 clip_path = folder_paths.get_full_path_or_raise("clip_vision", clip_name)1008 clip_vision = comfy.clip_vision.load(clip_path)1009 return (clip_vision,)1010 1011class CLIPVisionEncode:1012 @classmethod1013 def INPUT_TYPES(s):1014 return {"required": { "clip_vision": ("CLIP_VISION",),1015 "image": ("IMAGE",),1016 "crop": (["center", "none"],)1017 }}1018 RETURN_TYPES = ("CLIP_VISION_OUTPUT",)1019 FUNCTION = "encode"1020 1021 CATEGORY = "conditioning"1022 1023 def encode(self, clip_vision, image, crop):1024 crop_image = True1025 if crop != "center":1026 crop_image = False1027 output = clip_vision.encode_image(image, crop=crop_image)1028 return (output,)1029 1030class StyleModelLoader:1031 @classmethod1032 def INPUT_TYPES(s):1033 return {"required": { "style_model_name": (folder_paths.get_filename_list("style_models"), )}}1034 1035 RETURN_TYPES = ("STYLE_MODEL",)1036 FUNCTION = "load_style_model"1037 1038 CATEGORY = "loaders"1039 1040 def load_style_model(self, style_model_name):1041 style_model_path = folder_paths.get_full_path_or_raise("style_models", style_model_name)1042 style_model = comfy.sd.load_style_model(style_model_path)1043 return (style_model,)1044 1045 1046class StyleModelApply:1047 @classmethod1048 def INPUT_TYPES(s):1049 return {"required": {"conditioning": ("CONDITIONING", ),1050 "style_model": ("STYLE_MODEL", ),1051 "clip_vision_output": ("CLIP_VISION_OUTPUT", ),1052 "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}),1053 "strength_type": (["multiply", "attn_bias"], ),1054 }}1055 RETURN_TYPES = ("CONDITIONING",)1056 FUNCTION = "apply_stylemodel"1057 1058 CATEGORY = "conditioning/style_model"1059 1060 def apply_stylemodel(self, conditioning, style_model, clip_vision_output, strength, strength_type):1061 cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0)1062 if strength_type == "multiply":1063 cond *= strength1064 1065 n = cond.shape[1]1066 c_out = []1067 for t in conditioning:1068 (txt, keys) = t1069 keys = keys.copy()1070 # even if the strength is 1.0 (i.e, no change), if there's already a mask, we have to add to it1071 if "attention_mask" in keys or (strength_type == "attn_bias" and strength != 1.0):1072 # math.log raises an error if the argument is zero1073 # torch.log returns -inf, which is what we want1074 attn_bias = torch.log(torch.Tensor([strength if strength_type == "attn_bias" else 1.0]))1075 # get the size of the mask image1076 mask_ref_size = keys.get("attention_mask_img_shape", (1, 1))1077 n_ref = mask_ref_size[0] * mask_ref_size[1]1078 n_txt = txt.shape[1]1079 # grab the existing mask1080 mask = keys.get("attention_mask", None)1081 # create a default mask if it doesn't exist1082 if mask is None:1083 mask = torch.zeros((txt.shape[0], n_txt + n_ref, n_txt + n_ref), dtype=torch.float16)1084 # convert the mask dtype, because it might be boolean1085 # we want it to be interpreted as a bias1086 if mask.dtype == torch.bool:1087 # log(True) = log(1) = 01088 # log(False) = log(0) = -inf1089 mask = torch.log(mask.to(dtype=torch.float16))1090 # now we make the mask bigger to add space for our new tokens1091 new_mask = torch.zeros((txt.shape[0], n_txt + n + n_ref, n_txt + n + n_ref), dtype=torch.float16)1092 # copy over the old mask, in quandrants1093 new_mask[:, :n_txt, :n_txt] = mask[:, :n_txt, :n_txt]1094 new_mask[:, :n_txt, n_txt+n:] = mask[:, :n_txt, n_txt:]1095 new_mask[:, n_txt+n:, :n_txt] = mask[:, n_txt:, :n_txt]1096 new_mask[:, n_txt+n:, n_txt+n:] = mask[:, n_txt:, n_txt:]1097 # now fill in the attention bias to our redux tokens1098 new_mask[:, :n_txt, n_txt:n_txt+n] = attn_bias1099 new_mask[:, n_txt+n:, n_txt:n_txt+n] = attn_bias1100 keys["attention_mask"] = new_mask.to(txt.device)1101 keys["attention_mask_img_shape"] = mask_ref_size1102 1103 c_out.append([torch.cat((txt, cond), dim=1), keys])1104 1105 return (c_out,)1106 1107class unCLIPConditioning:1108 @classmethod1109 def INPUT_TYPES(s):1110 return {"required": {"conditioning": ("CONDITIONING", ),1111 "clip_vision_output": ("CLIP_VISION_OUTPUT", ),1112 "strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),1113 "noise_augmentation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),1114 }}1115 RETURN_TYPES = ("CONDITIONING",)1116 FUNCTION = "apply_adm"1117 1118 CATEGORY = "conditioning"1119 1120 def apply_adm(self, conditioning, clip_vision_output, strength, noise_augmentation):1121 if strength == 0:1122 return (conditioning, )1123 1124 c = []1125 for t in conditioning:1126 o = t[1].copy()1127 x = {"clip_vision_output": clip_vision_output, "strength": strength, "noise_augmentation": noise_augmentation}1128 if "unclip_conditioning" in o:1129 o["unclip_conditioning"] = o["unclip_conditioning"][:] + [x]1130 else:1131 o["unclip_conditioning"] = [x]1132 n = [t[0], o]1133 c.append(n)1134 return (c, )1135 1136class GLIGENLoader:1137 @classmethod1138 def INPUT_TYPES(s):1139 return {"required": { "gligen_name": (folder_paths.get_filename_list("gligen"), )}}1140 1141 RETURN_TYPES = ("GLIGEN",)1142 FUNCTION = "load_gligen"1143 1144 CATEGORY = "loaders"1145 1146 def load_gligen(self, gligen_name):1147 gligen_path = folder_paths.get_full_path_or_raise("gligen", gligen_name)1148 gligen = comfy.sd.load_gligen(gligen_path)1149 return (gligen,)1150 1151class GLIGENTextBoxApply:1152 @classmethod1153 def INPUT_TYPES(s):1154 return {"required": {"conditioning_to": ("CONDITIONING", ),1155 "clip": ("CLIP", ),1156 "gligen_textbox_model": ("GLIGEN", ),1157 "text": ("STRING", {"multiline": True, "dynamicPrompts": True}),1158 "width": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),1159 "height": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),1160 "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),1161 "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),1162 }}1163 RETURN_TYPES = ("CONDITIONING",)1164 FUNCTION = "append"1165 1166 CATEGORY = "conditioning/gligen"1167 1168 def append(self, conditioning_to, clip, gligen_textbox_model, text, width, height, x, y):1169 c = []1170 cond, cond_pooled = clip.encode_from_tokens(clip.tokenize(text), return_pooled="unprojected")1171 for t in conditioning_to:1172 n = [t[0], t[1].copy()]1173 position_params = [(cond_pooled, height // 8, width // 8, y // 8, x // 8)]1174 prev = []1175 if "gligen" in n[1]:1176 prev = n[1]['gligen'][2]1177 1178 n[1]['gligen'] = ("position", gligen_textbox_model, prev + position_params)1179 c.append(n)1180 return (c, )1181 1182class EmptyLatentImage:1183 def __init__(self):1184 self.device = comfy.model_management.intermediate_device()1185 1186 @classmethod1187 def INPUT_TYPES(s):1188 return {1189 "required": {1190 "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The width of the latent images in pixels."}),1191 "height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The height of the latent images in pixels."}),1192 "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."})1193 }1194 }1195 RETURN_TYPES = ("LATENT",)1196 OUTPUT_TOOLTIPS = ("The empty latent image batch.",)1197 FUNCTION = "generate"1198 1199 CATEGORY = "latent"1200 DESCRIPTION = "Create a new batch of empty latent images to be denoised via sampling."