mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-11 12:17:45 +08:00
Merge remote-tracking branch 'origin/master' into group-nodes
This commit is contained in:
commit
d0514b87bc
@ -62,3 +62,18 @@ class CONDCrossAttn(CONDRegular):
|
|||||||
c = c.repeat(1, crossattn_max_len // c.shape[1], 1) #padding with repeat doesn't change result
|
c = c.repeat(1, crossattn_max_len // c.shape[1], 1) #padding with repeat doesn't change result
|
||||||
out.append(c)
|
out.append(c)
|
||||||
return torch.cat(out)
|
return torch.cat(out)
|
||||||
|
|
||||||
|
class CONDConstant(CONDRegular):
|
||||||
|
def __init__(self, cond):
|
||||||
|
self.cond = cond
|
||||||
|
|
||||||
|
def process_cond(self, batch_size, device, **kwargs):
|
||||||
|
return self._copy_with(self.cond)
|
||||||
|
|
||||||
|
def can_concat(self, other):
|
||||||
|
if self.cond != other.cond:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def concat(self, others):
|
||||||
|
return self.cond
|
||||||
|
|||||||
@ -251,6 +251,12 @@ class Timestep(nn.Module):
|
|||||||
def forward(self, t):
|
def forward(self, t):
|
||||||
return timestep_embedding(t, self.dim)
|
return timestep_embedding(t, self.dim)
|
||||||
|
|
||||||
|
def apply_control(h, control, name):
|
||||||
|
if control is not None and name in control and len(control[name]) > 0:
|
||||||
|
ctrl = control[name].pop()
|
||||||
|
if ctrl is not None:
|
||||||
|
h += ctrl
|
||||||
|
return h
|
||||||
|
|
||||||
class UNetModel(nn.Module):
|
class UNetModel(nn.Module):
|
||||||
"""
|
"""
|
||||||
@ -617,25 +623,17 @@ class UNetModel(nn.Module):
|
|||||||
for id, module in enumerate(self.input_blocks):
|
for id, module in enumerate(self.input_blocks):
|
||||||
transformer_options["block"] = ("input", id)
|
transformer_options["block"] = ("input", id)
|
||||||
h = forward_timestep_embed(module, h, emb, context, transformer_options)
|
h = forward_timestep_embed(module, h, emb, context, transformer_options)
|
||||||
if control is not None and 'input' in control and len(control['input']) > 0:
|
h = apply_control(h, control, 'input')
|
||||||
ctrl = control['input'].pop()
|
|
||||||
if ctrl is not None:
|
|
||||||
h += ctrl
|
|
||||||
hs.append(h)
|
hs.append(h)
|
||||||
|
|
||||||
transformer_options["block"] = ("middle", 0)
|
transformer_options["block"] = ("middle", 0)
|
||||||
h = forward_timestep_embed(self.middle_block, h, emb, context, transformer_options)
|
h = forward_timestep_embed(self.middle_block, h, emb, context, transformer_options)
|
||||||
if control is not None and 'middle' in control and len(control['middle']) > 0:
|
h = apply_control(h, control, 'middle')
|
||||||
ctrl = control['middle'].pop()
|
|
||||||
if ctrl is not None:
|
|
||||||
h += ctrl
|
|
||||||
|
|
||||||
for id, module in enumerate(self.output_blocks):
|
for id, module in enumerate(self.output_blocks):
|
||||||
transformer_options["block"] = ("output", id)
|
transformer_options["block"] = ("output", id)
|
||||||
hsp = hs.pop()
|
hsp = hs.pop()
|
||||||
if control is not None and 'output' in control and len(control['output']) > 0:
|
hsp = apply_control(hsp, control, 'output')
|
||||||
ctrl = control['output'].pop()
|
|
||||||
if ctrl is not None:
|
|
||||||
hsp += ctrl
|
|
||||||
|
|
||||||
if "output_block_patch" in transformer_patches:
|
if "output_block_patch" in transformer_patches:
|
||||||
patch = transformer_patches["output_block_patch"]
|
patch = transformer_patches["output_block_patch"]
|
||||||
|
|||||||
@ -170,8 +170,8 @@ def timestep_embedding(timesteps, dim, max_period=10000, repeat_only=False):
|
|||||||
if not repeat_only:
|
if not repeat_only:
|
||||||
half = dim // 2
|
half = dim // 2
|
||||||
freqs = torch.exp(
|
freqs = torch.exp(
|
||||||
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
|
-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32, device=timesteps.device) / half
|
||||||
).to(device=timesteps.device)
|
)
|
||||||
args = timesteps[:, None].float() * freqs[None]
|
args = timesteps[:, None].float() * freqs[None]
|
||||||
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
||||||
if dim % 2:
|
if dim % 2:
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import torch
|
import torch
|
||||||
from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel
|
from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel
|
||||||
from comfy.ldm.modules.encoders.noise_aug_modules import CLIPEmbeddingNoiseAugmentation
|
from comfy.ldm.modules.encoders.noise_aug_modules import CLIPEmbeddingNoiseAugmentation
|
||||||
from comfy.ldm.modules.diffusionmodules.util import make_beta_schedule
|
|
||||||
from comfy.ldm.modules.diffusionmodules.openaimodel import Timestep
|
from comfy.ldm.modules.diffusionmodules.openaimodel import Timestep
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
import comfy.conds
|
import comfy.conds
|
||||||
import numpy as np
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
@ -14,79 +12,7 @@ class ModelType(Enum):
|
|||||||
V_PREDICTION = 2
|
V_PREDICTION = 2
|
||||||
|
|
||||||
|
|
||||||
#NOTE: all this sampling stuff will be moved
|
from comfy.model_sampling import EPS, V_PREDICTION, ModelSamplingDiscrete
|
||||||
class EPS:
|
|
||||||
def calculate_input(self, sigma, noise):
|
|
||||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (noise.ndim - 1))
|
|
||||||
return noise / (sigma ** 2 + self.sigma_data ** 2) ** 0.5
|
|
||||||
|
|
||||||
def calculate_denoised(self, sigma, model_output, model_input):
|
|
||||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
|
||||||
return model_input - model_output * sigma
|
|
||||||
|
|
||||||
|
|
||||||
class V_PREDICTION(EPS):
|
|
||||||
def calculate_denoised(self, sigma, model_output, model_input):
|
|
||||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
|
||||||
return model_input * self.sigma_data ** 2 / (sigma ** 2 + self.sigma_data ** 2) - model_output * sigma * self.sigma_data / (sigma ** 2 + self.sigma_data ** 2) ** 0.5
|
|
||||||
|
|
||||||
|
|
||||||
class ModelSamplingDiscrete(torch.nn.Module):
|
|
||||||
def __init__(self, model_config=None):
|
|
||||||
super().__init__()
|
|
||||||
beta_schedule = "linear"
|
|
||||||
if model_config is not None:
|
|
||||||
beta_schedule = model_config.beta_schedule
|
|
||||||
self._register_schedule(given_betas=None, beta_schedule=beta_schedule, timesteps=1000, linear_start=0.00085, linear_end=0.012, cosine_s=8e-3)
|
|
||||||
self.sigma_data = 1.0
|
|
||||||
|
|
||||||
def _register_schedule(self, given_betas=None, beta_schedule="linear", timesteps=1000,
|
|
||||||
linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3):
|
|
||||||
if given_betas is not None:
|
|
||||||
betas = given_betas
|
|
||||||
else:
|
|
||||||
betas = make_beta_schedule(beta_schedule, timesteps, linear_start=linear_start, linear_end=linear_end, cosine_s=cosine_s)
|
|
||||||
alphas = 1. - betas
|
|
||||||
alphas_cumprod = torch.tensor(np.cumprod(alphas, axis=0), dtype=torch.float32)
|
|
||||||
# alphas_cumprod_prev = np.append(1., alphas_cumprod[:-1])
|
|
||||||
|
|
||||||
timesteps, = betas.shape
|
|
||||||
self.num_timesteps = int(timesteps)
|
|
||||||
self.linear_start = linear_start
|
|
||||||
self.linear_end = linear_end
|
|
||||||
|
|
||||||
# self.register_buffer('betas', torch.tensor(betas, dtype=torch.float32))
|
|
||||||
# self.register_buffer('alphas_cumprod', torch.tensor(alphas_cumprod, dtype=torch.float32))
|
|
||||||
# self.register_buffer('alphas_cumprod_prev', torch.tensor(alphas_cumprod_prev, dtype=torch.float32))
|
|
||||||
|
|
||||||
sigmas = ((1 - alphas_cumprod) / alphas_cumprod) ** 0.5
|
|
||||||
|
|
||||||
self.register_buffer('sigmas', sigmas)
|
|
||||||
self.register_buffer('log_sigmas', sigmas.log())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sigma_min(self):
|
|
||||||
return self.sigmas[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sigma_max(self):
|
|
||||||
return self.sigmas[-1]
|
|
||||||
|
|
||||||
def timestep(self, sigma):
|
|
||||||
log_sigma = sigma.log()
|
|
||||||
dists = log_sigma.to(self.log_sigmas.device) - self.log_sigmas[:, None]
|
|
||||||
return dists.abs().argmin(dim=0).view(sigma.shape)
|
|
||||||
|
|
||||||
def sigma(self, timestep):
|
|
||||||
t = torch.clamp(timestep.float(), min=0, max=(len(self.sigmas) - 1))
|
|
||||||
low_idx = t.floor().long()
|
|
||||||
high_idx = t.ceil().long()
|
|
||||||
w = t.frac()
|
|
||||||
log_sigma = (1 - w) * self.log_sigmas[low_idx] + w * self.log_sigmas[high_idx]
|
|
||||||
return log_sigma.exp()
|
|
||||||
|
|
||||||
def percent_to_sigma(self, percent):
|
|
||||||
return self.sigma(torch.tensor(percent * 999.0))
|
|
||||||
|
|
||||||
def model_sampling(model_config, model_type):
|
def model_sampling(model_config, model_type):
|
||||||
if model_type == ModelType.EPS:
|
if model_type == ModelType.EPS:
|
||||||
@ -102,7 +28,6 @@ def model_sampling(model_config, model_type):
|
|||||||
return ModelSampling(model_config)
|
return ModelSampling(model_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(torch.nn.Module):
|
class BaseModel(torch.nn.Module):
|
||||||
def __init__(self, model_config, model_type=ModelType.EPS, device=None):
|
def __init__(self, model_config, model_type=ModelType.EPS, device=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -136,7 +61,10 @@ class BaseModel(torch.nn.Module):
|
|||||||
context = context.to(dtype)
|
context = context.to(dtype)
|
||||||
extra_conds = {}
|
extra_conds = {}
|
||||||
for o in kwargs:
|
for o in kwargs:
|
||||||
extra_conds[o] = kwargs[o].to(dtype)
|
extra = kwargs[o]
|
||||||
|
if hasattr(extra, "to"):
|
||||||
|
extra = extra.to(dtype)
|
||||||
|
extra_conds[o] = extra
|
||||||
model_output = self.diffusion_model(xc, t, context=context, control=control, transformer_options=transformer_options, **extra_conds).float()
|
model_output = self.diffusion_model(xc, t, context=context, control=control, transformer_options=transformer_options, **extra_conds).float()
|
||||||
return self.model_sampling.calculate_denoised(sigma, model_output, x)
|
return self.model_sampling.calculate_denoised(sigma, model_output, x)
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ class ModelPatcher:
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.patches = {}
|
self.patches = {}
|
||||||
self.backup = {}
|
self.backup = {}
|
||||||
|
self.object_patches = {}
|
||||||
|
self.object_patches_backup = {}
|
||||||
self.model_options = {"transformer_options":{}}
|
self.model_options = {"transformer_options":{}}
|
||||||
self.model_size()
|
self.model_size()
|
||||||
self.load_device = load_device
|
self.load_device = load_device
|
||||||
@ -38,6 +40,7 @@ class ModelPatcher:
|
|||||||
for k in self.patches:
|
for k in self.patches:
|
||||||
n.patches[k] = self.patches[k][:]
|
n.patches[k] = self.patches[k][:]
|
||||||
|
|
||||||
|
n.object_patches = self.object_patches.copy()
|
||||||
n.model_options = copy.deepcopy(self.model_options)
|
n.model_options = copy.deepcopy(self.model_options)
|
||||||
n.model_keys = self.model_keys
|
n.model_keys = self.model_keys
|
||||||
return n
|
return n
|
||||||
@ -91,6 +94,9 @@ class ModelPatcher:
|
|||||||
def set_model_output_block_patch(self, patch):
|
def set_model_output_block_patch(self, patch):
|
||||||
self.set_model_patch(patch, "output_block_patch")
|
self.set_model_patch(patch, "output_block_patch")
|
||||||
|
|
||||||
|
def add_object_patch(self, name, obj):
|
||||||
|
self.object_patches[name] = obj
|
||||||
|
|
||||||
def model_patches_to(self, device):
|
def model_patches_to(self, device):
|
||||||
to = self.model_options["transformer_options"]
|
to = self.model_options["transformer_options"]
|
||||||
if "patches" in to:
|
if "patches" in to:
|
||||||
@ -107,10 +113,10 @@ class ModelPatcher:
|
|||||||
for k in patch_list:
|
for k in patch_list:
|
||||||
if hasattr(patch_list[k], "to"):
|
if hasattr(patch_list[k], "to"):
|
||||||
patch_list[k] = patch_list[k].to(device)
|
patch_list[k] = patch_list[k].to(device)
|
||||||
if "unet_wrapper_function" in self.model_options:
|
if "model_function_wrapper" in self.model_options:
|
||||||
wrap_func = self.model_options["unet_wrapper_function"]
|
wrap_func = self.model_options["model_function_wrapper"]
|
||||||
if hasattr(wrap_func, "to"):
|
if hasattr(wrap_func, "to"):
|
||||||
self.model_options["unet_wrapper_function"] = wrap_func.to(device)
|
self.model_options["model_function_wrapper"] = wrap_func.to(device)
|
||||||
|
|
||||||
def model_dtype(self):
|
def model_dtype(self):
|
||||||
if hasattr(self.model, "get_dtype"):
|
if hasattr(self.model, "get_dtype"):
|
||||||
@ -150,6 +156,12 @@ class ModelPatcher:
|
|||||||
return sd
|
return sd
|
||||||
|
|
||||||
def patch_model(self, device_to=None):
|
def patch_model(self, device_to=None):
|
||||||
|
for k in self.object_patches:
|
||||||
|
old = getattr(self.model, k)
|
||||||
|
if k not in self.object_patches_backup:
|
||||||
|
self.object_patches_backup[k] = old
|
||||||
|
setattr(self.model, k, self.object_patches[k])
|
||||||
|
|
||||||
model_sd = self.model_state_dict()
|
model_sd = self.model_state_dict()
|
||||||
for key in self.patches:
|
for key in self.patches:
|
||||||
if key not in model_sd:
|
if key not in model_sd:
|
||||||
@ -290,3 +302,9 @@ class ModelPatcher:
|
|||||||
if device_to is not None:
|
if device_to is not None:
|
||||||
self.model.to(device_to)
|
self.model.to(device_to)
|
||||||
self.current_device = device_to
|
self.current_device = device_to
|
||||||
|
|
||||||
|
keys = list(self.object_patches_backup.keys())
|
||||||
|
for k in keys:
|
||||||
|
setattr(self.model, k, self.object_patches_backup[k])
|
||||||
|
|
||||||
|
self.object_patches_backup = {}
|
||||||
|
|||||||
80
comfy/model_sampling.py
Normal file
80
comfy/model_sampling.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
from comfy.ldm.modules.diffusionmodules.util import make_beta_schedule
|
||||||
|
|
||||||
|
|
||||||
|
class EPS:
|
||||||
|
def calculate_input(self, sigma, noise):
|
||||||
|
sigma = sigma.view(sigma.shape[:1] + (1,) * (noise.ndim - 1))
|
||||||
|
return noise / (sigma ** 2 + self.sigma_data ** 2) ** 0.5
|
||||||
|
|
||||||
|
def calculate_denoised(self, sigma, model_output, model_input):
|
||||||
|
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
||||||
|
return model_input - model_output * sigma
|
||||||
|
|
||||||
|
|
||||||
|
class V_PREDICTION(EPS):
|
||||||
|
def calculate_denoised(self, sigma, model_output, model_input):
|
||||||
|
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
||||||
|
return model_input * self.sigma_data ** 2 / (sigma ** 2 + self.sigma_data ** 2) - model_output * sigma * self.sigma_data / (sigma ** 2 + self.sigma_data ** 2) ** 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSamplingDiscrete(torch.nn.Module):
|
||||||
|
def __init__(self, model_config=None):
|
||||||
|
super().__init__()
|
||||||
|
beta_schedule = "linear"
|
||||||
|
if model_config is not None:
|
||||||
|
beta_schedule = model_config.beta_schedule
|
||||||
|
self._register_schedule(given_betas=None, beta_schedule=beta_schedule, timesteps=1000, linear_start=0.00085, linear_end=0.012, cosine_s=8e-3)
|
||||||
|
self.sigma_data = 1.0
|
||||||
|
|
||||||
|
def _register_schedule(self, given_betas=None, beta_schedule="linear", timesteps=1000,
|
||||||
|
linear_start=1e-4, linear_end=2e-2, cosine_s=8e-3):
|
||||||
|
if given_betas is not None:
|
||||||
|
betas = given_betas
|
||||||
|
else:
|
||||||
|
betas = make_beta_schedule(beta_schedule, timesteps, linear_start=linear_start, linear_end=linear_end, cosine_s=cosine_s)
|
||||||
|
alphas = 1. - betas
|
||||||
|
alphas_cumprod = torch.tensor(np.cumprod(alphas, axis=0), dtype=torch.float32)
|
||||||
|
# alphas_cumprod_prev = np.append(1., alphas_cumprod[:-1])
|
||||||
|
|
||||||
|
timesteps, = betas.shape
|
||||||
|
self.num_timesteps = int(timesteps)
|
||||||
|
self.linear_start = linear_start
|
||||||
|
self.linear_end = linear_end
|
||||||
|
|
||||||
|
# self.register_buffer('betas', torch.tensor(betas, dtype=torch.float32))
|
||||||
|
# self.register_buffer('alphas_cumprod', torch.tensor(alphas_cumprod, dtype=torch.float32))
|
||||||
|
# self.register_buffer('alphas_cumprod_prev', torch.tensor(alphas_cumprod_prev, dtype=torch.float32))
|
||||||
|
|
||||||
|
sigmas = ((1 - alphas_cumprod) / alphas_cumprod) ** 0.5
|
||||||
|
self.set_sigmas(sigmas)
|
||||||
|
|
||||||
|
def set_sigmas(self, sigmas):
|
||||||
|
self.register_buffer('sigmas', sigmas)
|
||||||
|
self.register_buffer('log_sigmas', sigmas.log())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sigma_min(self):
|
||||||
|
return self.sigmas[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sigma_max(self):
|
||||||
|
return self.sigmas[-1]
|
||||||
|
|
||||||
|
def timestep(self, sigma):
|
||||||
|
log_sigma = sigma.log()
|
||||||
|
dists = log_sigma.to(self.log_sigmas.device) - self.log_sigmas[:, None]
|
||||||
|
return dists.abs().argmin(dim=0).view(sigma.shape)
|
||||||
|
|
||||||
|
def sigma(self, timestep):
|
||||||
|
t = torch.clamp(timestep.float(), min=0, max=(len(self.sigmas) - 1))
|
||||||
|
low_idx = t.floor().long()
|
||||||
|
high_idx = t.ceil().long()
|
||||||
|
w = t.frac()
|
||||||
|
log_sigma = (1 - w) * self.log_sigmas[low_idx] + w * self.log_sigmas[high_idx]
|
||||||
|
return log_sigma.exp()
|
||||||
|
|
||||||
|
def percent_to_sigma(self, percent):
|
||||||
|
return self.sigma(torch.tensor(percent * 999.0))
|
||||||
|
|
||||||
@ -496,6 +496,9 @@ def load_unet(unet_path): #load unet in diffusers format
|
|||||||
model = model_config.get_model(new_sd, "")
|
model = model_config.get_model(new_sd, "")
|
||||||
model = model.to(offload_device)
|
model = model.to(offload_device)
|
||||||
model.load_model_weights(new_sd, "")
|
model.load_model_weights(new_sd, "")
|
||||||
|
left_over = sd.keys()
|
||||||
|
if len(left_over) > 0:
|
||||||
|
print("left over keys in unet:", left_over)
|
||||||
return comfy.model_patcher.ModelPatcher(model, load_device=model_management.get_torch_device(), offload_device=offload_device)
|
return comfy.model_patcher.ModelPatcher(model, load_device=model_management.get_torch_device(), offload_device=offload_device)
|
||||||
|
|
||||||
def save_checkpoint(output_path, model, clip, vae, metadata=None):
|
def save_checkpoint(output_path, model, clip, vae, metadata=None):
|
||||||
|
|||||||
@ -8,32 +8,54 @@ import zipfile
|
|||||||
from . import model_management
|
from . import model_management
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
|
def gen_empty_tokens(special_tokens, length):
|
||||||
|
start_token = special_tokens.get("start", None)
|
||||||
|
end_token = special_tokens.get("end", None)
|
||||||
|
pad_token = special_tokens.get("pad")
|
||||||
|
output = []
|
||||||
|
if start_token is not None:
|
||||||
|
output.append(start_token)
|
||||||
|
if end_token is not None:
|
||||||
|
output.append(end_token)
|
||||||
|
output += [pad_token] * (length - len(output))
|
||||||
|
return output
|
||||||
|
|
||||||
class ClipTokenWeightEncoder:
|
class ClipTokenWeightEncoder:
|
||||||
def encode_token_weights(self, token_weight_pairs):
|
def encode_token_weights(self, token_weight_pairs):
|
||||||
to_encode = list(self.empty_tokens)
|
to_encode = list()
|
||||||
|
max_token_len = 0
|
||||||
|
has_weights = False
|
||||||
for x in token_weight_pairs:
|
for x in token_weight_pairs:
|
||||||
tokens = list(map(lambda a: a[0], x))
|
tokens = list(map(lambda a: a[0], x))
|
||||||
|
max_token_len = max(len(tokens), max_token_len)
|
||||||
|
has_weights = has_weights or not all(map(lambda a: a[1] == 1.0, x))
|
||||||
to_encode.append(tokens)
|
to_encode.append(tokens)
|
||||||
|
|
||||||
|
sections = len(to_encode)
|
||||||
|
if has_weights or sections == 0:
|
||||||
|
to_encode.append(gen_empty_tokens(self.special_tokens, max_token_len))
|
||||||
|
|
||||||
out, pooled = self.encode(to_encode)
|
out, pooled = self.encode(to_encode)
|
||||||
z_empty = out[0:1]
|
if pooled is not None:
|
||||||
if pooled.shape[0] > 1:
|
first_pooled = pooled[0:1].cpu()
|
||||||
first_pooled = pooled[1:2]
|
|
||||||
else:
|
else:
|
||||||
first_pooled = pooled[0:1]
|
first_pooled = pooled
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for k in range(1, out.shape[0]):
|
for k in range(0, sections):
|
||||||
z = out[k:k+1]
|
z = out[k:k+1]
|
||||||
for i in range(len(z)):
|
if has_weights:
|
||||||
for j in range(len(z[i])):
|
z_empty = out[-1]
|
||||||
weight = token_weight_pairs[k - 1][j][1]
|
for i in range(len(z)):
|
||||||
z[i][j] = (z[i][j] - z_empty[0][j]) * weight + z_empty[0][j]
|
for j in range(len(z[i])):
|
||||||
|
weight = token_weight_pairs[k][j][1]
|
||||||
|
if weight != 1.0:
|
||||||
|
z[i][j] = (z[i][j] - z_empty[j]) * weight + z_empty[j]
|
||||||
output.append(z)
|
output.append(z)
|
||||||
|
|
||||||
if (len(output) == 0):
|
if (len(output) == 0):
|
||||||
return z_empty.cpu(), first_pooled.cpu()
|
return out[-1:].cpu(), first_pooled
|
||||||
return torch.cat(output, dim=-2).cpu(), first_pooled.cpu()
|
return torch.cat(output, dim=-2).cpu(), first_pooled
|
||||||
|
|
||||||
class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
||||||
"""Uses the CLIP transformer encoder for text (from huggingface)"""
|
"""Uses the CLIP transformer encoder for text (from huggingface)"""
|
||||||
@ -43,37 +65,43 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
|||||||
"hidden"
|
"hidden"
|
||||||
]
|
]
|
||||||
def __init__(self, version="openai/clip-vit-large-patch14", device="cpu", max_length=77,
|
def __init__(self, version="openai/clip-vit-large-patch14", device="cpu", max_length=77,
|
||||||
freeze=True, layer="last", layer_idx=None, textmodel_json_config=None, textmodel_path=None, dtype=None): # clip-vit-base-patch32
|
freeze=True, layer="last", layer_idx=None, textmodel_json_config=None, textmodel_path=None, dtype=None,
|
||||||
|
special_tokens={"start": 49406, "end": 49407, "pad": 49407},layer_norm_hidden_state=True, config_class=CLIPTextConfig,
|
||||||
|
model_class=CLIPTextModel, inner_name="text_model"): # clip-vit-base-patch32
|
||||||
super().__init__()
|
super().__init__()
|
||||||
assert layer in self.LAYERS
|
assert layer in self.LAYERS
|
||||||
self.num_layers = 12
|
self.num_layers = 12
|
||||||
if textmodel_path is not None:
|
if textmodel_path is not None:
|
||||||
self.transformer = CLIPTextModel.from_pretrained(textmodel_path)
|
self.transformer = model_class.from_pretrained(textmodel_path)
|
||||||
else:
|
else:
|
||||||
if textmodel_json_config is None:
|
if textmodel_json_config is None:
|
||||||
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd1_clip_config.json")
|
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd1_clip_config.json")
|
||||||
config = CLIPTextConfig.from_json_file(textmodel_json_config)
|
config = config_class.from_json_file(textmodel_json_config)
|
||||||
self.num_layers = config.num_hidden_layers
|
self.num_layers = config.num_hidden_layers
|
||||||
with comfy.ops.use_comfy_ops(device, dtype):
|
with comfy.ops.use_comfy_ops(device, dtype):
|
||||||
with modeling_utils.no_init_weights():
|
with modeling_utils.no_init_weights():
|
||||||
self.transformer = CLIPTextModel(config)
|
self.transformer = model_class(config)
|
||||||
|
|
||||||
|
self.inner_name = inner_name
|
||||||
if dtype is not None:
|
if dtype is not None:
|
||||||
self.transformer.to(dtype)
|
self.transformer.to(dtype)
|
||||||
self.transformer.text_model.embeddings.token_embedding.to(torch.float32)
|
inner_model = getattr(self.transformer, self.inner_name)
|
||||||
self.transformer.text_model.embeddings.position_embedding.to(torch.float32)
|
if hasattr(inner_model, "embeddings"):
|
||||||
|
inner_model.embeddings.to(torch.float32)
|
||||||
|
else:
|
||||||
|
self.transformer.set_input_embeddings(self.transformer.get_input_embeddings().to(torch.float32))
|
||||||
|
|
||||||
self.max_length = max_length
|
self.max_length = max_length
|
||||||
if freeze:
|
if freeze:
|
||||||
self.freeze()
|
self.freeze()
|
||||||
self.layer = layer
|
self.layer = layer
|
||||||
self.layer_idx = None
|
self.layer_idx = None
|
||||||
self.empty_tokens = [[49406] + [49407] * 76]
|
self.special_tokens = special_tokens
|
||||||
self.text_projection = torch.nn.Parameter(torch.eye(self.transformer.get_input_embeddings().weight.shape[1]))
|
self.text_projection = torch.nn.Parameter(torch.eye(self.transformer.get_input_embeddings().weight.shape[1]))
|
||||||
self.logit_scale = torch.nn.Parameter(torch.tensor(4.6055))
|
self.logit_scale = torch.nn.Parameter(torch.tensor(4.6055))
|
||||||
self.enable_attention_masks = False
|
self.enable_attention_masks = False
|
||||||
|
|
||||||
self.layer_norm_hidden_state = True
|
self.layer_norm_hidden_state = layer_norm_hidden_state
|
||||||
if layer == "hidden":
|
if layer == "hidden":
|
||||||
assert layer_idx is not None
|
assert layer_idx is not None
|
||||||
assert abs(layer_idx) <= self.num_layers
|
assert abs(layer_idx) <= self.num_layers
|
||||||
@ -117,7 +145,7 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
|||||||
else:
|
else:
|
||||||
print("WARNING: shape mismatch when trying to apply embedding, embedding will be ignored", y.shape[0], current_embeds.weight.shape[1])
|
print("WARNING: shape mismatch when trying to apply embedding, embedding will be ignored", y.shape[0], current_embeds.weight.shape[1])
|
||||||
while len(tokens_temp) < len(x):
|
while len(tokens_temp) < len(x):
|
||||||
tokens_temp += [self.empty_tokens[0][-1]]
|
tokens_temp += [self.special_tokens["pad"]]
|
||||||
out_tokens += [tokens_temp]
|
out_tokens += [tokens_temp]
|
||||||
|
|
||||||
n = token_dict_size
|
n = token_dict_size
|
||||||
@ -142,7 +170,7 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
|||||||
tokens = self.set_up_textual_embeddings(tokens, backup_embeds)
|
tokens = self.set_up_textual_embeddings(tokens, backup_embeds)
|
||||||
tokens = torch.LongTensor(tokens).to(device)
|
tokens = torch.LongTensor(tokens).to(device)
|
||||||
|
|
||||||
if self.transformer.text_model.final_layer_norm.weight.dtype != torch.float32:
|
if getattr(self.transformer, self.inner_name).final_layer_norm.weight.dtype != torch.float32:
|
||||||
precision_scope = torch.autocast
|
precision_scope = torch.autocast
|
||||||
else:
|
else:
|
||||||
precision_scope = lambda a, b: contextlib.nullcontext(a)
|
precision_scope = lambda a, b: contextlib.nullcontext(a)
|
||||||
@ -168,12 +196,16 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
|||||||
else:
|
else:
|
||||||
z = outputs.hidden_states[self.layer_idx]
|
z = outputs.hidden_states[self.layer_idx]
|
||||||
if self.layer_norm_hidden_state:
|
if self.layer_norm_hidden_state:
|
||||||
z = self.transformer.text_model.final_layer_norm(z)
|
z = getattr(self.transformer, self.inner_name).final_layer_norm(z)
|
||||||
|
|
||||||
pooled_output = outputs.pooler_output
|
if hasattr(outputs, "pooler_output"):
|
||||||
if self.text_projection is not None:
|
pooled_output = outputs.pooler_output.float()
|
||||||
|
else:
|
||||||
|
pooled_output = None
|
||||||
|
|
||||||
|
if self.text_projection is not None and pooled_output is not None:
|
||||||
pooled_output = pooled_output.float().to(self.text_projection.device) @ self.text_projection.float()
|
pooled_output = pooled_output.float().to(self.text_projection.device) @ self.text_projection.float()
|
||||||
return z.float(), pooled_output.float()
|
return z.float(), pooled_output
|
||||||
|
|
||||||
def encode(self, tokens):
|
def encode(self, tokens):
|
||||||
return self(tokens)
|
return self(tokens)
|
||||||
@ -343,17 +375,24 @@ def load_embed(embedding_name, embedding_directory, embedding_size, embed_key=No
|
|||||||
return embed_out
|
return embed_out
|
||||||
|
|
||||||
class SDTokenizer:
|
class SDTokenizer:
|
||||||
def __init__(self, tokenizer_path=None, max_length=77, pad_with_end=True, embedding_directory=None, embedding_size=768, embedding_key='clip_l'):
|
def __init__(self, tokenizer_path=None, max_length=77, pad_with_end=True, embedding_directory=None, embedding_size=768, embedding_key='clip_l', tokenizer_class=CLIPTokenizer, has_start_token=True, pad_to_max_length=True):
|
||||||
if tokenizer_path is None:
|
if tokenizer_path is None:
|
||||||
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd1_tokenizer")
|
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd1_tokenizer")
|
||||||
self.tokenizer = CLIPTokenizer.from_pretrained(tokenizer_path)
|
self.tokenizer = tokenizer_class.from_pretrained(tokenizer_path)
|
||||||
self.max_length = max_length
|
self.max_length = max_length
|
||||||
self.max_tokens_per_section = self.max_length - 2
|
|
||||||
|
|
||||||
empty = self.tokenizer('')["input_ids"]
|
empty = self.tokenizer('')["input_ids"]
|
||||||
self.start_token = empty[0]
|
if has_start_token:
|
||||||
self.end_token = empty[1]
|
self.tokens_start = 1
|
||||||
|
self.start_token = empty[0]
|
||||||
|
self.end_token = empty[1]
|
||||||
|
else:
|
||||||
|
self.tokens_start = 0
|
||||||
|
self.start_token = None
|
||||||
|
self.end_token = empty[0]
|
||||||
self.pad_with_end = pad_with_end
|
self.pad_with_end = pad_with_end
|
||||||
|
self.pad_to_max_length = pad_to_max_length
|
||||||
|
|
||||||
vocab = self.tokenizer.get_vocab()
|
vocab = self.tokenizer.get_vocab()
|
||||||
self.inv_vocab = {v: k for k, v in vocab.items()}
|
self.inv_vocab = {v: k for k, v in vocab.items()}
|
||||||
self.embedding_directory = embedding_directory
|
self.embedding_directory = embedding_directory
|
||||||
@ -414,11 +453,13 @@ class SDTokenizer:
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
#parse word
|
#parse word
|
||||||
tokens.append([(t, weight) for t in self.tokenizer(word)["input_ids"][1:-1]])
|
tokens.append([(t, weight) for t in self.tokenizer(word)["input_ids"][self.tokens_start:-1]])
|
||||||
|
|
||||||
#reshape token array to CLIP input size
|
#reshape token array to CLIP input size
|
||||||
batched_tokens = []
|
batched_tokens = []
|
||||||
batch = [(self.start_token, 1.0, 0)]
|
batch = []
|
||||||
|
if self.start_token is not None:
|
||||||
|
batch.append((self.start_token, 1.0, 0))
|
||||||
batched_tokens.append(batch)
|
batched_tokens.append(batch)
|
||||||
for i, t_group in enumerate(tokens):
|
for i, t_group in enumerate(tokens):
|
||||||
#determine if we're going to try and keep the tokens in a single batch
|
#determine if we're going to try and keep the tokens in a single batch
|
||||||
@ -435,16 +476,21 @@ class SDTokenizer:
|
|||||||
#add end token and pad
|
#add end token and pad
|
||||||
else:
|
else:
|
||||||
batch.append((self.end_token, 1.0, 0))
|
batch.append((self.end_token, 1.0, 0))
|
||||||
batch.extend([(pad_token, 1.0, 0)] * (remaining_length))
|
if self.pad_to_max_length:
|
||||||
|
batch.extend([(pad_token, 1.0, 0)] * (remaining_length))
|
||||||
#start new batch
|
#start new batch
|
||||||
batch = [(self.start_token, 1.0, 0)]
|
batch = []
|
||||||
|
if self.start_token is not None:
|
||||||
|
batch.append((self.start_token, 1.0, 0))
|
||||||
batched_tokens.append(batch)
|
batched_tokens.append(batch)
|
||||||
else:
|
else:
|
||||||
batch.extend([(t,w,i+1) for t,w in t_group])
|
batch.extend([(t,w,i+1) for t,w in t_group])
|
||||||
t_group = []
|
t_group = []
|
||||||
|
|
||||||
#fill last batch
|
#fill last batch
|
||||||
batch.extend([(self.end_token, 1.0, 0)] + [(pad_token, 1.0, 0)] * (self.max_length - len(batch) - 1))
|
batch.append((self.end_token, 1.0, 0))
|
||||||
|
if self.pad_to_max_length:
|
||||||
|
batch.extend([(pad_token, 1.0, 0)] * (self.max_length - len(batch)))
|
||||||
|
|
||||||
if not return_word_ids:
|
if not return_word_ids:
|
||||||
batched_tokens = [[(t, w) for t, w,_ in x] for x in batched_tokens]
|
batched_tokens = [[(t, w) for t, w,_ in x] for x in batched_tokens]
|
||||||
|
|||||||
@ -9,8 +9,7 @@ class SD2ClipHModel(sd1_clip.SDClipModel):
|
|||||||
layer_idx=23
|
layer_idx=23
|
||||||
|
|
||||||
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd2_clip_config.json")
|
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sd2_clip_config.json")
|
||||||
super().__init__(device=device, freeze=freeze, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, textmodel_path=textmodel_path, dtype=dtype)
|
super().__init__(device=device, freeze=freeze, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, textmodel_path=textmodel_path, dtype=dtype, special_tokens={"start": 49406, "end": 49407, "pad": 0})
|
||||||
self.empty_tokens = [[49406] + [49407] + [0] * 75]
|
|
||||||
|
|
||||||
class SD2ClipHTokenizer(sd1_clip.SDTokenizer):
|
class SD2ClipHTokenizer(sd1_clip.SDTokenizer):
|
||||||
def __init__(self, tokenizer_path=None, embedding_directory=None):
|
def __init__(self, tokenizer_path=None, embedding_directory=None):
|
||||||
|
|||||||
@ -9,9 +9,8 @@ class SDXLClipG(sd1_clip.SDClipModel):
|
|||||||
layer_idx=-2
|
layer_idx=-2
|
||||||
|
|
||||||
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "clip_config_bigg.json")
|
textmodel_json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "clip_config_bigg.json")
|
||||||
super().__init__(device=device, freeze=freeze, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, textmodel_path=textmodel_path, dtype=dtype)
|
super().__init__(device=device, freeze=freeze, layer=layer, layer_idx=layer_idx, textmodel_json_config=textmodel_json_config, textmodel_path=textmodel_path, dtype=dtype,
|
||||||
self.empty_tokens = [[49406] + [49407] + [0] * 75]
|
special_tokens={"start": 49406, "end": 49407, "pad": 0}, layer_norm_hidden_state=False)
|
||||||
self.layer_norm_hidden_state = False
|
|
||||||
|
|
||||||
def load_sd(self, sd):
|
def load_sd(self, sd):
|
||||||
return super().load_sd(sd)
|
return super().load_sd(sd)
|
||||||
@ -38,8 +37,7 @@ class SDXLTokenizer:
|
|||||||
class SDXLClipModel(torch.nn.Module):
|
class SDXLClipModel(torch.nn.Module):
|
||||||
def __init__(self, device="cpu", dtype=None):
|
def __init__(self, device="cpu", dtype=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.clip_l = sd1_clip.SDClipModel(layer="hidden", layer_idx=11, device=device, dtype=dtype)
|
self.clip_l = sd1_clip.SDClipModel(layer="hidden", layer_idx=11, device=device, dtype=dtype, layer_norm_hidden_state=False)
|
||||||
self.clip_l.layer_norm_hidden_state = False
|
|
||||||
self.clip_g = SDXLClipG(device=device, dtype=dtype)
|
self.clip_g = SDXLClipG(device=device, dtype=dtype)
|
||||||
|
|
||||||
def clip_layer(self, layer_idx):
|
def clip_layer(self, layer_idx):
|
||||||
|
|||||||
57
comfy_extras/nodes_model_advanced.py
Normal file
57
comfy_extras/nodes_model_advanced.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import folder_paths
|
||||||
|
import comfy.sd
|
||||||
|
import comfy.model_sampling
|
||||||
|
|
||||||
|
|
||||||
|
def rescale_zero_terminal_snr_sigmas(sigmas):
|
||||||
|
alphas_cumprod = 1 / ((sigmas * sigmas) + 1)
|
||||||
|
alphas_bar_sqrt = alphas_cumprod.sqrt()
|
||||||
|
|
||||||
|
# Store old values.
|
||||||
|
alphas_bar_sqrt_0 = alphas_bar_sqrt[0].clone()
|
||||||
|
alphas_bar_sqrt_T = alphas_bar_sqrt[-1].clone()
|
||||||
|
|
||||||
|
# Shift so the last timestep is zero.
|
||||||
|
alphas_bar_sqrt -= (alphas_bar_sqrt_T)
|
||||||
|
|
||||||
|
# Scale so the first timestep is back to the old value.
|
||||||
|
alphas_bar_sqrt *= alphas_bar_sqrt_0 / (alphas_bar_sqrt_0 - alphas_bar_sqrt_T)
|
||||||
|
|
||||||
|
# Convert alphas_bar_sqrt to betas
|
||||||
|
alphas_bar = alphas_bar_sqrt**2 # Revert sqrt
|
||||||
|
alphas_bar[-1] = 4.8973451890853435e-08
|
||||||
|
return ((1 - alphas_bar) / alphas_bar) ** 0.5
|
||||||
|
|
||||||
|
class ModelSamplingDiscrete:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {"required": { "model": ("MODEL",),
|
||||||
|
"sampling": (["eps", "v_prediction"],),
|
||||||
|
"zsnr": ("BOOLEAN", {"default": False}),
|
||||||
|
}}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("MODEL",)
|
||||||
|
FUNCTION = "patch"
|
||||||
|
|
||||||
|
CATEGORY = "advanced/model"
|
||||||
|
|
||||||
|
def patch(self, model, sampling, zsnr):
|
||||||
|
m = model.clone()
|
||||||
|
|
||||||
|
if sampling == "eps":
|
||||||
|
sampling_type = comfy.model_sampling.EPS
|
||||||
|
elif sampling == "v_prediction":
|
||||||
|
sampling_type = comfy.model_sampling.V_PREDICTION
|
||||||
|
|
||||||
|
class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingDiscrete, sampling_type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
model_sampling = ModelSamplingAdvanced()
|
||||||
|
if zsnr:
|
||||||
|
model_sampling.set_sigmas(rescale_zero_terminal_snr_sigmas(model_sampling.sigmas))
|
||||||
|
m.add_object_patch("model_sampling", model_sampling)
|
||||||
|
return (m, )
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ModelSamplingDiscrete": ModelSamplingDiscrete,
|
||||||
|
}
|
||||||
@ -23,7 +23,7 @@ class Blend:
|
|||||||
"max": 1.0,
|
"max": 1.0,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
}),
|
}),
|
||||||
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light"],),
|
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light", "difference"],),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +54,8 @@ class Blend:
|
|||||||
return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2))
|
return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2))
|
||||||
elif mode == "soft_light":
|
elif mode == "soft_light":
|
||||||
return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1), img1 + (2 * img2 - 1) * (self.g(img1) - img1))
|
return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1), img1 + (2 * img2 - 1) * (self.g(img1) - img1))
|
||||||
|
elif mode == "difference":
|
||||||
|
return img1 - img2
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported blend mode: {mode}")
|
raise ValueError(f"Unsupported blend mode: {mode}")
|
||||||
|
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@ -1798,6 +1798,7 @@ def init_custom_nodes():
|
|||||||
"nodes_freelunch.py",
|
"nodes_freelunch.py",
|
||||||
"nodes_custom_sampler.py",
|
"nodes_custom_sampler.py",
|
||||||
"nodes_hypertile.py",
|
"nodes_hypertile.py",
|
||||||
|
"nodes_model_advanced.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
for node_file in extras_files:
|
for node_file in extras_files:
|
||||||
|
|||||||
@ -5,6 +5,22 @@ import { api } from "./api.js";
|
|||||||
import { defaultGraph } from "./defaultGraph.js";
|
import { defaultGraph } from "./defaultGraph.js";
|
||||||
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
||||||
|
|
||||||
|
|
||||||
|
function sanitizeNodeName(string) {
|
||||||
|
let entityMap = {
|
||||||
|
'&': '',
|
||||||
|
'<': '',
|
||||||
|
'>': '',
|
||||||
|
'"': '',
|
||||||
|
"'": '',
|
||||||
|
'`': '',
|
||||||
|
'=': ''
|
||||||
|
};
|
||||||
|
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
|
||||||
|
return entityMap[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import("types/comfy").ComfyExtension} ComfyExtension
|
* @typedef {import("types/comfy").ComfyExtension} ComfyExtension
|
||||||
*/
|
*/
|
||||||
@ -1495,6 +1511,7 @@ export class ComfyApp {
|
|||||||
|
|
||||||
// Find missing node types
|
// Find missing node types
|
||||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||||
|
n.type = sanitizeNodeName(n.type);
|
||||||
missingNodeTypes.push(n.type);
|
missingNodeTypes.push(n.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user