SuperRealCo commited on
Commit
b97a97f
·
verified ·
1 Parent(s): 47be4e7

Delete nodes.py

Browse files
Files changed (1) hide show
  1. nodes.py +0 -2363
nodes.py DELETED
@@ -1,2363 +0,0 @@
1
- from __future__ import annotations
2
- import torch
3
-
4
- import os
5
- import sys
6
- import json
7
- import hashlib
8
- import traceback
9
- import math
10
- import time
11
- import random
12
- import logging
13
-
14
- from PIL import Image, ImageOps, ImageSequence
15
- from PIL.PngImagePlugin import PngInfo
16
-
17
- import numpy as np
18
- import safetensors.torch
19
-
20
- sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
21
-
22
- import comfy.diffusers_load
23
- import comfy.samplers
24
- import comfy.sample
25
- import comfy.sd
26
- import comfy.utils
27
- import comfy.controlnet
28
- from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict, FileLocator
29
-
30
- import comfy.clip_vision
31
-
32
- import comfy.model_management
33
- from comfy.cli_args import args
34
-
35
- import importlib
36
-
37
- import folder_paths
38
- import latent_preview
39
- import node_helpers
40
-
41
- def before_node_execution():
42
- comfy.model_management.throw_exception_if_processing_interrupted()
43
-
44
- def interrupt_processing(value=True):
45
- comfy.model_management.interrupt_current_processing(value)
46
-
47
- MAX_RESOLUTION=16384
48
-
49
- class CLIPTextEncode(ComfyNodeABC):
50
- @classmethod
51
- 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
-
72
- class ConditioningCombine:
73
- @classmethod
74
- 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
-
84
- class ConditioningAverage :
85
- @classmethod
86
- 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_from
117
-
118
- n = [tw, t_to]
119
- out.append(n)
120
- return (out, )
121
-
122
- class ConditioningConcat:
123
- @classmethod
124
- 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
-
150
- class ConditioningSetArea:
151
- @classmethod
152
- 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
-
171
- class ConditioningSetAreaPercentage:
172
- @classmethod
173
- 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
-
192
- class ConditioningSetAreaStrength:
193
- @classmethod
194
- 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
-
208
- class ConditioningSetMask:
209
- @classmethod
210
- 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 = False
223
- if set_cond_area != "default":
224
- set_area_to_bounds = True
225
- 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
-
233
- class ConditioningZeroOut:
234
- @classmethod
235
- 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
- conditioning_lyrics = d.get("conditioning_lyrics", None)
250
- if conditioning_lyrics is not None:
251
- d["conditioning_lyrics"] = torch.zeros_like(conditioning_lyrics)
252
- n = [torch.zeros_like(t[0]), d]
253
- c.append(n)
254
- return (c, )
255
-
256
- class ConditioningSetTimestepRange:
257
- @classmethod
258
- def INPUT_TYPES(s):
259
- return {"required": {"conditioning": ("CONDITIONING", ),
260
- "start": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
261
- "end": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
262
- }}
263
- RETURN_TYPES = ("CONDITIONING",)
264
- FUNCTION = "set_range"
265
-
266
- CATEGORY = "advanced/conditioning"
267
-
268
- def set_range(self, conditioning, start, end):
269
- c = node_helpers.conditioning_set_values(conditioning, {"start_percent": start,
270
- "end_percent": end})
271
- return (c, )
272
-
273
- class VAEDecode:
274
- @classmethod
275
- def INPUT_TYPES(s):
276
- return {
277
- "required": {
278
- "samples": ("LATENT", {"tooltip": "The latent to be decoded."}),
279
- "vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."})
280
- }
281
- }
282
- RETURN_TYPES = ("IMAGE",)
283
- OUTPUT_TOOLTIPS = ("The decoded image.",)
284
- FUNCTION = "decode"
285
-
286
- CATEGORY = "latent"
287
- DESCRIPTION = "Decodes latent images back into pixel space images."
288
-
289
- def decode(self, vae, samples):
290
- images = vae.decode(samples["samples"])
291
- if len(images.shape) == 5: #Combine batches
292
- images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
293
- return (images, )
294
-
295
- class VAEDecodeTiled:
296
- @classmethod
297
- def INPUT_TYPES(s):
298
- return {"required": {"samples": ("LATENT", ), "vae": ("VAE", ),
299
- "tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
300
- "overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),
301
- "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."}),
302
- "temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),
303
- }}
304
- RETURN_TYPES = ("IMAGE",)
305
- FUNCTION = "decode"
306
-
307
- CATEGORY = "_for_testing"
308
-
309
- def decode(self, vae, samples, tile_size, overlap=64, temporal_size=64, temporal_overlap=8):
310
- if tile_size < overlap * 4:
311
- overlap = tile_size // 4
312
- if temporal_size < temporal_overlap * 2:
313
- temporal_overlap = temporal_overlap // 2
314
- temporal_compression = vae.temporal_compression_decode()
315
- if temporal_compression is not None:
316
- temporal_size = max(2, temporal_size // temporal_compression)
317
- temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression))
318
- else:
319
- temporal_size = None
320
- temporal_overlap = None
321
-
322
- compression = vae.spacial_compression_decode()
323
- 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)
324
- if len(images.shape) == 5: #Combine batches
325
- images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
326
- return (images, )
327
-
328
- class VAEEncode:
329
- @classmethod
330
- def INPUT_TYPES(s):
331
- return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", )}}
332
- RETURN_TYPES = ("LATENT",)
333
- FUNCTION = "encode"
334
-
335
- CATEGORY = "latent"
336
-
337
- def encode(self, vae, pixels):
338
- t = vae.encode(pixels[:,:,:,:3])
339
- return ({"samples":t}, )
340
-
341
- class VAEEncodeTiled:
342
- @classmethod
343
- def INPUT_TYPES(s):
344
- return {"required": {"pixels": ("IMAGE", ), "vae": ("VAE", ),
345
- "tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
346
- "overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),
347
- "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."}),
348
- "temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),
349
- }}
350
- RETURN_TYPES = ("LATENT",)
351
- FUNCTION = "encode"
352
-
353
- CATEGORY = "_for_testing"
354
-
355
- def encode(self, vae, pixels, tile_size, overlap, temporal_size=64, temporal_overlap=8):
356
- t = vae.encode_tiled(pixels[:,:,:,:3], tile_x=tile_size, tile_y=tile_size, overlap=overlap, tile_t=temporal_size, overlap_t=temporal_overlap)
357
- return ({"samples": t}, )
358
-
359
- class VAEEncodeForInpaint:
360
- @classmethod
361
- def INPUT_TYPES(s):
362
- return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ), "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),}}
363
- RETURN_TYPES = ("LATENT",)
364
- FUNCTION = "encode"
365
-
366
- CATEGORY = "latent/inpaint"
367
-
368
- def encode(self, vae, pixels, mask, grow_mask_by=6):
369
- x = (pixels.shape[1] // vae.downscale_ratio) * vae.downscale_ratio
370
- y = (pixels.shape[2] // vae.downscale_ratio) * vae.downscale_ratio
371
- mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")
372
-
373
- pixels = pixels.clone()
374
- if pixels.shape[1] != x or pixels.shape[2] != y:
375
- x_offset = (pixels.shape[1] % vae.downscale_ratio) // 2
376
- y_offset = (pixels.shape[2] % vae.downscale_ratio) // 2
377
- pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]
378
- mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]
379
-
380
- #grow mask by a few pixels to keep things seamless in latent space
381
- if grow_mask_by == 0:
382
- mask_erosion = mask
383
- else:
384
- kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))
385
- padding = math.ceil((grow_mask_by - 1) / 2)
386
-
387
- mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1)
388
-
389
- m = (1.0 - mask.round()).squeeze(1)
390
- for i in range(3):
391
- pixels[:,:,:,i] -= 0.5
392
- pixels[:,:,:,i] *= m
393
- pixels[:,:,:,i] += 0.5
394
- t = vae.encode(pixels)
395
-
396
- return ({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, )
397
-
398
-
399
- class InpaintModelConditioning:
400
- @classmethod
401
- def INPUT_TYPES(s):
402
- return {"required": {"positive": ("CONDITIONING", ),
403
- "negative": ("CONDITIONING", ),
404
- "vae": ("VAE", ),
405
- "pixels": ("IMAGE", ),
406
- "mask": ("MASK", ),
407
- "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."}),
408
- }}
409
-
410
- RETURN_TYPES = ("CONDITIONING","CONDITIONING","LATENT")
411
- RETURN_NAMES = ("positive", "negative", "latent")
412
- FUNCTION = "encode"
413
-
414
- CATEGORY = "conditioning/inpaint"
415
-
416
- def encode(self, positive, negative, pixels, vae, mask, noise_mask=True):
417
- x = (pixels.shape[1] // 8) * 8
418
- y = (pixels.shape[2] // 8) * 8
419
- mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")
420
-
421
- orig_pixels = pixels
422
- pixels = orig_pixels.clone()
423
- if pixels.shape[1] != x or pixels.shape[2] != y:
424
- x_offset = (pixels.shape[1] % 8) // 2
425
- y_offset = (pixels.shape[2] % 8) // 2
426
- pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:]
427
- mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset]
428
-
429
- m = (1.0 - mask.round()).squeeze(1)
430
- for i in range(3):
431
- pixels[:,:,:,i] -= 0.5
432
- pixels[:,:,:,i] *= m
433
- pixels[:,:,:,i] += 0.5
434
- concat_latent = vae.encode(pixels)
435
- orig_latent = vae.encode(orig_pixels)
436
-
437
- out_latent = {}
438
-
439
- out_latent["samples"] = orig_latent
440
- if noise_mask:
441
- out_latent["noise_mask"] = mask
442
-
443
- out = []
444
- for conditioning in [positive, negative]:
445
- c = node_helpers.conditioning_set_values(conditioning, {"concat_latent_image": concat_latent,
446
- "concat_mask": mask})
447
- out.append(c)
448
- return (out[0], out[1], out_latent)
449
-
450
-
451
- class SaveLatent:
452
- def __init__(self):
453
- self.output_dir = folder_paths.get_output_directory()
454
-
455
- @classmethod
456
- def INPUT_TYPES(s):
457
- return {"required": { "samples": ("LATENT", ),
458
- "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})},
459
- "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
460
- }
461
- RETURN_TYPES = ()
462
- FUNCTION = "save"
463
-
464
- OUTPUT_NODE = True
465
-
466
- CATEGORY = "_for_testing"
467
-
468
- def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
469
- full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
470
-
471
- # support save metadata for latent sharing
472
- prompt_info = ""
473
- if prompt is not None:
474
- prompt_info = json.dumps(prompt)
475
-
476
- metadata = None
477
- if not args.disable_metadata:
478
- metadata = {"prompt": prompt_info}
479
- if extra_pnginfo is not None:
480
- for x in extra_pnginfo:
481
- metadata[x] = json.dumps(extra_pnginfo[x])
482
-
483
- file = f"{filename}_{counter:05}_.latent"
484
-
485
- results: list[FileLocator] = []
486
- results.append({
487
- "filename": file,
488
- "subfolder": subfolder,
489
- "type": "output"
490
- })
491
-
492
- file = os.path.join(full_output_folder, file)
493
-
494
- output = {}
495
- output["latent_tensor"] = samples["samples"].contiguous()
496
- output["latent_format_version_0"] = torch.tensor([])
497
-
498
- comfy.utils.save_torch_file(output, file, metadata=metadata)
499
- return { "ui": { "latents": results } }
500
-
501
-
502
- class LoadLatent:
503
- @classmethod
504
- def INPUT_TYPES(s):
505
- input_dir = folder_paths.get_input_directory()
506
- files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".latent")]
507
- return {"required": {"latent": [sorted(files), ]}, }
508
-
509
- CATEGORY = "_for_testing"
510
-
511
- RETURN_TYPES = ("LATENT", )
512
- FUNCTION = "load"
513
-
514
- def load(self, latent):
515
- latent_path = folder_paths.get_annotated_filepath(latent)
516
- latent = safetensors.torch.load_file(latent_path, device="cpu")
517
- multiplier = 1.0
518
- if "latent_format_version_0" not in latent:
519
- multiplier = 1.0 / 0.18215
520
- samples = {"samples": latent["latent_tensor"].float() * multiplier}
521
- return (samples, )
522
-
523
- @classmethod
524
- def IS_CHANGED(s, latent):
525
- image_path = folder_paths.get_annotated_filepath(latent)
526
- m = hashlib.sha256()
527
- with open(image_path, 'rb') as f:
528
- m.update(f.read())
529
- return m.digest().hex()
530
-
531
- @classmethod
532
- def VALIDATE_INPUTS(s, latent):
533
- if not folder_paths.exists_annotated_filepath(latent):
534
- return "Invalid latent file: {}".format(latent)
535
- return True
536
-
537
-
538
- class CheckpointLoader:
539
- @classmethod
540
- def INPUT_TYPES(s):
541
- return {"required": { "config_name": (folder_paths.get_filename_list("configs"), ),
542
- "ckpt_name": (folder_paths.get_filename_list("checkpoints"), )}}
543
- RETURN_TYPES = ("MODEL", "CLIP", "VAE")
544
- FUNCTION = "load_checkpoint"
545
-
546
- CATEGORY = "advanced/loaders"
547
- DEPRECATED = True
548
-
549
- def load_checkpoint(self, config_name, ckpt_name):
550
- config_path = folder_paths.get_full_path("configs", config_name)
551
- ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
552
- return comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
553
-
554
- class CheckpointLoaderSimple:
555
- @classmethod
556
- def INPUT_TYPES(s):
557
- return {
558
- "required": {
559
- "ckpt_name": (folder_paths.get_filename_list("checkpoints"), {"tooltip": "The name of the checkpoint (model) to load."}),
560
- }
561
- }
562
- RETURN_TYPES = ("MODEL", "CLIP", "VAE")
563
- OUTPUT_TOOLTIPS = ("The model used for denoising latents.",
564
- "The CLIP model used for encoding text prompts.",
565
- "The VAE model used for encoding and decoding images to and from latent space.")
566
- FUNCTION = "load_checkpoint"
567
-
568
- CATEGORY = "loaders"
569
- DESCRIPTION = "Loads a diffusion model checkpoint, diffusion models are used to denoise latents."
570
-
571
- def load_checkpoint(self, ckpt_name):
572
- ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
573
- out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
574
- return out[:3]
575
-
576
- class DiffusersLoader:
577
- @classmethod
578
- def INPUT_TYPES(cls):
579
- paths = []
580
- for search_path in folder_paths.get_folder_paths("diffusers"):
581
- if os.path.exists(search_path):
582
- for root, subdir, files in os.walk(search_path, followlinks=True):
583
- if "model_index.json" in files:
584
- paths.append(os.path.relpath(root, start=search_path))
585
-
586
- return {"required": {"model_path": (paths,), }}
587
- RETURN_TYPES = ("MODEL", "CLIP", "VAE")
588
- FUNCTION = "load_checkpoint"
589
-
590
- CATEGORY = "advanced/loaders/deprecated"
591
-
592
- def load_checkpoint(self, model_path, output_vae=True, output_clip=True):
593
- for search_path in folder_paths.get_folder_paths("diffusers"):
594
- if os.path.exists(search_path):
595
- path = os.path.join(search_path, model_path)
596
- if os.path.exists(path):
597
- model_path = path
598
- break
599
-
600
- return comfy.diffusers_load.load_diffusers(model_path, output_vae=output_vae, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
601
-
602
-
603
- class unCLIPCheckpointLoader:
604
- @classmethod
605
- def INPUT_TYPES(s):
606
- return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
607
- }}
608
- RETURN_TYPES = ("MODEL", "CLIP", "VAE", "CLIP_VISION")
609
- FUNCTION = "load_checkpoint"
610
-
611
- CATEGORY = "loaders"
612
-
613
- def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):
614
- ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
615
- 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"))
616
- return out
617
-
618
- class CLIPSetLastLayer:
619
- @classmethod
620
- def INPUT_TYPES(s):
621
- return {"required": { "clip": ("CLIP", ),
622
- "stop_at_clip_layer": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),
623
- }}
624
- RETURN_TYPES = ("CLIP",)
625
- FUNCTION = "set_last_layer"
626
-
627
- CATEGORY = "conditioning"
628
-
629
- def set_last_layer(self, clip, stop_at_clip_layer):
630
- clip = clip.clone()
631
- clip.clip_layer(stop_at_clip_layer)
632
- return (clip,)
633
-
634
- class LoraLoader:
635
- def __init__(self):
636
- self.loaded_lora = None
637
-
638
- @classmethod
639
- def INPUT_TYPES(s):
640
- return {
641
- "required": {
642
- "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
643
- "clip": ("CLIP", {"tooltip": "The CLIP model the LoRA will be applied to."}),
644
- "lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}),
645
- "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."}),
646
- "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."}),
647
- }
648
- }
649
-
650
- RETURN_TYPES = ("MODEL", "CLIP")
651
- OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.")
652
- FUNCTION = "load_lora"
653
-
654
- CATEGORY = "loaders"
655
- 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."
656
-
657
- def load_lora(self, model, clip, lora_name, strength_model, strength_clip):
658
- if strength_model == 0 and strength_clip == 0:
659
- return (model, clip)
660
-
661
- lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
662
- lora = None
663
- if self.loaded_lora is not None:
664
- if self.loaded_lora[0] == lora_path:
665
- lora = self.loaded_lora[1]
666
- else:
667
- self.loaded_lora = None
668
-
669
- if lora is None:
670
- lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
671
- self.loaded_lora = (lora_path, lora)
672
-
673
- model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
674
- return (model_lora, clip_lora)
675
-
676
- class LoraLoaderModelOnly(LoraLoader):
677
- @classmethod
678
- def INPUT_TYPES(s):
679
- return {"required": { "model": ("MODEL",),
680
- "lora_name": (folder_paths.get_filename_list("loras"), ),
681
- "strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),
682
- }}
683
- RETURN_TYPES = ("MODEL",)
684
- FUNCTION = "load_lora_model_only"
685
-
686
- def load_lora_model_only(self, model, lora_name, strength_model):
687
- return (self.load_lora(model, None, lora_name, strength_model, 0)[0],)
688
-
689
- class VAELoader:
690
- @staticmethod
691
- def vae_list():
692
- vaes = folder_paths.get_filename_list("vae")
693
- approx_vaes = folder_paths.get_filename_list("vae_approx")
694
- sdxl_taesd_enc = False
695
- sdxl_taesd_dec = False
696
- sd1_taesd_enc = False
697
- sd1_taesd_dec = False
698
- sd3_taesd_enc = False
699
- sd3_taesd_dec = False
700
- f1_taesd_enc = False
701
- f1_taesd_dec = False
702
-
703
- for v in approx_vaes:
704
- if v.startswith("taesd_decoder."):
705
- sd1_taesd_dec = True
706
- elif v.startswith("taesd_encoder."):
707
- sd1_taesd_enc = True
708
- elif v.startswith("taesdxl_decoder."):
709
- sdxl_taesd_dec = True
710
- elif v.startswith("taesdxl_encoder."):
711
- sdxl_taesd_enc = True
712
- elif v.startswith("taesd3_decoder."):
713
- sd3_taesd_dec = True
714
- elif v.startswith("taesd3_encoder."):
715
- sd3_taesd_enc = True
716
- elif v.startswith("taef1_encoder."):
717
- f1_taesd_dec = True
718
- elif v.startswith("taef1_decoder."):
719
- f1_taesd_enc = True
720
- if sd1_taesd_dec and sd1_taesd_enc:
721
- vaes.append("taesd")
722
- if sdxl_taesd_dec and sdxl_taesd_enc:
723
- vaes.append("taesdxl")
724
- if sd3_taesd_dec and sd3_taesd_enc:
725
- vaes.append("taesd3")
726
- if f1_taesd_dec and f1_taesd_enc:
727
- vaes.append("taef1")
728
- return vaes
729
-
730
- @staticmethod
731
- def load_taesd(name):
732
- sd = {}
733
- approx_vaes = folder_paths.get_filename_list("vae_approx")
734
-
735
- encoder = next(filter(lambda a: a.startswith("{}_encoder.".format(name)), approx_vaes))
736
- decoder = next(filter(lambda a: a.startswith("{}_decoder.".format(name)), approx_vaes))
737
-
738
- enc = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", encoder))
739
- for k in enc:
740
- sd["taesd_encoder.{}".format(k)] = enc[k]
741
-
742
- dec = comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", decoder))
743
- for k in dec:
744
- sd["taesd_decoder.{}".format(k)] = dec[k]
745
-
746
- if name == "taesd":
747
- sd["vae_scale"] = torch.tensor(0.18215)
748
- sd["vae_shift"] = torch.tensor(0.0)
749
- elif name == "taesdxl":
750
- sd["vae_scale"] = torch.tensor(0.13025)
751
- sd["vae_shift"] = torch.tensor(0.0)
752
- elif name == "taesd3":
753
- sd["vae_scale"] = torch.tensor(1.5305)
754
- sd["vae_shift"] = torch.tensor(0.0609)
755
- elif name == "taef1":
756
- sd["vae_scale"] = torch.tensor(0.3611)
757
- sd["vae_shift"] = torch.tensor(0.1159)
758
- return sd
759
-
760
- @classmethod
761
- def INPUT_TYPES(s):
762
- return {"required": { "vae_name": (s.vae_list(), )}}
763
- RETURN_TYPES = ("VAE",)
764
- FUNCTION = "load_vae"
765
-
766
- CATEGORY = "loaders"
767
-
768
- #TODO: scale factor?
769
- def load_vae(self, vae_name):
770
- if vae_name in ["taesd", "taesdxl", "taesd3", "taef1"]:
771
- sd = self.load_taesd(vae_name)
772
- else:
773
- vae_path = folder_paths.get_full_path_or_raise("vae", vae_name)
774
- sd = comfy.utils.load_torch_file(vae_path)
775
- vae = comfy.sd.VAE(sd=sd)
776
- vae.throw_exception_if_invalid()
777
- return (vae,)
778
-
779
- class ControlNetLoader:
780
- @classmethod
781
- def INPUT_TYPES(s):
782
- return {"required": { "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}
783
-
784
- RETURN_TYPES = ("CONTROL_NET",)
785
- FUNCTION = "load_controlnet"
786
-
787
- CATEGORY = "loaders"
788
-
789
- def load_controlnet(self, control_net_name):
790
- controlnet_path = folder_paths.get_full_path_or_raise("controlnet", control_net_name)
791
- controlnet = comfy.controlnet.load_controlnet(controlnet_path)
792
- if controlnet is None:
793
- raise RuntimeError("ERROR: controlnet file is invalid and does not contain a valid controlnet model.")
794
- return (controlnet,)
795
-
796
- class DiffControlNetLoader:
797
- @classmethod
798
- def INPUT_TYPES(s):
799
- return {"required": { "model": ("MODEL",),
800
- "control_net_name": (folder_paths.get_filename_list("controlnet"), )}}
801
-
802
- RETURN_TYPES = ("CONTROL_NET",)
803
- FUNCTION = "load_controlnet"
804
-
805
- CATEGORY = "loaders"
806
-
807
- def load_controlnet(self, model, control_net_name):
808
- controlnet_path = folder_paths.get_full_path_or_raise("controlnet", control_net_name)
809
- controlnet = comfy.controlnet.load_controlnet(controlnet_path, model)
810
- return (controlnet,)
811
-
812
-
813
- class ControlNetApply:
814
- @classmethod
815
- def INPUT_TYPES(s):
816
- return {"required": {"conditioning": ("CONDITIONING", ),
817
- "control_net": ("CONTROL_NET", ),
818
- "image": ("IMAGE", ),
819
- "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
820
- }}
821
- RETURN_TYPES = ("CONDITIONING",)
822
- FUNCTION = "apply_controlnet"
823
-
824
- DEPRECATED = True
825
- CATEGORY = "conditioning/controlnet"
826
-
827
- def apply_controlnet(self, conditioning, control_net, image, strength):
828
- if strength == 0:
829
- return (conditioning, )
830
-
831
- c = []
832
- control_hint = image.movedim(-1,1)
833
- for t in conditioning:
834
- n = [t[0], t[1].copy()]
835
- c_net = control_net.copy().set_cond_hint(control_hint, strength)
836
- if 'control' in t[1]:
837
- c_net.set_previous_controlnet(t[1]['control'])
838
- n[1]['control'] = c_net
839
- n[1]['control_apply_to_uncond'] = True
840
- c.append(n)
841
- return (c, )
842
-
843
-
844
- class ControlNetApplyAdvanced:
845
- @classmethod
846
- def INPUT_TYPES(s):
847
- return {"required": {"positive": ("CONDITIONING", ),
848
- "negative": ("CONDITIONING", ),
849
- "control_net": ("CONTROL_NET", ),
850
- "image": ("IMAGE", ),
851
- "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
852
- "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
853
- "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
854
- },
855
- "optional": {"vae": ("VAE", ),
856
- }
857
- }
858
-
859
- RETURN_TYPES = ("CONDITIONING","CONDITIONING")
860
- RETURN_NAMES = ("positive", "negative")
861
- FUNCTION = "apply_controlnet"
862
-
863
- CATEGORY = "conditioning/controlnet"
864
-
865
- def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent, vae=None, extra_concat=[]):
866
- if strength == 0:
867
- return (positive, negative)
868
-
869
- control_hint = image.movedim(-1,1)
870
- cnets = {}
871
-
872
- out = []
873
- for conditioning in [positive, negative]:
874
- c = []
875
- for t in conditioning:
876
- d = t[1].copy()
877
-
878
- prev_cnet = d.get('control', None)
879
- if prev_cnet in cnets:
880
- c_net = cnets[prev_cnet]
881
- else:
882
- c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent), vae=vae, extra_concat=extra_concat)
883
- c_net.set_previous_controlnet(prev_cnet)
884
- cnets[prev_cnet] = c_net
885
-
886
- d['control'] = c_net
887
- d['control_apply_to_uncond'] = False
888
- n = [t[0], d]
889
- c.append(n)
890
- out.append(c)
891
- return (out[0], out[1])
892
-
893
-
894
- class UNETLoader:
895
- @classmethod
896
- def INPUT_TYPES(s):
897
- return {"required": { "unet_name": (folder_paths.get_filename_list("diffusion_models"), ),
898
- "weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],)
899
- }}
900
- RETURN_TYPES = ("MODEL",)
901
- FUNCTION = "load_unet"
902
-
903
- CATEGORY = "advanced/loaders"
904
-
905
- def load_unet(self, unet_name, weight_dtype):
906
- model_options = {}
907
- if weight_dtype == "fp8_e4m3fn":
908
- model_options["dtype"] = torch.float8_e4m3fn
909
- elif weight_dtype == "fp8_e4m3fn_fast":
910
- model_options["dtype"] = torch.float8_e4m3fn
911
- model_options["fp8_optimizations"] = True
912
- elif weight_dtype == "fp8_e5m2":
913
- model_options["dtype"] = torch.float8_e5m2
914
-
915
- unet_path = folder_paths.get_full_path_or_raise("diffusion_models", unet_name)
916
- model = comfy.sd.load_diffusion_model(unet_path, model_options=model_options)
917
- return (model,)
918
-
919
- class CLIPLoader:
920
- @classmethod
921
- def INPUT_TYPES(s):
922
- return {"required": { "clip_name": (folder_paths.get_filename_list("text_encoders"), ),
923
- "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2"], ),
924
- },
925
- "optional": {
926
- "device": (["default", "cpu"], {"advanced": True}),
927
- }}
928
- RETURN_TYPES = ("CLIP",)
929
- FUNCTION = "load_clip"
930
-
931
- CATEGORY = "advanced/loaders"
932
-
933
- 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\n hidream: llama-3.1 (Recommend) or t5\nomnigen2: qwen vl 2.5 3B"
934
-
935
- def load_clip(self, clip_name, type="stable_diffusion", device="default"):
936
- clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION)
937
-
938
- model_options = {}
939
- if device == "cpu":
940
- model_options["load_device"] = model_options["offload_device"] = torch.device("cpu")
941
-
942
- clip_path = folder_paths.get_full_path_or_raise("text_encoders", clip_name)
943
- 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)
944
- return (clip,)
945
-
946
- class DualCLIPLoader:
947
- @classmethod
948
- def INPUT_TYPES(s):
949
- return {"required": { "clip_name1": (folder_paths.get_filename_list("text_encoders"), ),
950
- "clip_name2": (folder_paths.get_filename_list("text_encoders"), ),
951
- "type": (["sdxl", "sd3", "flux", "hunyuan_video", "hidream"], ),
952
- },
953
- "optional": {
954
- "device": (["default", "cpu"], {"advanced": True}),
955
- }}
956
- RETURN_TYPES = ("CLIP",)
957
- FUNCTION = "load_clip"
958
-
959
- CATEGORY = "advanced/loaders"
960
-
961
- DESCRIPTION = "[Recipes]\n\nsdxl: clip-l, clip-g\nsd3: clip-l, clip-g / clip-l, t5 / clip-g, t5\nflux: clip-l, t5\nhidream: at least one of t5 or llama, recommended t5 and llama"
962
-
963
- def load_clip(self, clip_name1, clip_name2, type, device="default"):
964
- clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION)
965
-
966
- clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)
967
- clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)
968
-
969
- model_options = {}
970
- if device == "cpu":
971
- model_options["load_device"] = model_options["offload_device"] = torch.device("cpu")
972
-
973
- 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)
974
- return (clip,)
975
-
976
- class CLIPVisionLoader:
977
- @classmethod
978
- def INPUT_TYPES(s):
979
- return {"required": { "clip_name": (folder_paths.get_filename_list("clip_vision"), ),
980
- }}
981
- RETURN_TYPES = ("CLIP_VISION",)
982
- FUNCTION = "load_clip"
983
-
984
- CATEGORY = "loaders"
985
-
986
- def load_clip(self, clip_name):
987
- clip_path = folder_paths.get_full_path_or_raise("clip_vision", clip_name)
988
- clip_vision = comfy.clip_vision.load(clip_path)
989
- if clip_vision is None:
990
- raise RuntimeError("ERROR: clip vision file is invalid and does not contain a valid vision model.")
991
- return (clip_vision,)
992
-
993
- class CLIPVisionEncode:
994
- @classmethod
995
- def INPUT_TYPES(s):
996
- return {"required": { "clip_vision": ("CLIP_VISION",),
997
- "image": ("IMAGE",),
998
- "crop": (["center", "none"],)
999
- }}
1000
- RETURN_TYPES = ("CLIP_VISION_OUTPUT",)
1001
- FUNCTION = "encode"
1002
-
1003
- CATEGORY = "conditioning"
1004
-
1005
- def encode(self, clip_vision, image, crop):
1006
- crop_image = True
1007
- if crop != "center":
1008
- crop_image = False
1009
- output = clip_vision.encode_image(image, crop=crop_image)
1010
- return (output,)
1011
-
1012
- class StyleModelLoader:
1013
- @classmethod
1014
- def INPUT_TYPES(s):
1015
- return {"required": { "style_model_name": (folder_paths.get_filename_list("style_models"), )}}
1016
-
1017
- RETURN_TYPES = ("STYLE_MODEL",)
1018
- FUNCTION = "load_style_model"
1019
-
1020
- CATEGORY = "loaders"
1021
-
1022
- def load_style_model(self, style_model_name):
1023
- style_model_path = folder_paths.get_full_path_or_raise("style_models", style_model_name)
1024
- style_model = comfy.sd.load_style_model(style_model_path)
1025
- return (style_model,)
1026
-
1027
-
1028
- class StyleModelApply:
1029
- @classmethod
1030
- def INPUT_TYPES(s):
1031
- return {"required": {"conditioning": ("CONDITIONING", ),
1032
- "style_model": ("STYLE_MODEL", ),
1033
- "clip_vision_output": ("CLIP_VISION_OUTPUT", ),
1034
- "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}),
1035
- "strength_type": (["multiply", "attn_bias"], ),
1036
- }}
1037
- RETURN_TYPES = ("CONDITIONING",)
1038
- FUNCTION = "apply_stylemodel"
1039
-
1040
- CATEGORY = "conditioning/style_model"
1041
-
1042
- def apply_stylemodel(self, conditioning, style_model, clip_vision_output, strength, strength_type):
1043
- cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0)
1044
- if strength_type == "multiply":
1045
- cond *= strength
1046
-
1047
- n = cond.shape[1]
1048
- c_out = []
1049
- for t in conditioning:
1050
- (txt, keys) = t
1051
- keys = keys.copy()
1052
- # even if the strength is 1.0 (i.e, no change), if there's already a mask, we have to add to it
1053
- if "attention_mask" in keys or (strength_type == "attn_bias" and strength != 1.0):
1054
- # math.log raises an error if the argument is zero
1055
- # torch.log returns -inf, which is what we want
1056
- attn_bias = torch.log(torch.Tensor([strength if strength_type == "attn_bias" else 1.0]))
1057
- # get the size of the mask image
1058
- mask_ref_size = keys.get("attention_mask_img_shape", (1, 1))
1059
- n_ref = mask_ref_size[0] * mask_ref_size[1]
1060
- n_txt = txt.shape[1]
1061
- # grab the existing mask
1062
- mask = keys.get("attention_mask", None)
1063
- # create a default mask if it doesn't exist
1064
- if mask is None:
1065
- mask = torch.zeros((txt.shape[0], n_txt + n_ref, n_txt + n_ref), dtype=torch.float16)
1066
- # convert the mask dtype, because it might be boolean
1067
- # we want it to be interpreted as a bias
1068
- if mask.dtype == torch.bool:
1069
- # log(True) = log(1) = 0
1070
- # log(False) = log(0) = -inf
1071
- mask = torch.log(mask.to(dtype=torch.float16))
1072
- # now we make the mask bigger to add space for our new tokens
1073
- new_mask = torch.zeros((txt.shape[0], n_txt + n + n_ref, n_txt + n + n_ref), dtype=torch.float16)
1074
- # copy over the old mask, in quandrants
1075
- new_mask[:, :n_txt, :n_txt] = mask[:, :n_txt, :n_txt]
1076
- new_mask[:, :n_txt, n_txt+n:] = mask[:, :n_txt, n_txt:]
1077
- new_mask[:, n_txt+n:, :n_txt] = mask[:, n_txt:, :n_txt]
1078
- new_mask[:, n_txt+n:, n_txt+n:] = mask[:, n_txt:, n_txt:]
1079
- # now fill in the attention bias to our redux tokens
1080
- new_mask[:, :n_txt, n_txt:n_txt+n] = attn_bias
1081
- new_mask[:, n_txt+n:, n_txt:n_txt+n] = attn_bias
1082
- keys["attention_mask"] = new_mask.to(txt.device)
1083
- keys["attention_mask_img_shape"] = mask_ref_size
1084
-
1085
- c_out.append([torch.cat((txt, cond), dim=1), keys])
1086
-
1087
- return (c_out,)
1088
-
1089
- class unCLIPConditioning:
1090
- @classmethod
1091
- def INPUT_TYPES(s):
1092
- return {"required": {"conditioning": ("CONDITIONING", ),
1093
- "clip_vision_output": ("CLIP_VISION_OUTPUT", ),
1094
- "strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
1095
- "noise_augmentation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
1096
- }}
1097
- RETURN_TYPES = ("CONDITIONING",)
1098
- FUNCTION = "apply_adm"
1099
-
1100
- CATEGORY = "conditioning"
1101
-
1102
- def apply_adm(self, conditioning, clip_vision_output, strength, noise_augmentation):
1103
- if strength == 0:
1104
- return (conditioning, )
1105
-
1106
- c = node_helpers.conditioning_set_values(conditioning, {"unclip_conditioning": [{"clip_vision_output": clip_vision_output, "strength": strength, "noise_augmentation": noise_augmentation}]}, append=True)
1107
- return (c, )
1108
-
1109
- class GLIGENLoader:
1110
- @classmethod
1111
- def INPUT_TYPES(s):
1112
- return {"required": { "gligen_name": (folder_paths.get_filename_list("gligen"), )}}
1113
-
1114
- RETURN_TYPES = ("GLIGEN",)
1115
- FUNCTION = "load_gligen"
1116
-
1117
- CATEGORY = "loaders"
1118
-
1119
- def load_gligen(self, gligen_name):
1120
- gligen_path = folder_paths.get_full_path_or_raise("gligen", gligen_name)
1121
- gligen = comfy.sd.load_gligen(gligen_path)
1122
- return (gligen,)
1123
-
1124
- class GLIGENTextBoxApply:
1125
- @classmethod
1126
- def INPUT_TYPES(s):
1127
- return {"required": {"conditioning_to": ("CONDITIONING", ),
1128
- "clip": ("CLIP", ),
1129
- "gligen_textbox_model": ("GLIGEN", ),
1130
- "text": ("STRING", {"multiline": True, "dynamicPrompts": True}),
1131
- "width": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),
1132
- "height": ("INT", {"default": 64, "min": 8, "max": MAX_RESOLUTION, "step": 8}),
1133
- "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1134
- "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1135
- }}
1136
- RETURN_TYPES = ("CONDITIONING",)
1137
- FUNCTION = "append"
1138
-
1139
- CATEGORY = "conditioning/gligen"
1140
-
1141
- def append(self, conditioning_to, clip, gligen_textbox_model, text, width, height, x, y):
1142
- c = []
1143
- cond, cond_pooled = clip.encode_from_tokens(clip.tokenize(text), return_pooled="unprojected")
1144
- for t in conditioning_to:
1145
- n = [t[0], t[1].copy()]
1146
- position_params = [(cond_pooled, height // 8, width // 8, y // 8, x // 8)]
1147
- prev = []
1148
- if "gligen" in n[1]:
1149
- prev = n[1]['gligen'][2]
1150
-
1151
- n[1]['gligen'] = ("position", gligen_textbox_model, prev + position_params)
1152
- c.append(n)
1153
- return (c, )
1154
-
1155
- class EmptyLatentImage:
1156
- def __init__(self):
1157
- self.device = comfy.model_management.intermediate_device()
1158
-
1159
- @classmethod
1160
- def INPUT_TYPES(s):
1161
- return {
1162
- "required": {
1163
- "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The width of the latent images in pixels."}),
1164
- "height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "tooltip": "The height of the latent images in pixels."}),
1165
- "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."})
1166
- }
1167
- }
1168
- RETURN_TYPES = ("LATENT",)
1169
- OUTPUT_TOOLTIPS = ("The empty latent image batch.",)
1170
- FUNCTION = "generate"
1171
-
1172
- CATEGORY = "latent"
1173
- DESCRIPTION = "Create a new batch of empty latent images to be denoised via sampling."
1174
-
1175
- def generate(self, width, height, batch_size=1):
1176
- latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
1177
- return ({"samples":latent}, )
1178
-
1179
-
1180
- class LatentFromBatch:
1181
- @classmethod
1182
- def INPUT_TYPES(s):
1183
- return {"required": { "samples": ("LATENT",),
1184
- "batch_index": ("INT", {"default": 0, "min": 0, "max": 63}),
1185
- "length": ("INT", {"default": 1, "min": 1, "max": 64}),
1186
- }}
1187
- RETURN_TYPES = ("LATENT",)
1188
- FUNCTION = "frombatch"
1189
-
1190
- CATEGORY = "latent/batch"
1191
-
1192
- def frombatch(self, samples, batch_index, length):
1193
- s = samples.copy()
1194
- s_in = samples["samples"]
1195
- batch_index = min(s_in.shape[0] - 1, batch_index)
1196
- length = min(s_in.shape[0] - batch_index, length)
1197
- s["samples"] = s_in[batch_index:batch_index + length].clone()
1198
- if "noise_mask" in samples:
1199
- masks = samples["noise_mask"]
1200
- if masks.shape[0] == 1:
1201
- s["noise_mask"] = masks.clone()
1202
- else:
1203
- if masks.shape[0] < s_in.shape[0]:
1204
- masks = masks.repeat(math.ceil(s_in.shape[0] / masks.shape[0]), 1, 1, 1)[:s_in.shape[0]]
1205
- s["noise_mask"] = masks[batch_index:batch_index + length].clone()
1206
- if "batch_index" not in s:
1207
- s["batch_index"] = [x for x in range(batch_index, batch_index+length)]
1208
- else:
1209
- s["batch_index"] = samples["batch_index"][batch_index:batch_index + length]
1210
- return (s,)
1211
-
1212
- class RepeatLatentBatch:
1213
- @classmethod
1214
- def INPUT_TYPES(s):
1215
- return {"required": { "samples": ("LATENT",),
1216
- "amount": ("INT", {"default": 1, "min": 1, "max": 64}),
1217
- }}
1218
- RETURN_TYPES = ("LATENT",)
1219
- FUNCTION = "repeat"
1220
-
1221
- CATEGORY = "latent/batch"
1222
-
1223
- def repeat(self, samples, amount):
1224
- s = samples.copy()
1225
- s_in = samples["samples"]
1226
-
1227
- s["samples"] = s_in.repeat((amount, 1,1,1))
1228
- if "noise_mask" in samples and samples["noise_mask"].shape[0] > 1:
1229
- masks = samples["noise_mask"]
1230
- if masks.shape[0] < s_in.shape[0]:
1231
- masks = masks.repeat(math.ceil(s_in.shape[0] / masks.shape[0]), 1, 1, 1)[:s_in.shape[0]]
1232
- s["noise_mask"] = samples["noise_mask"].repeat((amount, 1,1,1))
1233
- if "batch_index" in s:
1234
- offset = max(s["batch_index"]) - min(s["batch_index"]) + 1
1235
- s["batch_index"] = s["batch_index"] + [x + (i * offset) for i in range(1, amount) for x in s["batch_index"]]
1236
- return (s,)
1237
-
1238
- class LatentUpscale:
1239
- upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
1240
- crop_methods = ["disabled", "center"]
1241
-
1242
- @classmethod
1243
- def INPUT_TYPES(s):
1244
- return {"required": { "samples": ("LATENT",), "upscale_method": (s.upscale_methods,),
1245
- "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1246
- "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1247
- "crop": (s.crop_methods,)}}
1248
- RETURN_TYPES = ("LATENT",)
1249
- FUNCTION = "upscale"
1250
-
1251
- CATEGORY = "latent"
1252
-
1253
- def upscale(self, samples, upscale_method, width, height, crop):
1254
- if width == 0 and height == 0:
1255
- s = samples
1256
- else:
1257
- s = samples.copy()
1258
-
1259
- if width == 0:
1260
- height = max(64, height)
1261
- width = max(64, round(samples["samples"].shape[-1] * height / samples["samples"].shape[-2]))
1262
- elif height == 0:
1263
- width = max(64, width)
1264
- height = max(64, round(samples["samples"].shape[-2] * width / samples["samples"].shape[-1]))
1265
- else:
1266
- width = max(64, width)
1267
- height = max(64, height)
1268
-
1269
- s["samples"] = comfy.utils.common_upscale(samples["samples"], width // 8, height // 8, upscale_method, crop)
1270
- return (s,)
1271
-
1272
- class LatentUpscaleBy:
1273
- upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
1274
-
1275
- @classmethod
1276
- def INPUT_TYPES(s):
1277
- return {"required": { "samples": ("LATENT",), "upscale_method": (s.upscale_methods,),
1278
- "scale_by": ("FLOAT", {"default": 1.5, "min": 0.01, "max": 8.0, "step": 0.01}),}}
1279
- RETURN_TYPES = ("LATENT",)
1280
- FUNCTION = "upscale"
1281
-
1282
- CATEGORY = "latent"
1283
-
1284
- def upscale(self, samples, upscale_method, scale_by):
1285
- s = samples.copy()
1286
- width = round(samples["samples"].shape[-1] * scale_by)
1287
- height = round(samples["samples"].shape[-2] * scale_by)
1288
- s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, "disabled")
1289
- return (s,)
1290
-
1291
- class LatentRotate:
1292
- @classmethod
1293
- def INPUT_TYPES(s):
1294
- return {"required": { "samples": ("LATENT",),
1295
- "rotation": (["none", "90 degrees", "180 degrees", "270 degrees"],),
1296
- }}
1297
- RETURN_TYPES = ("LATENT",)
1298
- FUNCTION = "rotate"
1299
-
1300
- CATEGORY = "latent/transform"
1301
-
1302
- def rotate(self, samples, rotation):
1303
- s = samples.copy()
1304
- rotate_by = 0
1305
- if rotation.startswith("90"):
1306
- rotate_by = 1
1307
- elif rotation.startswith("180"):
1308
- rotate_by = 2
1309
- elif rotation.startswith("270"):
1310
- rotate_by = 3
1311
-
1312
- s["samples"] = torch.rot90(samples["samples"], k=rotate_by, dims=[3, 2])
1313
- return (s,)
1314
-
1315
- class LatentFlip:
1316
- @classmethod
1317
- def INPUT_TYPES(s):
1318
- return {"required": { "samples": ("LATENT",),
1319
- "flip_method": (["x-axis: vertically", "y-axis: horizontally"],),
1320
- }}
1321
- RETURN_TYPES = ("LATENT",)
1322
- FUNCTION = "flip"
1323
-
1324
- CATEGORY = "latent/transform"
1325
-
1326
- def flip(self, samples, flip_method):
1327
- s = samples.copy()
1328
- if flip_method.startswith("x"):
1329
- s["samples"] = torch.flip(samples["samples"], dims=[2])
1330
- elif flip_method.startswith("y"):
1331
- s["samples"] = torch.flip(samples["samples"], dims=[3])
1332
-
1333
- return (s,)
1334
-
1335
- class LatentComposite:
1336
- @classmethod
1337
- def INPUT_TYPES(s):
1338
- return {"required": { "samples_to": ("LATENT",),
1339
- "samples_from": ("LATENT",),
1340
- "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1341
- "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1342
- "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1343
- }}
1344
- RETURN_TYPES = ("LATENT",)
1345
- FUNCTION = "composite"
1346
-
1347
- CATEGORY = "latent"
1348
-
1349
- def composite(self, samples_to, samples_from, x, y, composite_method="normal", feather=0):
1350
- x = x // 8
1351
- y = y // 8
1352
- feather = feather // 8
1353
- samples_out = samples_to.copy()
1354
- s = samples_to["samples"].clone()
1355
- samples_to = samples_to["samples"]
1356
- samples_from = samples_from["samples"]
1357
- if feather == 0:
1358
- s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
1359
- else:
1360
- samples_from = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x]
1361
- mask = torch.ones_like(samples_from)
1362
- for t in range(feather):
1363
- if y != 0:
1364
- mask[:,:,t:1+t,:] *= ((1.0/feather) * (t + 1))
1365
-
1366
- if y + samples_from.shape[2] < samples_to.shape[2]:
1367
- mask[:,:,mask.shape[2] -1 -t: mask.shape[2]-t,:] *= ((1.0/feather) * (t + 1))
1368
- if x != 0:
1369
- mask[:,:,:,t:1+t] *= ((1.0/feather) * (t + 1))
1370
- if x + samples_from.shape[3] < samples_to.shape[3]:
1371
- mask[:,:,:,mask.shape[3]- 1 - t: mask.shape[3]- t] *= ((1.0/feather) * (t + 1))
1372
- rev_mask = torch.ones_like(mask) - mask
1373
- s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] * mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] * rev_mask
1374
- samples_out["samples"] = s
1375
- return (samples_out,)
1376
-
1377
- class LatentBlend:
1378
- @classmethod
1379
- def INPUT_TYPES(s):
1380
- return {"required": {
1381
- "samples1": ("LATENT",),
1382
- "samples2": ("LATENT",),
1383
- "blend_factor": ("FLOAT", {
1384
- "default": 0.5,
1385
- "min": 0,
1386
- "max": 1,
1387
- "step": 0.01
1388
- }),
1389
- }}
1390
-
1391
- RETURN_TYPES = ("LATENT",)
1392
- FUNCTION = "blend"
1393
-
1394
- CATEGORY = "_for_testing"
1395
-
1396
- def blend(self, samples1, samples2, blend_factor:float, blend_mode: str="normal"):
1397
-
1398
- samples_out = samples1.copy()
1399
- samples1 = samples1["samples"]
1400
- samples2 = samples2["samples"]
1401
-
1402
- if samples1.shape != samples2.shape:
1403
- samples2.permute(0, 3, 1, 2)
1404
- samples2 = comfy.utils.common_upscale(samples2, samples1.shape[3], samples1.shape[2], 'bicubic', crop='center')
1405
- samples2.permute(0, 2, 3, 1)
1406
-
1407
- samples_blended = self.blend_mode(samples1, samples2, blend_mode)
1408
- samples_blended = samples1 * blend_factor + samples_blended * (1 - blend_factor)
1409
- samples_out["samples"] = samples_blended
1410
- return (samples_out,)
1411
-
1412
- def blend_mode(self, img1, img2, mode):
1413
- if mode == "normal":
1414
- return img2
1415
- else:
1416
- raise ValueError(f"Unsupported blend mode: {mode}")
1417
-
1418
- class LatentCrop:
1419
- @classmethod
1420
- def INPUT_TYPES(s):
1421
- return {"required": { "samples": ("LATENT",),
1422
- "width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
1423
- "height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
1424
- "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1425
- "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1426
- }}
1427
- RETURN_TYPES = ("LATENT",)
1428
- FUNCTION = "crop"
1429
-
1430
- CATEGORY = "latent/transform"
1431
-
1432
- def crop(self, samples, width, height, x, y):
1433
- s = samples.copy()
1434
- samples = samples['samples']
1435
- x = x // 8
1436
- y = y // 8
1437
-
1438
- #enfonce minimum size of 64
1439
- if x > (samples.shape[3] - 8):
1440
- x = samples.shape[3] - 8
1441
- if y > (samples.shape[2] - 8):
1442
- y = samples.shape[2] - 8
1443
-
1444
- new_height = height // 8
1445
- new_width = width // 8
1446
- to_x = new_width + x
1447
- to_y = new_height + y
1448
- s['samples'] = samples[:,:,y:to_y, x:to_x]
1449
- return (s,)
1450
-
1451
- class SetLatentNoiseMask:
1452
- @classmethod
1453
- def INPUT_TYPES(s):
1454
- return {"required": { "samples": ("LATENT",),
1455
- "mask": ("MASK",),
1456
- }}
1457
- RETURN_TYPES = ("LATENT",)
1458
- FUNCTION = "set_mask"
1459
-
1460
- CATEGORY = "latent/inpaint"
1461
-
1462
- def set_mask(self, samples, mask):
1463
- s = samples.copy()
1464
- s["noise_mask"] = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
1465
- return (s,)
1466
-
1467
- def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
1468
- latent_image = latent["samples"]
1469
- latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
1470
-
1471
- if disable_noise:
1472
- noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
1473
- else:
1474
- batch_inds = latent["batch_index"] if "batch_index" in latent else None
1475
- noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
1476
-
1477
- noise_mask = None
1478
- if "noise_mask" in latent:
1479
- noise_mask = latent["noise_mask"]
1480
-
1481
- callback = latent_preview.prepare_callback(model, steps)
1482
- disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
1483
- samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
1484
- denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
1485
- force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed)
1486
- out = latent.copy()
1487
- out["samples"] = samples
1488
- return (out, )
1489
-
1490
- class KSampler:
1491
- @classmethod
1492
- def INPUT_TYPES(s):
1493
- return {
1494
- "required": {
1495
- "model": ("MODEL", {"tooltip": "The model used for denoising the input latent."}),
1496
- "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}),
1497
- "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "The number of steps used in the denoising process."}),
1498
- "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality."}),
1499
- "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The algorithm used when sampling, this can affect the quality, speed, and style of the generated output."}),
1500
- "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"tooltip": "The scheduler controls how noise is gradually removed to form the image."}),
1501
- "positive": ("CONDITIONING", {"tooltip": "The conditioning describing the attributes you want to include in the image."}),
1502
- "negative": ("CONDITIONING", {"tooltip": "The conditioning describing the attributes you want to exclude from the image."}),
1503
- "latent_image": ("LATENT", {"tooltip": "The latent image to denoise."}),
1504
- "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The amount of denoising applied, lower values will maintain the structure of the initial image allowing for image to image sampling."}),
1505
- }
1506
- }
1507
-
1508
- RETURN_TYPES = ("LATENT",)
1509
- OUTPUT_TOOLTIPS = ("The denoised latent.",)
1510
- FUNCTION = "sample"
1511
-
1512
- CATEGORY = "sampling"
1513
- DESCRIPTION = "Uses the provided model, positive and negative conditioning to denoise the latent image."
1514
-
1515
- def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0):
1516
- return common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)
1517
-
1518
- class KSamplerAdvanced:
1519
- @classmethod
1520
- def INPUT_TYPES(s):
1521
- return {"required":
1522
- {"model": ("MODEL",),
1523
- "add_noise": (["enable", "disable"], ),
1524
- "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
1525
- "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
1526
- "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
1527
- "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
1528
- "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
1529
- "positive": ("CONDITIONING", ),
1530
- "negative": ("CONDITIONING", ),
1531
- "latent_image": ("LATENT", ),
1532
- "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
1533
- "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
1534
- "return_with_leftover_noise": (["disable", "enable"], ),
1535
- }
1536
- }
1537
-
1538
- RETURN_TYPES = ("LATENT",)
1539
- FUNCTION = "sample"
1540
-
1541
- CATEGORY = "sampling"
1542
-
1543
- def sample(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise=1.0):
1544
- force_full_denoise = True
1545
- if return_with_leftover_noise == "enable":
1546
- force_full_denoise = False
1547
- disable_noise = False
1548
- if add_noise == "disable":
1549
- disable_noise = True
1550
- return common_ksampler(model, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
1551
-
1552
- class SaveImage:
1553
- def __init__(self):
1554
- self.output_dir = folder_paths.get_output_directory()
1555
- self.type = "output"
1556
- self.prefix_append = ""
1557
- self.compress_level = 4
1558
-
1559
- @classmethod
1560
- def INPUT_TYPES(s):
1561
- return {
1562
- "required": {
1563
- "images": ("IMAGE", {"tooltip": "The images to save."}),
1564
- "filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
1565
- },
1566
- "hidden": {
1567
- "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
1568
- },
1569
- }
1570
-
1571
- RETURN_TYPES = ()
1572
- FUNCTION = "save_images"
1573
-
1574
- OUTPUT_NODE = True
1575
-
1576
- CATEGORY = "image"
1577
- DESCRIPTION = "Saves the input images to your ComfyUI output directory."
1578
-
1579
- def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
1580
- filename_prefix += self.prefix_append
1581
- full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
1582
- results = list()
1583
- for (batch_number, image) in enumerate(images):
1584
- i = 255. * image.cpu().numpy()
1585
- img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
1586
- metadata = None
1587
- if not args.disable_metadata:
1588
- metadata = PngInfo()
1589
- if prompt is not None:
1590
- metadata.add_text("prompt", json.dumps(prompt))
1591
- if extra_pnginfo is not None:
1592
- for x in extra_pnginfo:
1593
- metadata.add_text(x, json.dumps(extra_pnginfo[x]))
1594
-
1595
- filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
1596
- file = f"{filename_with_batch_num}_{counter:05}_.png"
1597
- img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
1598
- results.append({
1599
- "filename": file,
1600
- "subfolder": subfolder,
1601
- "type": self.type
1602
- })
1603
- counter += 1
1604
-
1605
- return { "ui": { "images": results } }
1606
-
1607
- class PreviewImage(SaveImage):
1608
- def __init__(self):
1609
- self.output_dir = folder_paths.get_temp_directory()
1610
- self.type = "temp"
1611
- self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
1612
- self.compress_level = 1
1613
-
1614
- @classmethod
1615
- def INPUT_TYPES(s):
1616
- return {"required":
1617
- {"images": ("IMAGE", ), },
1618
- "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
1619
- }
1620
-
1621
- class LoadImage:
1622
- @classmethod
1623
- def INPUT_TYPES(s):
1624
- input_dir = folder_paths.get_input_directory()
1625
- files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
1626
- files = folder_paths.filter_files_content_types(files, ["image"])
1627
- return {"required":
1628
- {"image": (sorted(files), {"image_upload": True})},
1629
- }
1630
-
1631
- CATEGORY = "image"
1632
-
1633
- RETURN_TYPES = ("IMAGE", "MASK")
1634
- FUNCTION = "load_image"
1635
- def load_image(self, image):
1636
- image_path = folder_paths.get_annotated_filepath(image)
1637
-
1638
- img = node_helpers.pillow(Image.open, image_path)
1639
-
1640
- output_images = []
1641
- output_masks = []
1642
- w, h = None, None
1643
-
1644
- excluded_formats = ['MPO']
1645
-
1646
- for i in ImageSequence.Iterator(img):
1647
- i = node_helpers.pillow(ImageOps.exif_transpose, i)
1648
-
1649
- if i.mode == 'I':
1650
- i = i.point(lambda i: i * (1 / 255))
1651
- image = i.convert("RGB")
1652
-
1653
- if len(output_images) == 0:
1654
- w = image.size[0]
1655
- h = image.size[1]
1656
-
1657
- if image.size[0] != w or image.size[1] != h:
1658
- continue
1659
-
1660
- image = np.array(image).astype(np.float32) / 255.0
1661
- image = torch.from_numpy(image)[None,]
1662
- if 'A' in i.getbands():
1663
- mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
1664
- mask = 1. - torch.from_numpy(mask)
1665
- elif i.mode == 'P' and 'transparency' in i.info:
1666
- mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
1667
- mask = 1. - torch.from_numpy(mask)
1668
- else:
1669
- mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
1670
- output_images.append(image)
1671
- output_masks.append(mask.unsqueeze(0))
1672
-
1673
- if len(output_images) > 1 and img.format not in excluded_formats:
1674
- output_image = torch.cat(output_images, dim=0)
1675
- output_mask = torch.cat(output_masks, dim=0)
1676
- else:
1677
- output_image = output_images[0]
1678
- output_mask = output_masks[0]
1679
-
1680
- return (output_image, output_mask)
1681
-
1682
- @classmethod
1683
- def IS_CHANGED(s, image):
1684
- image_path = folder_paths.get_annotated_filepath(image)
1685
- m = hashlib.sha256()
1686
- with open(image_path, 'rb') as f:
1687
- m.update(f.read())
1688
- return m.digest().hex()
1689
-
1690
- @classmethod
1691
- def VALIDATE_INPUTS(s, image):
1692
- if not folder_paths.exists_annotated_filepath(image):
1693
- return "Invalid image file: {}".format(image)
1694
-
1695
- return True
1696
-
1697
- class LoadImageMask:
1698
- _color_channels = ["alpha", "red", "green", "blue"]
1699
- @classmethod
1700
- def INPUT_TYPES(s):
1701
- input_dir = folder_paths.get_input_directory()
1702
- files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
1703
- return {"required":
1704
- {"image": (sorted(files), {"image_upload": True}),
1705
- "channel": (s._color_channels, ), }
1706
- }
1707
-
1708
- CATEGORY = "mask"
1709
-
1710
- RETURN_TYPES = ("MASK",)
1711
- FUNCTION = "load_image"
1712
- def load_image(self, image, channel):
1713
- image_path = folder_paths.get_annotated_filepath(image)
1714
- i = node_helpers.pillow(Image.open, image_path)
1715
- i = node_helpers.pillow(ImageOps.exif_transpose, i)
1716
- if i.getbands() != ("R", "G", "B", "A"):
1717
- if i.mode == 'I':
1718
- i = i.point(lambda i: i * (1 / 255))
1719
- i = i.convert("RGBA")
1720
- mask = None
1721
- c = channel[0].upper()
1722
- if c in i.getbands():
1723
- mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
1724
- mask = torch.from_numpy(mask)
1725
- if c == 'A':
1726
- mask = 1. - mask
1727
- else:
1728
- mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
1729
- return (mask.unsqueeze(0),)
1730
-
1731
- @classmethod
1732
- def IS_CHANGED(s, image, channel):
1733
- image_path = folder_paths.get_annotated_filepath(image)
1734
- m = hashlib.sha256()
1735
- with open(image_path, 'rb') as f:
1736
- m.update(f.read())
1737
- return m.digest().hex()
1738
-
1739
- @classmethod
1740
- def VALIDATE_INPUTS(s, image):
1741
- if not folder_paths.exists_annotated_filepath(image):
1742
- return "Invalid image file: {}".format(image)
1743
-
1744
- return True
1745
-
1746
-
1747
- class LoadImageOutput(LoadImage):
1748
- @classmethod
1749
- def INPUT_TYPES(s):
1750
- return {
1751
- "required": {
1752
- "image": ("COMBO", {
1753
- "image_upload": True,
1754
- "image_folder": "output",
1755
- "remote": {
1756
- "route": "/internal/files/output",
1757
- "refresh_button": True,
1758
- "control_after_refresh": "first",
1759
- },
1760
- }),
1761
- }
1762
- }
1763
-
1764
- DESCRIPTION = "Load an image from the output folder. When the refresh button is clicked, the node will update the image list and automatically select the first image, allowing for easy iteration."
1765
- EXPERIMENTAL = True
1766
- FUNCTION = "load_image"
1767
-
1768
-
1769
- class ImageScale:
1770
- upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
1771
- crop_methods = ["disabled", "center"]
1772
-
1773
- @classmethod
1774
- def INPUT_TYPES(s):
1775
- return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
1776
- "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
1777
- "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
1778
- "crop": (s.crop_methods,)}}
1779
- RETURN_TYPES = ("IMAGE",)
1780
- FUNCTION = "upscale"
1781
-
1782
- CATEGORY = "image/upscaling"
1783
-
1784
- def upscale(self, image, upscale_method, width, height, crop):
1785
- if width == 0 and height == 0:
1786
- s = image
1787
- else:
1788
- samples = image.movedim(-1,1)
1789
-
1790
- if width == 0:
1791
- width = max(1, round(samples.shape[3] * height / samples.shape[2]))
1792
- elif height == 0:
1793
- height = max(1, round(samples.shape[2] * width / samples.shape[3]))
1794
-
1795
- s = comfy.utils.common_upscale(samples, width, height, upscale_method, crop)
1796
- s = s.movedim(1,-1)
1797
- return (s,)
1798
-
1799
- class ImageScaleBy:
1800
- upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
1801
-
1802
- @classmethod
1803
- def INPUT_TYPES(s):
1804
- return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
1805
- "scale_by": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 8.0, "step": 0.01}),}}
1806
- RETURN_TYPES = ("IMAGE",)
1807
- FUNCTION = "upscale"
1808
-
1809
- CATEGORY = "image/upscaling"
1810
-
1811
- def upscale(self, image, upscale_method, scale_by):
1812
- samples = image.movedim(-1,1)
1813
- width = round(samples.shape[3] * scale_by)
1814
- height = round(samples.shape[2] * scale_by)
1815
- s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
1816
- s = s.movedim(1,-1)
1817
- return (s,)
1818
-
1819
- class ImageInvert:
1820
-
1821
- @classmethod
1822
- def INPUT_TYPES(s):
1823
- return {"required": { "image": ("IMAGE",)}}
1824
-
1825
- RETURN_TYPES = ("IMAGE",)
1826
- FUNCTION = "invert"
1827
-
1828
- CATEGORY = "image"
1829
-
1830
- def invert(self, image):
1831
- s = 1.0 - image
1832
- return (s,)
1833
-
1834
- class ImageBatch:
1835
-
1836
- @classmethod
1837
- def INPUT_TYPES(s):
1838
- return {"required": { "image1": ("IMAGE",), "image2": ("IMAGE",)}}
1839
-
1840
- RETURN_TYPES = ("IMAGE",)
1841
- FUNCTION = "batch"
1842
-
1843
- CATEGORY = "image"
1844
-
1845
- def batch(self, image1, image2):
1846
- if image1.shape[1:] != image2.shape[1:]:
1847
- image2 = comfy.utils.common_upscale(image2.movedim(-1,1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1,-1)
1848
- s = torch.cat((image1, image2), dim=0)
1849
- return (s,)
1850
-
1851
- class EmptyImage:
1852
- def __init__(self, device="cpu"):
1853
- self.device = device
1854
-
1855
- @classmethod
1856
- def INPUT_TYPES(s):
1857
- return {"required": { "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
1858
- "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
1859
- "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
1860
- "color": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFF, "step": 1, "display": "color"}),
1861
- }}
1862
- RETURN_TYPES = ("IMAGE",)
1863
- FUNCTION = "generate"
1864
-
1865
- CATEGORY = "image"
1866
-
1867
- def generate(self, width, height, batch_size=1, color=0):
1868
- r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF)
1869
- g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF)
1870
- b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
1871
- return (torch.cat((r, g, b), dim=-1), )
1872
-
1873
- class ImagePadForOutpaint:
1874
-
1875
- @classmethod
1876
- def INPUT_TYPES(s):
1877
- return {
1878
- "required": {
1879
- "image": ("IMAGE",),
1880
- "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1881
- "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1882
- "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1883
- "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
1884
- "feathering": ("INT", {"default": 40, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
1885
- }
1886
- }
1887
-
1888
- RETURN_TYPES = ("IMAGE", "MASK")
1889
- FUNCTION = "expand_image"
1890
-
1891
- CATEGORY = "image"
1892
-
1893
- def expand_image(self, image, left, top, right, bottom, feathering):
1894
- d1, d2, d3, d4 = image.size()
1895
-
1896
- new_image = torch.ones(
1897
- (d1, d2 + top + bottom, d3 + left + right, d4),
1898
- dtype=torch.float32,
1899
- ) * 0.5
1900
-
1901
- new_image[:, top:top + d2, left:left + d3, :] = image
1902
-
1903
- mask = torch.ones(
1904
- (d2 + top + bottom, d3 + left + right),
1905
- dtype=torch.float32,
1906
- )
1907
-
1908
- t = torch.zeros(
1909
- (d2, d3),
1910
- dtype=torch.float32
1911
- )
1912
-
1913
- if feathering > 0 and feathering * 2 < d2 and feathering * 2 < d3:
1914
-
1915
- for i in range(d2):
1916
- for j in range(d3):
1917
- dt = i if top != 0 else d2
1918
- db = d2 - i if bottom != 0 else d2
1919
-
1920
- dl = j if left != 0 else d3
1921
- dr = d3 - j if right != 0 else d3
1922
-
1923
- d = min(dt, db, dl, dr)
1924
-
1925
- if d >= feathering:
1926
- continue
1927
-
1928
- v = (feathering - d) / feathering
1929
-
1930
- t[i, j] = v * v
1931
-
1932
- mask[top:top + d2, left:left + d3] = t
1933
-
1934
- return (new_image, mask.unsqueeze(0))
1935
-
1936
-
1937
- NODE_CLASS_MAPPINGS = {
1938
- "KSampler": KSampler,
1939
- "CheckpointLoaderSimple": CheckpointLoaderSimple,
1940
- "CLIPTextEncode": CLIPTextEncode,
1941
- "CLIPSetLastLayer": CLIPSetLastLayer,
1942
- "VAEDecode": VAEDecode,
1943
- "VAEEncode": VAEEncode,
1944
- "VAEEncodeForInpaint": VAEEncodeForInpaint,
1945
- "VAELoader": VAELoader,
1946
- "EmptyLatentImage": EmptyLatentImage,
1947
- "LatentUpscale": LatentUpscale,
1948
- "LatentUpscaleBy": LatentUpscaleBy,
1949
- "LatentFromBatch": LatentFromBatch,
1950
- "RepeatLatentBatch": RepeatLatentBatch,
1951
- "SaveImage": SaveImage,
1952
- "PreviewImage": PreviewImage,
1953
- "LoadImage": LoadImage,
1954
- "LoadImageMask": LoadImageMask,
1955
- "LoadImageOutput": LoadImageOutput,
1956
- "ImageScale": ImageScale,
1957
- "ImageScaleBy": ImageScaleBy,
1958
- "ImageInvert": ImageInvert,
1959
- "ImageBatch": ImageBatch,
1960
- "ImagePadForOutpaint": ImagePadForOutpaint,
1961
- "EmptyImage": EmptyImage,
1962
- "ConditioningAverage": ConditioningAverage ,
1963
- "ConditioningCombine": ConditioningCombine,
1964
- "ConditioningConcat": ConditioningConcat,
1965
- "ConditioningSetArea": ConditioningSetArea,
1966
- "ConditioningSetAreaPercentage": ConditioningSetAreaPercentage,
1967
- "ConditioningSetAreaStrength": ConditioningSetAreaStrength,
1968
- "ConditioningSetMask": ConditioningSetMask,
1969
- "KSamplerAdvanced": KSamplerAdvanced,
1970
- "SetLatentNoiseMask": SetLatentNoiseMask,
1971
- "LatentComposite": LatentComposite,
1972
- "LatentBlend": LatentBlend,
1973
- "LatentRotate": LatentRotate,
1974
- "LatentFlip": LatentFlip,
1975
- "LatentCrop": LatentCrop,
1976
- "LoraLoader": LoraLoader,
1977
- "CLIPLoader": CLIPLoader,
1978
- "UNETLoader": UNETLoader,
1979
- "DualCLIPLoader": DualCLIPLoader,
1980
- "CLIPVisionEncode": CLIPVisionEncode,
1981
- "StyleModelApply": StyleModelApply,
1982
- "unCLIPConditioning": unCLIPConditioning,
1983
- "ControlNetApply": ControlNetApply,
1984
- "ControlNetApplyAdvanced": ControlNetApplyAdvanced,
1985
- "ControlNetLoader": ControlNetLoader,
1986
- "DiffControlNetLoader": DiffControlNetLoader,
1987
- "StyleModelLoader": StyleModelLoader,
1988
- "CLIPVisionLoader": CLIPVisionLoader,
1989
- "VAEDecodeTiled": VAEDecodeTiled,
1990
- "VAEEncodeTiled": VAEEncodeTiled,
1991
- "unCLIPCheckpointLoader": unCLIPCheckpointLoader,
1992
- "GLIGENLoader": GLIGENLoader,
1993
- "GLIGENTextBoxApply": GLIGENTextBoxApply,
1994
- "InpaintModelConditioning": InpaintModelConditioning,
1995
-
1996
- "CheckpointLoader": CheckpointLoader,
1997
- "DiffusersLoader": DiffusersLoader,
1998
-
1999
- "LoadLatent": LoadLatent,
2000
- "SaveLatent": SaveLatent,
2001
-
2002
- "ConditioningZeroOut": ConditioningZeroOut,
2003
- "ConditioningSetTimestepRange": ConditioningSetTimestepRange,
2004
- "LoraLoaderModelOnly": LoraLoaderModelOnly,
2005
- }
2006
-
2007
- NODE_DISPLAY_NAME_MAPPINGS = {
2008
- # Sampling
2009
- "KSampler": "KSampler",
2010
- "KSamplerAdvanced": "KSampler (Advanced)",
2011
- # Loaders
2012
- "CheckpointLoader": "Load Checkpoint With Config (DEPRECATED)",
2013
- "CheckpointLoaderSimple": "Load Checkpoint",
2014
- "VAELoader": "Load VAE",
2015
- "LoraLoader": "Load LoRA",
2016
- "CLIPLoader": "Load CLIP",
2017
- "ControlNetLoader": "Load ControlNet Model",
2018
- "DiffControlNetLoader": "Load ControlNet Model (diff)",
2019
- "StyleModelLoader": "Load Style Model",
2020
- "CLIPVisionLoader": "Load CLIP Vision",
2021
- "UpscaleModelLoader": "Load Upscale Model",
2022
- "UNETLoader": "Load Diffusion Model",
2023
- # Conditioning
2024
- "CLIPVisionEncode": "CLIP Vision Encode",
2025
- "StyleModelApply": "Apply Style Model",
2026
- "CLIPTextEncode": "CLIP Text Encode (Prompt)",
2027
- "CLIPSetLastLayer": "CLIP Set Last Layer",
2028
- "ConditioningCombine": "Conditioning (Combine)",
2029
- "ConditioningAverage ": "Conditioning (Average)",
2030
- "ConditioningConcat": "Conditioning (Concat)",
2031
- "ConditioningSetArea": "Conditioning (Set Area)",
2032
- "ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
2033
- "ConditioningSetMask": "Conditioning (Set Mask)",
2034
- "ControlNetApply": "Apply ControlNet (OLD)",
2035
- "ControlNetApplyAdvanced": "Apply ControlNet",
2036
- # Latent
2037
- "VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
2038
- "SetLatentNoiseMask": "Set Latent Noise Mask",
2039
- "VAEDecode": "VAE Decode",
2040
- "VAEEncode": "VAE Encode",
2041
- "LatentRotate": "Rotate Latent",
2042
- "LatentFlip": "Flip Latent",
2043
- "LatentCrop": "Crop Latent",
2044
- "EmptyLatentImage": "Empty Latent Image",
2045
- "LatentUpscale": "Upscale Latent",
2046
- "LatentUpscaleBy": "Upscale Latent By",
2047
- "LatentComposite": "Latent Composite",
2048
- "LatentBlend": "Latent Blend",
2049
- "LatentFromBatch" : "Latent From Batch",
2050
- "RepeatLatentBatch": "Repeat Latent Batch",
2051
- # Image
2052
- "SaveImage": "Save Image",
2053
- "PreviewImage": "Preview Image",
2054
- "LoadImage": "Load Image",
2055
- "LoadImageMask": "Load Image (as Mask)",
2056
- "LoadImageOutput": "Load Image (from Outputs)",
2057
- "ImageScale": "Upscale Image",
2058
- "ImageScaleBy": "Upscale Image By",
2059
- "ImageUpscaleWithModel": "Upscale Image (using Model)",
2060
- "ImageInvert": "Invert Image",
2061
- "ImagePadForOutpaint": "Pad Image for Outpainting",
2062
- "ImageBatch": "Batch Images",
2063
- "ImageCrop": "Image Crop",
2064
- "ImageStitch": "Image Stitch",
2065
- "ImageBlend": "Image Blend",
2066
- "ImageBlur": "Image Blur",
2067
- "ImageQuantize": "Image Quantize",
2068
- "ImageSharpen": "Image Sharpen",
2069
- "ImageScaleToTotalPixels": "Scale Image to Total Pixels",
2070
- "GetImageSize": "Get Image Size",
2071
- # _for_testing
2072
- "VAEDecodeTiled": "VAE Decode (Tiled)",
2073
- "VAEEncodeTiled": "VAE Encode (Tiled)",
2074
- }
2075
-
2076
- EXTENSION_WEB_DIRS = {}
2077
-
2078
- # Dictionary of successfully loaded module names and associated directories.
2079
- LOADED_MODULE_DIRS = {}
2080
-
2081
-
2082
- def get_module_name(module_path: str) -> str:
2083
- """
2084
- Returns the module name based on the given module path.
2085
- Examples:
2086
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node.py") -> "my_custom_node"
2087
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node") -> "my_custom_node"
2088
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/") -> "my_custom_node"
2089
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__.py") -> "my_custom_node"
2090
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__") -> "my_custom_node"
2091
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node/__init__/") -> "my_custom_node"
2092
- get_module_name("C:/Users/username/ComfyUI/custom_nodes/my_custom_node.disabled") -> "custom_nodes
2093
- Args:
2094
- module_path (str): The path of the module.
2095
- Returns:
2096
- str: The module name.
2097
- """
2098
- base_path = os.path.basename(module_path)
2099
- if os.path.isfile(module_path):
2100
- base_path = os.path.splitext(base_path)[0]
2101
- return base_path
2102
-
2103
-
2104
- def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes") -> bool:
2105
- module_name = get_module_name(module_path)
2106
- if os.path.isfile(module_path):
2107
- sp = os.path.splitext(module_path)
2108
- module_name = sp[0]
2109
- sys_module_name = module_name
2110
- elif os.path.isdir(module_path):
2111
- sys_module_name = module_path.replace(".", "_x_")
2112
-
2113
- try:
2114
- logging.debug("Trying to load custom node {}".format(module_path))
2115
- if os.path.isfile(module_path):
2116
- module_spec = importlib.util.spec_from_file_location(sys_module_name, module_path)
2117
- module_dir = os.path.split(module_path)[0]
2118
- else:
2119
- module_spec = importlib.util.spec_from_file_location(sys_module_name, os.path.join(module_path, "__init__.py"))
2120
- module_dir = module_path
2121
-
2122
- module = importlib.util.module_from_spec(module_spec)
2123
- sys.modules[sys_module_name] = module
2124
- module_spec.loader.exec_module(module)
2125
-
2126
- LOADED_MODULE_DIRS[module_name] = os.path.abspath(module_dir)
2127
-
2128
- try:
2129
- from comfy_config import config_parser
2130
-
2131
- project_config = config_parser.extract_node_configuration(module_path)
2132
-
2133
- web_dir_name = project_config.tool_comfy.web
2134
-
2135
- if web_dir_name:
2136
- web_dir_path = os.path.join(module_path, web_dir_name)
2137
-
2138
- if os.path.isdir(web_dir_path):
2139
- project_name = project_config.project.name
2140
-
2141
- EXTENSION_WEB_DIRS[project_name] = web_dir_path
2142
-
2143
- logging.info("Automatically register web folder {} for {}".format(web_dir_name, project_name))
2144
- except Exception as e:
2145
- logging.warning(f"Unable to parse pyproject.toml due to lack dependency pydantic-settings, please run 'pip install -r requirements.txt': {e}")
2146
-
2147
- if hasattr(module, "WEB_DIRECTORY") and getattr(module, "WEB_DIRECTORY") is not None:
2148
- web_dir = os.path.abspath(os.path.join(module_dir, getattr(module, "WEB_DIRECTORY")))
2149
- if os.path.isdir(web_dir):
2150
- EXTENSION_WEB_DIRS[module_name] = web_dir
2151
-
2152
- if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
2153
- for name, node_cls in module.NODE_CLASS_MAPPINGS.items():
2154
- if name not in ignore:
2155
- NODE_CLASS_MAPPINGS[name] = node_cls
2156
- node_cls.RELATIVE_PYTHON_MODULE = "{}.{}".format(module_parent, get_module_name(module_path))
2157
- if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
2158
- NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
2159
- return True
2160
- else:
2161
- logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.")
2162
- return False
2163
- except Exception as e:
2164
- logging.warning(traceback.format_exc())
2165
- logging.warning(f"Cannot import {module_path} module for custom nodes: {e}")
2166
- return False
2167
-
2168
- def init_external_custom_nodes():
2169
- """
2170
- Initializes the external custom nodes.
2171
-
2172
- This function loads custom nodes from the specified folder paths and imports them into the application.
2173
- It measures the import times for each custom node and logs the results.
2174
-
2175
- Returns:
2176
- None
2177
- """
2178
- base_node_names = set(NODE_CLASS_MAPPINGS.keys())
2179
- node_paths = folder_paths.get_folder_paths("custom_nodes")
2180
- node_import_times = []
2181
- for custom_node_path in node_paths:
2182
- possible_modules = os.listdir(os.path.realpath(custom_node_path))
2183
- if "__pycache__" in possible_modules:
2184
- possible_modules.remove("__pycache__")
2185
-
2186
- for possible_module in possible_modules:
2187
- module_path = os.path.join(custom_node_path, possible_module)
2188
- if os.path.isfile(module_path) and os.path.splitext(module_path)[1] != ".py": continue
2189
- if module_path.endswith(".disabled"): continue
2190
- if args.disable_all_custom_nodes and possible_module not in args.whitelist_custom_nodes:
2191
- logging.info(f"Skipping {possible_module} due to disable_all_custom_nodes and whitelist_custom_nodes")
2192
- continue
2193
- time_before = time.perf_counter()
2194
- success = load_custom_node(module_path, base_node_names, module_parent="custom_nodes")
2195
- node_import_times.append((time.perf_counter() - time_before, module_path, success))
2196
-
2197
- if len(node_import_times) > 0:
2198
- logging.info("\nImport times for custom nodes:")
2199
- for n in sorted(node_import_times):
2200
- if n[2]:
2201
- import_message = ""
2202
- else:
2203
- import_message = " (IMPORT FAILED)"
2204
- logging.info("{:6.1f} seconds{}: {}".format(n[0], import_message, n[1]))
2205
- logging.info("")
2206
-
2207
- def init_builtin_extra_nodes():
2208
- """
2209
- Initializes the built-in extra nodes in ComfyUI.
2210
-
2211
- This function loads the extra node files located in the "comfy_extras" directory and imports them into ComfyUI.
2212
- If any of the extra node files fail to import, a warning message is logged.
2213
-
2214
- Returns:
2215
- None
2216
- """
2217
- extras_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras")
2218
- extras_files = [
2219
- "nodes_latent.py",
2220
- "nodes_hypernetwork.py",
2221
- "nodes_upscale_model.py",
2222
- "nodes_post_processing.py",
2223
- "nodes_mask.py",
2224
- "nodes_compositing.py",
2225
- "nodes_rebatch.py",
2226
- "nodes_model_merging.py",
2227
- "nodes_tomesd.py",
2228
- "nodes_clip_sdxl.py",
2229
- "nodes_canny.py",
2230
- "nodes_freelunch.py",
2231
- "nodes_custom_sampler.py",
2232
- "nodes_hypertile.py",
2233
- "nodes_model_advanced.py",
2234
- "nodes_model_downscale.py",
2235
- "nodes_images.py",
2236
- "nodes_video_model.py",
2237
- "nodes_train.py",
2238
- "nodes_sag.py",
2239
- "nodes_perpneg.py",
2240
- "nodes_stable3d.py",
2241
- "nodes_sdupscale.py",
2242
- "nodes_photomaker.py",
2243
- "nodes_pixart.py",
2244
- "nodes_cond.py",
2245
- "nodes_morphology.py",
2246
- "nodes_stable_cascade.py",
2247
- "nodes_differential_diffusion.py",
2248
- "nodes_ip2p.py",
2249
- "nodes_model_merging_model_specific.py",
2250
- "nodes_pag.py",
2251
- "nodes_align_your_steps.py",
2252
- "nodes_attention_multiply.py",
2253
- "nodes_advanced_samplers.py",
2254
- "nodes_webcam.py",
2255
- "nodes_audio.py",
2256
- "nodes_sd3.py",
2257
- "nodes_gits.py",
2258
- "nodes_controlnet.py",
2259
- "nodes_hunyuan.py",
2260
- "nodes_flux.py",
2261
- "nodes_lora_extract.py",
2262
- "nodes_torch_compile.py",
2263
- "nodes_mochi.py",
2264
- "nodes_slg.py",
2265
- "nodes_mahiro.py",
2266
- "nodes_lt.py",
2267
- "nodes_hooks.py",
2268
- "nodes_load_3d.py",
2269
- "nodes_cosmos.py",
2270
- "nodes_video.py",
2271
- "nodes_lumina2.py",
2272
- "nodes_wan.py",
2273
- "nodes_lotus.py",
2274
- "nodes_hunyuan3d.py",
2275
- "nodes_primitive.py",
2276
- "nodes_cfg.py",
2277
- "nodes_optimalsteps.py",
2278
- "nodes_hidream.py",
2279
- "nodes_fresca.py",
2280
- "nodes_apg.py",
2281
- "nodes_preview_any.py",
2282
- "nodes_ace.py",
2283
- "nodes_string.py",
2284
- "nodes_camera_trajectory.py",
2285
- "nodes_edit_model.py",
2286
- "nodes_tcfg.py"
2287
- ]
2288
-
2289
- import_failed = []
2290
- for node_file in extras_files:
2291
- if not load_custom_node(os.path.join(extras_dir, node_file), module_parent="comfy_extras"):
2292
- import_failed.append(node_file)
2293
-
2294
- return import_failed
2295
-
2296
-
2297
- def init_builtin_api_nodes():
2298
- api_nodes_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_api_nodes")
2299
- api_nodes_files = [
2300
- "nodes_ideogram.py",
2301
- "nodes_openai.py",
2302
- "nodes_minimax.py",
2303
- "nodes_veo2.py",
2304
- "nodes_kling.py",
2305
- "nodes_bfl.py",
2306
- "nodes_luma.py",
2307
- "nodes_recraft.py",
2308
- "nodes_pixverse.py",
2309
- "nodes_stability.py",
2310
- "nodes_pika.py",
2311
- "nodes_runway.py",
2312
- "nodes_tripo.py",
2313
- "nodes_moonvalley.py",
2314
- "nodes_rodin.py",
2315
- "nodes_gemini.py",
2316
- ]
2317
-
2318
- if not load_custom_node(os.path.join(api_nodes_dir, "canary.py"), module_parent="comfy_api_nodes"):
2319
- return api_nodes_files
2320
-
2321
- import_failed = []
2322
- for node_file in api_nodes_files:
2323
- if not load_custom_node(os.path.join(api_nodes_dir, node_file), module_parent="comfy_api_nodes"):
2324
- import_failed.append(node_file)
2325
-
2326
- return import_failed
2327
-
2328
-
2329
- def init_extra_nodes(init_custom_nodes=True, init_api_nodes=True):
2330
- import_failed = init_builtin_extra_nodes()
2331
-
2332
- import_failed_api = []
2333
- if init_api_nodes:
2334
- import_failed_api = init_builtin_api_nodes()
2335
-
2336
- if init_custom_nodes:
2337
- init_external_custom_nodes()
2338
- else:
2339
- logging.info("Skipping loading of custom nodes")
2340
-
2341
- if len(import_failed_api) > 0:
2342
- logging.warning("WARNING: some comfy_api_nodes/ nodes did not import correctly. This may be because they are missing some dependencies.\n")
2343
- for node in import_failed_api:
2344
- logging.warning("IMPORT FAILED: {}".format(node))
2345
- logging.warning("\nThis issue might be caused by new missing dependencies added the last time you updated ComfyUI.")
2346
- if args.windows_standalone_build:
2347
- logging.warning("Please run the update script: update/update_comfyui.bat")
2348
- else:
2349
- logging.warning("Please do a: pip install -r requirements.txt")
2350
- logging.warning("")
2351
-
2352
- if len(import_failed) > 0:
2353
- logging.warning("WARNING: some comfy_extras/ nodes did not import correctly. This may be because they are missing some dependencies.\n")
2354
- for node in import_failed:
2355
- logging.warning("IMPORT FAILED: {}".format(node))
2356
- logging.warning("\nThis issue might be caused by new missing dependencies added the last time you updated ComfyUI.")
2357
- if args.windows_standalone_build:
2358
- logging.warning("Please run the update script: update/update_comfyui.bat")
2359
- else:
2360
- logging.warning("Please do a: pip install -r requirements.txt")
2361
- logging.warning("")
2362
-
2363
- return import_failed