mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-13 15:07:13 +08:00
Merge branch 'master' into patch_hooks
This commit is contained in:
commit
06fbdb03ef
9
.github/workflows/test-unit.yml
vendored
9
.github/workflows/test-unit.yml
vendored
@ -8,10 +8,15 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v4
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
|
|||||||
@ -10,14 +10,14 @@ def get_logs():
|
|||||||
return "\n".join([formatter.format(x) for x in logs])
|
return "\n".join([formatter.format(x) for x in logs])
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(verbose: bool = False, capacity: int = 300):
|
def setup_logger(log_level: str = 'INFO', capacity: int = 300):
|
||||||
global logs
|
global logs
|
||||||
if logs:
|
if logs:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Setup default global logger
|
# Setup default global logger
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
logger.setLevel(log_level)
|
||||||
|
|
||||||
stream_handler = logging.StreamHandler()
|
stream_handler = logging.StreamHandler()
|
||||||
stream_handler.setFormatter(logging.Formatter("%(message)s"))
|
stream_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class EnumAction(argparse.Action):
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
parser.add_argument("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0", help="Specify the IP address to listen on (default: 127.0.0.1). If --listen is provided without an argument, it defaults to 0.0.0.0. (listens on all)")
|
parser.add_argument("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0,::", help="Specify the IP address to listen on (default: 127.0.0.1). You can give a list of ip addresses by separating them with a comma like: 127.2.2.2,127.3.3.3 If --listen is provided without an argument, it defaults to 0.0.0.0,:: (listens on all ipv4 and ipv6)")
|
||||||
parser.add_argument("--port", type=int, default=8188, help="Set the listen port.")
|
parser.add_argument("--port", type=int, default=8188, help="Set the listen port.")
|
||||||
parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file. Enables TLS, makes app accessible at https://... requires --tls-certfile to function")
|
parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file. Enables TLS, makes app accessible at https://... requires --tls-certfile to function")
|
||||||
parser.add_argument("--tls-certfile", type=str, help="Path to TLS (SSL) certificate file. Enables TLS, makes app accessible at https://... requires --tls-keyfile to function")
|
parser.add_argument("--tls-certfile", type=str, help="Path to TLS (SSL) certificate file. Enables TLS, makes app accessible at https://... requires --tls-keyfile to function")
|
||||||
@ -136,7 +136,7 @@ parser.add_argument("--disable-all-custom-nodes", action="store_true", help="Dis
|
|||||||
|
|
||||||
parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.")
|
parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.")
|
||||||
|
|
||||||
parser.add_argument("--verbose", action="store_true", help="Enables more debug prints.")
|
parser.add_argument("--verbose", default='INFO', const='DEBUG', nargs="?", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Set the logging level')
|
||||||
|
|
||||||
# The default built-in provider hosted under web/
|
# The default built-in provider hosted under web/
|
||||||
DEFAULT_VERSION_STRING = "comfyanonymous/ComfyUI@latest"
|
DEFAULT_VERSION_STRING = "comfyanonymous/ComfyUI@latest"
|
||||||
|
|||||||
@ -226,7 +226,7 @@ class ControlNet(ControlBase):
|
|||||||
compression_ratio *= self.vae.downscale_ratio
|
compression_ratio *= self.vae.downscale_ratio
|
||||||
else:
|
else:
|
||||||
if self.latent_format is not None:
|
if self.latent_format is not None:
|
||||||
raise ValueError("This Controlnet needs a VAE but none was provided, please use a different ControlNetApply node with a VAE input.")
|
raise ValueError("This Controlnet needs a VAE but none was provided, please use a ControlNetApply node with a VAE input and connect it.")
|
||||||
self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * compression_ratio, x_noisy.shape[2] * compression_ratio, self.upscale_algorithm, "center")
|
self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * compression_ratio, x_noisy.shape[2] * compression_ratio, self.upscale_algorithm, "center")
|
||||||
if self.vae is not None:
|
if self.vae is not None:
|
||||||
loaded_models = comfy.model_management.loaded_models(only_currently_used=True)
|
loaded_models = comfy.model_management.loaded_models(only_currently_used=True)
|
||||||
|
|||||||
@ -1154,3 +1154,36 @@ def sample_dpmpp_2s_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback
|
|||||||
if sigmas[i + 1] > 0:
|
if sigmas[i + 1] > 0:
|
||||||
x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
|
x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def sample_dpmpp_2m_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disable=None):
|
||||||
|
"""DPM-Solver++(2M)."""
|
||||||
|
extra_args = {} if extra_args is None else extra_args
|
||||||
|
s_in = x.new_ones([x.shape[0]])
|
||||||
|
t_fn = lambda sigma: sigma.log().neg()
|
||||||
|
|
||||||
|
old_uncond_denoised = None
|
||||||
|
uncond_denoised = None
|
||||||
|
def post_cfg_function(args):
|
||||||
|
nonlocal uncond_denoised
|
||||||
|
uncond_denoised = args["uncond_denoised"]
|
||||||
|
return args["denoised"]
|
||||||
|
|
||||||
|
model_options = extra_args.get("model_options", {}).copy()
|
||||||
|
extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True)
|
||||||
|
|
||||||
|
for i in trange(len(sigmas) - 1, disable=disable):
|
||||||
|
denoised = model(x, sigmas[i] * s_in, **extra_args)
|
||||||
|
if callback is not None:
|
||||||
|
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
|
||||||
|
t, t_next = t_fn(sigmas[i]), t_fn(sigmas[i + 1])
|
||||||
|
h = t_next - t
|
||||||
|
if old_uncond_denoised is None or sigmas[i + 1] == 0:
|
||||||
|
denoised_mix = -torch.exp(-h) * uncond_denoised
|
||||||
|
else:
|
||||||
|
h_last = t - t_fn(sigmas[i - 1])
|
||||||
|
r = h_last / h
|
||||||
|
denoised_mix = -torch.exp(-h) * uncond_denoised - torch.expm1(-h) * (1 / (2 * r)) * (denoised - old_uncond_denoised)
|
||||||
|
x = denoised + denoised_mix + torch.exp(-h) * x
|
||||||
|
old_uncond_denoised = uncond_denoised
|
||||||
|
return x
|
||||||
@ -4,6 +4,7 @@ class LatentFormat:
|
|||||||
scale_factor = 1.0
|
scale_factor = 1.0
|
||||||
latent_channels = 4
|
latent_channels = 4
|
||||||
latent_rgb_factors = None
|
latent_rgb_factors = None
|
||||||
|
latent_rgb_factors_bias = None
|
||||||
taesd_decoder_name = None
|
taesd_decoder_name = None
|
||||||
|
|
||||||
def process_in(self, latent):
|
def process_in(self, latent):
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class Flux(nn.Module):
|
|||||||
raise ValueError("Didn't get guidance strength for guidance distilled model.")
|
raise ValueError("Didn't get guidance strength for guidance distilled model.")
|
||||||
vec = vec + self.guidance_in(timestep_embedding(guidance, 256).to(img.dtype))
|
vec = vec + self.guidance_in(timestep_embedding(guidance, 256).to(img.dtype))
|
||||||
|
|
||||||
vec = vec + self.vector_in(y)
|
vec = vec + self.vector_in(y[:,:self.params.vec_in_dim])
|
||||||
txt = self.txt_in(txt)
|
txt = self.txt_in(txt)
|
||||||
|
|
||||||
ids = torch.cat((txt_ids, img_ids), dim=1)
|
ids = torch.cat((txt_ids, img_ids), dim=1)
|
||||||
@ -151,8 +151,8 @@ class Flux(nn.Module):
|
|||||||
h_len = ((h + (patch_size // 2)) // patch_size)
|
h_len = ((h + (patch_size // 2)) // patch_size)
|
||||||
w_len = ((w + (patch_size // 2)) // patch_size)
|
w_len = ((w + (patch_size // 2)) // patch_size)
|
||||||
img_ids = torch.zeros((h_len, w_len, 3), device=x.device, dtype=x.dtype)
|
img_ids = torch.zeros((h_len, w_len, 3), device=x.device, dtype=x.dtype)
|
||||||
img_ids[..., 1] = img_ids[..., 1] + torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype)[:, None]
|
img_ids[:, :, 1] = torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype).unsqueeze(1)
|
||||||
img_ids[..., 2] = img_ids[..., 2] + torch.linspace(0, w_len - 1, steps=w_len, device=x.device, dtype=x.dtype)[None, :]
|
img_ids[:, :, 2] = torch.linspace(0, w_len - 1, steps=w_len, device=x.device, dtype=x.dtype).unsqueeze(0)
|
||||||
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
|
img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs)
|
||||||
|
|
||||||
txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype)
|
txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype)
|
||||||
|
|||||||
@ -295,6 +295,7 @@ def model_lora_keys_unet(model, key_map={}):
|
|||||||
unet_key = "diffusion_model.{}".format(diffusers_keys[k])
|
unet_key = "diffusion_model.{}".format(diffusers_keys[k])
|
||||||
key_lora = k[:-len(".weight")].replace(".", "_")
|
key_lora = k[:-len(".weight")].replace(".", "_")
|
||||||
key_map["lora_unet_{}".format(key_lora)] = unet_key
|
key_map["lora_unet_{}".format(key_lora)] = unet_key
|
||||||
|
key_map["lycoris_{}".format(key_lora)] = unet_key #simpletuner lycoris format
|
||||||
|
|
||||||
diffusers_lora_prefix = ["", "unet."]
|
diffusers_lora_prefix = ["", "unet."]
|
||||||
for p in diffusers_lora_prefix:
|
for p in diffusers_lora_prefix:
|
||||||
|
|||||||
@ -110,8 +110,12 @@ class LowVramPatch:
|
|||||||
self.key = key
|
self.key = key
|
||||||
self.patches = patches
|
self.patches = patches
|
||||||
def __call__(self, weight):
|
def __call__(self, weight):
|
||||||
return comfy.lora.calculate_weight(self.patches[self.key], weight, self.key, intermediate_dtype=weight.dtype)
|
intermediate_dtype = weight.dtype
|
||||||
|
if intermediate_dtype not in [torch.float32, torch.float16, torch.bfloat16]: #intermediate_dtype has to be one that is supported in math ops
|
||||||
|
intermediate_dtype = torch.float32
|
||||||
|
return comfy.float.stochastic_rounding(comfy.lora.calculate_weight(self.patches[self.key], weight.to(intermediate_dtype), self.key, intermediate_dtype=intermediate_dtype), weight.dtype, seed=string_to_seed(self.key))
|
||||||
|
|
||||||
|
return comfy.lora.calculate_weight(self.patches[self.key], weight, self.key, intermediate_dtype=intermediate_dtype)
|
||||||
class CallbacksMP:
|
class CallbacksMP:
|
||||||
ON_CLONE = "on_clone"
|
ON_CLONE = "on_clone"
|
||||||
ON_LOAD = "on_load_after"
|
ON_LOAD = "on_load_after"
|
||||||
|
|||||||
@ -260,7 +260,6 @@ def fp8_linear(self, input):
|
|||||||
|
|
||||||
if len(input.shape) == 3:
|
if len(input.shape) == 3:
|
||||||
inn = input.reshape(-1, input.shape[2]).to(dtype)
|
inn = input.reshape(-1, input.shape[2]).to(dtype)
|
||||||
non_blocking = comfy.model_management.device_supports_non_blocking(input.device)
|
|
||||||
w, bias = cast_bias_weight(self, input, dtype=dtype, bias_dtype=input.dtype)
|
w, bias = cast_bias_weight(self, input, dtype=dtype, bias_dtype=input.dtype)
|
||||||
w = w.t()
|
w = w.t()
|
||||||
|
|
||||||
|
|||||||
@ -644,7 +644,7 @@ class Sampler:
|
|||||||
|
|
||||||
KSAMPLER_NAMES = ["euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp", "heun", "heunpp2","dpm_2", "dpm_2_ancestral",
|
KSAMPLER_NAMES = ["euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp", "heun", "heunpp2","dpm_2", "dpm_2_ancestral",
|
||||||
"lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp", "dpmpp_sde", "dpmpp_sde_gpu",
|
"lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp", "dpmpp_sde", "dpmpp_sde_gpu",
|
||||||
"dpmpp_2m", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm",
|
"dpmpp_2m", "dpmpp_2m_cfg_pp", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm",
|
||||||
"ipndm", "ipndm_v", "deis"]
|
"ipndm", "ipndm_v", "deis"]
|
||||||
|
|
||||||
class KSAMPLER(Sampler):
|
class KSAMPLER(Sampler):
|
||||||
|
|||||||
61
comfy/sd.py
61
comfy/sd.py
@ -406,6 +406,32 @@ def load_clip(ckpt_paths, embedding_directory=None, clip_type=CLIPType.STABLE_DI
|
|||||||
clip_data.append(comfy.utils.load_torch_file(p, safe_load=True))
|
clip_data.append(comfy.utils.load_torch_file(p, safe_load=True))
|
||||||
return load_text_encoder_state_dicts(clip_data, embedding_directory=embedding_directory, clip_type=clip_type, model_options=model_options)
|
return load_text_encoder_state_dicts(clip_data, embedding_directory=embedding_directory, clip_type=clip_type, model_options=model_options)
|
||||||
|
|
||||||
|
|
||||||
|
class TEModel(Enum):
|
||||||
|
CLIP_L = 1
|
||||||
|
CLIP_H = 2
|
||||||
|
CLIP_G = 3
|
||||||
|
T5_XXL = 4
|
||||||
|
T5_XL = 5
|
||||||
|
T5_BASE = 6
|
||||||
|
|
||||||
|
def detect_te_model(sd):
|
||||||
|
if "text_model.encoder.layers.30.mlp.fc1.weight" in sd:
|
||||||
|
return TEModel.CLIP_G
|
||||||
|
if "text_model.encoder.layers.22.mlp.fc1.weight" in sd:
|
||||||
|
return TEModel.CLIP_H
|
||||||
|
if "text_model.encoder.layers.0.mlp.fc1.weight" in sd:
|
||||||
|
return TEModel.CLIP_L
|
||||||
|
if "encoder.block.23.layer.1.DenseReluDense.wi_1.weight" in sd:
|
||||||
|
weight = sd["encoder.block.23.layer.1.DenseReluDense.wi_1.weight"]
|
||||||
|
if weight.shape[-1] == 4096:
|
||||||
|
return TEModel.T5_XXL
|
||||||
|
elif weight.shape[-1] == 2048:
|
||||||
|
return TEModel.T5_XL
|
||||||
|
if "encoder.block.0.layer.0.SelfAttention.k.weight" in sd:
|
||||||
|
return TEModel.T5_BASE
|
||||||
|
return None
|
||||||
|
|
||||||
def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip_type=CLIPType.STABLE_DIFFUSION, model_options={}):
|
def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip_type=CLIPType.STABLE_DIFFUSION, model_options={}):
|
||||||
clip_data = state_dicts
|
clip_data = state_dicts
|
||||||
class EmptyClass:
|
class EmptyClass:
|
||||||
@ -421,35 +447,42 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip
|
|||||||
clip_target = EmptyClass()
|
clip_target = EmptyClass()
|
||||||
clip_target.params = {}
|
clip_target.params = {}
|
||||||
if len(clip_data) == 1:
|
if len(clip_data) == 1:
|
||||||
if "text_model.encoder.layers.30.mlp.fc1.weight" in clip_data[0]:
|
te_model = detect_te_model(clip_data[0])
|
||||||
|
if te_model == TEModel.CLIP_G:
|
||||||
if clip_type == CLIPType.STABLE_CASCADE:
|
if clip_type == CLIPType.STABLE_CASCADE:
|
||||||
clip_target.clip = sdxl_clip.StableCascadeClipModel
|
clip_target.clip = sdxl_clip.StableCascadeClipModel
|
||||||
clip_target.tokenizer = sdxl_clip.StableCascadeTokenizer
|
clip_target.tokenizer = sdxl_clip.StableCascadeTokenizer
|
||||||
|
elif clip_type == CLIPType.SD3:
|
||||||
|
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=False, clip_g=True, t5=False)
|
||||||
|
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
||||||
else:
|
else:
|
||||||
clip_target.clip = sdxl_clip.SDXLRefinerClipModel
|
clip_target.clip = sdxl_clip.SDXLRefinerClipModel
|
||||||
clip_target.tokenizer = sdxl_clip.SDXLTokenizer
|
clip_target.tokenizer = sdxl_clip.SDXLTokenizer
|
||||||
elif "text_model.encoder.layers.22.mlp.fc1.weight" in clip_data[0]:
|
elif te_model == TEModel.CLIP_H:
|
||||||
clip_target.clip = comfy.text_encoders.sd2_clip.SD2ClipModel
|
clip_target.clip = comfy.text_encoders.sd2_clip.SD2ClipModel
|
||||||
clip_target.tokenizer = comfy.text_encoders.sd2_clip.SD2Tokenizer
|
clip_target.tokenizer = comfy.text_encoders.sd2_clip.SD2Tokenizer
|
||||||
elif "encoder.block.23.layer.1.DenseReluDense.wi_1.weight" in clip_data[0]:
|
elif te_model == TEModel.T5_XXL:
|
||||||
weight = clip_data[0]["encoder.block.23.layer.1.DenseReluDense.wi_1.weight"]
|
weight = clip_data[0]["encoder.block.23.layer.1.DenseReluDense.wi_1.weight"]
|
||||||
dtype_t5 = weight.dtype
|
dtype_t5 = weight.dtype
|
||||||
if weight.shape[-1] == 4096:
|
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=False, clip_g=False, t5=True, dtype_t5=dtype_t5)
|
||||||
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=False, clip_g=False, t5=True, dtype_t5=dtype_t5)
|
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
||||||
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
elif te_model == TEModel.T5_XL:
|
||||||
elif weight.shape[-1] == 2048:
|
clip_target.clip = comfy.text_encoders.aura_t5.AuraT5Model
|
||||||
clip_target.clip = comfy.text_encoders.aura_t5.AuraT5Model
|
clip_target.tokenizer = comfy.text_encoders.aura_t5.AuraT5Tokenizer
|
||||||
clip_target.tokenizer = comfy.text_encoders.aura_t5.AuraT5Tokenizer
|
elif te_model == TEModel.T5_BASE:
|
||||||
elif "encoder.block.0.layer.0.SelfAttention.k.weight" in clip_data[0]:
|
|
||||||
clip_target.clip = comfy.text_encoders.sa_t5.SAT5Model
|
clip_target.clip = comfy.text_encoders.sa_t5.SAT5Model
|
||||||
clip_target.tokenizer = comfy.text_encoders.sa_t5.SAT5Tokenizer
|
clip_target.tokenizer = comfy.text_encoders.sa_t5.SAT5Tokenizer
|
||||||
else:
|
else:
|
||||||
w = clip_data[0].get("text_model.embeddings.position_embedding.weight", None)
|
if clip_type == CLIPType.SD3:
|
||||||
clip_target.clip = sd1_clip.SD1ClipModel
|
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=True, clip_g=False, t5=False)
|
||||||
clip_target.tokenizer = sd1_clip.SD1Tokenizer
|
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
||||||
|
else:
|
||||||
|
clip_target.clip = sd1_clip.SD1ClipModel
|
||||||
|
clip_target.tokenizer = sd1_clip.SD1Tokenizer
|
||||||
elif len(clip_data) == 2:
|
elif len(clip_data) == 2:
|
||||||
if clip_type == CLIPType.SD3:
|
if clip_type == CLIPType.SD3:
|
||||||
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=True, clip_g=True, t5=False)
|
te_models = [detect_te_model(clip_data[0]), detect_te_model(clip_data[1])]
|
||||||
|
clip_target.clip = comfy.text_encoders.sd3_clip.sd3_clip(clip_l=TEModel.CLIP_L in te_models, clip_g=TEModel.CLIP_G in te_models, t5=TEModel.T5_XXL in te_models)
|
||||||
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
clip_target.tokenizer = comfy.text_encoders.sd3_clip.SD3Tokenizer
|
||||||
elif clip_type == CLIPType.HUNYUAN_DIT:
|
elif clip_type == CLIPType.HUNYUAN_DIT:
|
||||||
clip_target.clip = comfy.text_encoders.hydit.HyditModel
|
clip_target.clip = comfy.text_encoders.hydit.HyditModel
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class T5XXLModel(sd1_clip.SDClipModel):
|
|||||||
class T5XXLTokenizer(sd1_clip.SDTokenizer):
|
class T5XXLTokenizer(sd1_clip.SDTokenizer):
|
||||||
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
||||||
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_tokenizer")
|
tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "t5_tokenizer")
|
||||||
super().__init__(tokenizer_path, pad_with_end=False, embedding_size=4096, embedding_key='t5xxl', tokenizer_class=T5TokenizerFast, has_start_token=False, pad_to_max_length=False, max_length=99999999, min_length=256)
|
super().__init__(tokenizer_path, embedding_directory=embedding_directory, pad_with_end=False, embedding_size=4096, embedding_key='t5xxl', tokenizer_class=T5TokenizerFast, has_start_token=False, pad_to_max_length=False, max_length=99999999, min_length=256)
|
||||||
|
|
||||||
|
|
||||||
class FluxTokenizer:
|
class FluxTokenizer:
|
||||||
|
|||||||
@ -16,14 +16,15 @@ class EmptyLatentAudio:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required": {"seconds": ("FLOAT", {"default": 47.6, "min": 1.0, "max": 1000.0, "step": 0.1})}}
|
return {"required": {"seconds": ("FLOAT", {"default": 47.6, "min": 1.0, "max": 1000.0, "step": 0.1}),
|
||||||
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}),
|
||||||
|
}}
|
||||||
RETURN_TYPES = ("LATENT",)
|
RETURN_TYPES = ("LATENT",)
|
||||||
FUNCTION = "generate"
|
FUNCTION = "generate"
|
||||||
|
|
||||||
CATEGORY = "latent/audio"
|
CATEGORY = "latent/audio"
|
||||||
|
|
||||||
def generate(self, seconds):
|
def generate(self, seconds, batch_size):
|
||||||
batch_size = 1
|
|
||||||
length = round((seconds * 44100 / 2048) / 2) * 2
|
length = round((seconds * 44100 / 2048) / 2) * 2
|
||||||
latent = torch.zeros([batch_size, 64, length], device=self.device)
|
latent = torch.zeros([batch_size, 64, length], device=self.device)
|
||||||
return ({"samples":latent, "type": "audio"}, )
|
return ({"samples":latent, "type": "audio"}, )
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class PatchModelAddDownscale:
|
|||||||
RETURN_TYPES = ("MODEL",)
|
RETURN_TYPES = ("MODEL",)
|
||||||
FUNCTION = "patch"
|
FUNCTION = "patch"
|
||||||
|
|
||||||
CATEGORY = "_for_testing"
|
CATEGORY = "model_patches/unet"
|
||||||
|
|
||||||
def patch(self, model, block_number, downscale_factor, start_percent, end_percent, downscale_after_skip, downscale_method, upscale_method):
|
def patch(self, model, block_number, downscale_factor, start_percent, end_percent, downscale_after_skip, downscale_method, upscale_method):
|
||||||
model_sampling = model.get_model_object("model_sampling")
|
model_sampling = model.get_model_object("model_sampling")
|
||||||
|
|||||||
@ -93,6 +93,7 @@ class ControlNetApplySD3(nodes.ControlNetApplyAdvanced):
|
|||||||
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
|
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
|
||||||
}}
|
}}
|
||||||
CATEGORY = "conditioning/controlnet"
|
CATEGORY = "conditioning/controlnet"
|
||||||
|
DEPRECATED = True
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"TripleCLIPLoader": TripleCLIPLoader,
|
"TripleCLIPLoader": TripleCLIPLoader,
|
||||||
@ -103,5 +104,5 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
# Sampling
|
# Sampling
|
||||||
"ControlNetApplySD3": "Apply Controlnet",
|
"ControlNetApplySD3": "Apply Controlnet with VAE",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,7 @@ class StableCascade_SuperResolutionControlnet:
|
|||||||
RETURN_NAMES = ("controlnet_input", "stage_c", "stage_b")
|
RETURN_NAMES = ("controlnet_input", "stage_c", "stage_b")
|
||||||
FUNCTION = "generate"
|
FUNCTION = "generate"
|
||||||
|
|
||||||
|
EXPERIMENTAL = True
|
||||||
CATEGORY = "_for_testing/stable_cascade"
|
CATEGORY = "_for_testing/stable_cascade"
|
||||||
|
|
||||||
def generate(self, image, vae):
|
def generate(self, image, vae):
|
||||||
|
|||||||
@ -154,7 +154,7 @@ class TomePatchModel:
|
|||||||
RETURN_TYPES = ("MODEL",)
|
RETURN_TYPES = ("MODEL",)
|
||||||
FUNCTION = "patch"
|
FUNCTION = "patch"
|
||||||
|
|
||||||
CATEGORY = "_for_testing"
|
CATEGORY = "model_patches/unet"
|
||||||
|
|
||||||
def patch(self, model, ratio):
|
def patch(self, model, ratio):
|
||||||
self.u = None
|
self.u = None
|
||||||
|
|||||||
@ -4,6 +4,7 @@ class TorchCompileModel:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required": { "model": ("MODEL",),
|
return {"required": { "model": ("MODEL",),
|
||||||
|
"backend": (["inductor", "cudagraphs"],),
|
||||||
}}
|
}}
|
||||||
RETURN_TYPES = ("MODEL",)
|
RETURN_TYPES = ("MODEL",)
|
||||||
FUNCTION = "patch"
|
FUNCTION = "patch"
|
||||||
@ -11,9 +12,9 @@ class TorchCompileModel:
|
|||||||
CATEGORY = "_for_testing"
|
CATEGORY = "_for_testing"
|
||||||
EXPERIMENTAL = True
|
EXPERIMENTAL = True
|
||||||
|
|
||||||
def patch(self, model):
|
def patch(self, model, backend):
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
m.add_object_patch("diffusion_model", torch.compile(model=m.get_model_object("diffusion_model")))
|
m.add_object_patch("diffusion_model", torch.compile(model=m.get_model_object("diffusion_model"), backend=backend))
|
||||||
return (m, )
|
return (m, )
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
|||||||
@ -107,7 +107,7 @@ class VideoTriangleCFGGuidance:
|
|||||||
return (m, )
|
return (m, )
|
||||||
|
|
||||||
class ImageOnlyCheckpointSave(comfy_extras.nodes_model_merging.CheckpointSave):
|
class ImageOnlyCheckpointSave(comfy_extras.nodes_model_merging.CheckpointSave):
|
||||||
CATEGORY = "_for_testing"
|
CATEGORY = "advanced/model_merging"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
|
|||||||
@ -36,12 +36,20 @@ class TAESDPreviewerImpl(LatentPreviewer):
|
|||||||
|
|
||||||
|
|
||||||
class Latent2RGBPreviewer(LatentPreviewer):
|
class Latent2RGBPreviewer(LatentPreviewer):
|
||||||
def __init__(self, latent_rgb_factors):
|
def __init__(self, latent_rgb_factors, latent_rgb_factors_bias=None):
|
||||||
self.latent_rgb_factors = torch.tensor(latent_rgb_factors, device="cpu")
|
self.latent_rgb_factors = torch.tensor(latent_rgb_factors, device="cpu").transpose(0, 1)
|
||||||
|
self.latent_rgb_factors_bias = None
|
||||||
|
if latent_rgb_factors_bias is not None:
|
||||||
|
self.latent_rgb_factors_bias = torch.tensor(latent_rgb_factors_bias, device="cpu")
|
||||||
|
|
||||||
def decode_latent_to_preview(self, x0):
|
def decode_latent_to_preview(self, x0):
|
||||||
self.latent_rgb_factors = self.latent_rgb_factors.to(dtype=x0.dtype, device=x0.device)
|
self.latent_rgb_factors = self.latent_rgb_factors.to(dtype=x0.dtype, device=x0.device)
|
||||||
latent_image = x0[0].permute(1, 2, 0) @ self.latent_rgb_factors
|
if self.latent_rgb_factors_bias is not None:
|
||||||
|
self.latent_rgb_factors_bias = self.latent_rgb_factors_bias.to(dtype=x0.dtype, device=x0.device)
|
||||||
|
|
||||||
|
latent_image = torch.nn.functional.linear(x0[0].permute(1, 2, 0), self.latent_rgb_factors, bias=self.latent_rgb_factors_bias)
|
||||||
|
# latent_image = x0[0].permute(1, 2, 0) @ self.latent_rgb_factors
|
||||||
|
|
||||||
return preview_to_image(latent_image)
|
return preview_to_image(latent_image)
|
||||||
|
|
||||||
|
|
||||||
@ -71,7 +79,7 @@ def get_previewer(device, latent_format):
|
|||||||
|
|
||||||
if previewer is None:
|
if previewer is None:
|
||||||
if latent_format.latent_rgb_factors is not None:
|
if latent_format.latent_rgb_factors is not None:
|
||||||
previewer = Latent2RGBPreviewer(latent_format.latent_rgb_factors)
|
previewer = Latent2RGBPreviewer(latent_format.latent_rgb_factors, latent_format.latent_rgb_factors_bias)
|
||||||
return previewer
|
return previewer
|
||||||
|
|
||||||
def prepare_callback(model, steps, x0_output_dict=None):
|
def prepare_callback(model, steps, x0_output_dict=None):
|
||||||
|
|||||||
9
main.py
9
main.py
@ -9,7 +9,7 @@ from comfy.cli_args import args
|
|||||||
from app.logger import setup_logger
|
from app.logger import setup_logger
|
||||||
|
|
||||||
|
|
||||||
setup_logger(verbose=args.verbose)
|
setup_logger(log_level=args.verbose)
|
||||||
|
|
||||||
|
|
||||||
def execute_prestartup_script():
|
def execute_prestartup_script():
|
||||||
@ -160,7 +160,10 @@ def prompt_worker(q, server):
|
|||||||
need_gc = False
|
need_gc = False
|
||||||
|
|
||||||
async def run(server, address='', port=8188, verbose=True, call_on_start=None):
|
async def run(server, address='', port=8188, verbose=True, call_on_start=None):
|
||||||
await asyncio.gather(server.start(address, port, verbose, call_on_start), server.publish_loop())
|
addresses = []
|
||||||
|
for addr in address.split(","):
|
||||||
|
addresses.append((addr, port))
|
||||||
|
await asyncio.gather(server.start_multi_address(addresses, call_on_start), server.publish_loop())
|
||||||
|
|
||||||
|
|
||||||
def hijack_progress(server):
|
def hijack_progress(server):
|
||||||
@ -248,6 +251,8 @@ if __name__ == "__main__":
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
if os.name == 'nt' and address == '0.0.0.0':
|
if os.name == 'nt' and address == '0.0.0.0':
|
||||||
address = '127.0.0.1'
|
address = '127.0.0.1'
|
||||||
|
if ':' in address:
|
||||||
|
address = "[{}]".format(address)
|
||||||
webbrowser.open(f"{scheme}://{address}:{port}")
|
webbrowser.open(f"{scheme}://{address}:{port}")
|
||||||
call_on_start = startup_server
|
call_on_start = startup_server
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
# model_manager/__init__.py
|
# model_manager/__init__.py
|
||||||
from .download_models import download_model, DownloadModelStatus, DownloadStatusType, create_model_path, check_file_exists, track_download_progress, validate_model_subdirectory, validate_filename
|
from .download_models import download_model, DownloadModelStatus, DownloadStatusType, create_model_path, check_file_exists, track_download_progress, validate_filename
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
#NOTE: This was an experiment and WILL BE REMOVED
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import logging
|
import logging
|
||||||
from folder_paths import models_dir
|
from folder_paths import folder_names_and_paths, get_folder_paths
|
||||||
import re
|
import re
|
||||||
from typing import Callable, Any, Optional, Awaitable, Dict
|
from typing import Callable, Any, Optional, Awaitable, Dict
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@ -17,6 +18,7 @@ class DownloadStatusType(Enum):
|
|||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DownloadModelStatus():
|
class DownloadModelStatus():
|
||||||
status: str
|
status: str
|
||||||
@ -29,7 +31,7 @@ class DownloadModelStatus():
|
|||||||
self.progress_percentage = progress_percentage
|
self.progress_percentage = progress_percentage
|
||||||
self.message = message
|
self.message = message
|
||||||
self.already_existed = already_existed
|
self.already_existed = already_existed
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
@ -38,102 +40,112 @@ class DownloadModelStatus():
|
|||||||
"already_existed": self.already_existed
|
"already_existed": self.already_existed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def download_model(model_download_request: Callable[[str], Awaitable[aiohttp.ClientResponse]],
|
async def download_model(model_download_request: Callable[[str], Awaitable[aiohttp.ClientResponse]],
|
||||||
model_name: str,
|
model_name: str,
|
||||||
model_url: str,
|
model_url: str,
|
||||||
model_sub_directory: str,
|
model_directory: str,
|
||||||
|
folder_path: str,
|
||||||
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]],
|
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]],
|
||||||
progress_interval: float = 1.0) -> DownloadModelStatus:
|
progress_interval: float = 1.0) -> DownloadModelStatus:
|
||||||
"""
|
"""
|
||||||
Download a model file from a given URL into the models directory.
|
Download a model file from a given URL into the models directory.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_download_request (Callable[[str], Awaitable[aiohttp.ClientResponse]]):
|
model_download_request (Callable[[str], Awaitable[aiohttp.ClientResponse]]):
|
||||||
A function that makes an HTTP request. This makes it easier to mock in unit tests.
|
A function that makes an HTTP request. This makes it easier to mock in unit tests.
|
||||||
model_name (str):
|
model_name (str):
|
||||||
The name of the model file to be downloaded. This will be the filename on disk.
|
The name of the model file to be downloaded. This will be the filename on disk.
|
||||||
model_url (str):
|
model_url (str):
|
||||||
The URL from which to download the model.
|
The URL from which to download the model.
|
||||||
model_sub_directory (str):
|
model_directory (str):
|
||||||
The subdirectory within the main models directory where the model
|
The subdirectory within the main models directory where the model
|
||||||
should be saved (e.g., 'checkpoints', 'loras', etc.).
|
should be saved (e.g., 'checkpoints', 'loras', etc.).
|
||||||
progress_callback (Callable[[str, DownloadModelStatus], Awaitable[Any]]):
|
progress_callback (Callable[[str, DownloadModelStatus], Awaitable[Any]]):
|
||||||
An asynchronous function to call with progress updates.
|
An asynchronous function to call with progress updates.
|
||||||
|
folder_path (str);
|
||||||
|
Path to which model folder should be used as the root.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DownloadModelStatus: The result of the download operation.
|
DownloadModelStatus: The result of the download operation.
|
||||||
"""
|
"""
|
||||||
if not validate_model_subdirectory(model_sub_directory):
|
|
||||||
return DownloadModelStatus(
|
|
||||||
DownloadStatusType.ERROR,
|
|
||||||
0,
|
|
||||||
"Invalid model subdirectory",
|
|
||||||
False
|
|
||||||
)
|
|
||||||
|
|
||||||
if not validate_filename(model_name):
|
if not validate_filename(model_name):
|
||||||
return DownloadModelStatus(
|
return DownloadModelStatus(
|
||||||
DownloadStatusType.ERROR,
|
DownloadStatusType.ERROR,
|
||||||
0,
|
0,
|
||||||
"Invalid model name",
|
"Invalid model name",
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
|
|
||||||
file_path, relative_path = create_model_path(model_name, model_sub_directory, models_dir)
|
if not model_directory in folder_names_and_paths:
|
||||||
existing_file = await check_file_exists(file_path, model_name, progress_callback, relative_path)
|
return DownloadModelStatus(
|
||||||
|
DownloadStatusType.ERROR,
|
||||||
|
0,
|
||||||
|
"Invalid or unrecognized model directory. model_directory must be a known model type (eg 'checkpoints'). If you are seeing this error for a custom model type, ensure the relevant custom nodes are installed and working.",
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not folder_path in get_folder_paths(model_directory):
|
||||||
|
return DownloadModelStatus(
|
||||||
|
DownloadStatusType.ERROR,
|
||||||
|
0,
|
||||||
|
f"Invalid folder path '{folder_path}', does not match the list of known directories ({get_folder_paths(model_directory)}). If you're seeing this in the downloader UI, you may need to refresh the page.",
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = create_model_path(model_name, folder_path)
|
||||||
|
existing_file = await check_file_exists(file_path, model_name, progress_callback)
|
||||||
if existing_file:
|
if existing_file:
|
||||||
return existing_file
|
return existing_file
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logging.info(f"Downloading {model_name} from {model_url}")
|
||||||
status = DownloadModelStatus(DownloadStatusType.PENDING, 0, f"Starting download of {model_name}", False)
|
status = DownloadModelStatus(DownloadStatusType.PENDING, 0, f"Starting download of {model_name}", False)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
|
|
||||||
response = await model_download_request(model_url)
|
response = await model_download_request(model_url)
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_message = f"Failed to download {model_name}. Status code: {response.status}"
|
error_message = f"Failed to download {model_name}. Status code: {response.status}"
|
||||||
logging.error(error_message)
|
logging.error(error_message)
|
||||||
status = DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
status = DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
return DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
return DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
||||||
|
|
||||||
return await track_download_progress(response, file_path, model_name, progress_callback, relative_path, progress_interval)
|
return await track_download_progress(response, file_path, model_name, progress_callback, progress_interval)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in downloading model: {e}")
|
logging.error(f"Error in downloading model: {e}")
|
||||||
return await handle_download_error(e, model_name, progress_callback, relative_path)
|
return await handle_download_error(e, model_name, progress_callback)
|
||||||
|
|
||||||
|
|
||||||
def create_model_path(model_name: str, model_directory: str, models_base_dir: str) -> tuple[str, str]:
|
|
||||||
full_model_dir = os.path.join(models_base_dir, model_directory)
|
def create_model_path(model_name: str, folder_path: str) -> tuple[str, str]:
|
||||||
os.makedirs(full_model_dir, exist_ok=True)
|
os.makedirs(folder_path, exist_ok=True)
|
||||||
file_path = os.path.join(full_model_dir, model_name)
|
file_path = os.path.join(folder_path, model_name)
|
||||||
|
|
||||||
# Ensure the resulting path is still within the base directory
|
# Ensure the resulting path is still within the base directory
|
||||||
abs_file_path = os.path.abspath(file_path)
|
abs_file_path = os.path.abspath(file_path)
|
||||||
abs_base_dir = os.path.abspath(str(models_base_dir))
|
abs_base_dir = os.path.abspath(folder_path)
|
||||||
if os.path.commonprefix([abs_file_path, abs_base_dir]) != abs_base_dir:
|
if os.path.commonprefix([abs_file_path, abs_base_dir]) != abs_base_dir:
|
||||||
raise Exception(f"Invalid model directory: {model_directory}/{model_name}")
|
raise Exception(f"Invalid model directory: {folder_path}/{model_name}")
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
relative_path = '/'.join([model_directory, model_name])
|
async def check_file_exists(file_path: str,
|
||||||
return file_path, relative_path
|
model_name: str,
|
||||||
|
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]]
|
||||||
async def check_file_exists(file_path: str,
|
) -> Optional[DownloadModelStatus]:
|
||||||
model_name: str,
|
|
||||||
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]],
|
|
||||||
relative_path: str) -> Optional[DownloadModelStatus]:
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
status = DownloadModelStatus(DownloadStatusType.COMPLETED, 100, f"{model_name} already exists", True)
|
status = DownloadModelStatus(DownloadStatusType.COMPLETED, 100, f"{model_name} already exists", True)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
return status
|
return status
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def track_download_progress(response: aiohttp.ClientResponse,
|
async def track_download_progress(response: aiohttp.ClientResponse,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]],
|
progress_callback: Callable[[str, DownloadModelStatus], Awaitable[Any]],
|
||||||
relative_path: str,
|
|
||||||
interval: float = 1.0) -> DownloadModelStatus:
|
interval: float = 1.0) -> DownloadModelStatus:
|
||||||
try:
|
try:
|
||||||
total_size = int(response.headers.get('Content-Length', 0))
|
total_size = int(response.headers.get('Content-Length', 0))
|
||||||
@ -144,10 +156,11 @@ async def track_download_progress(response: aiohttp.ClientResponse,
|
|||||||
nonlocal last_update_time
|
nonlocal last_update_time
|
||||||
progress = (downloaded / total_size) * 100 if total_size > 0 else 0
|
progress = (downloaded / total_size) * 100 if total_size > 0 else 0
|
||||||
status = DownloadModelStatus(DownloadStatusType.IN_PROGRESS, progress, f"Downloading {model_name}", False)
|
status = DownloadModelStatus(DownloadStatusType.IN_PROGRESS, progress, f"Downloading {model_name}", False)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
last_update_time = time.time()
|
last_update_time = time.time()
|
||||||
|
|
||||||
with open(file_path, 'wb') as f:
|
temp_file_path = file_path + '.tmp'
|
||||||
|
with open(temp_file_path, 'wb') as f:
|
||||||
chunk_iterator = response.content.iter_chunked(8192)
|
chunk_iterator = response.content.iter_chunked(8192)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -156,58 +169,39 @@ async def track_download_progress(response: aiohttp.ClientResponse,
|
|||||||
break
|
break
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
downloaded += len(chunk)
|
downloaded += len(chunk)
|
||||||
|
|
||||||
if time.time() - last_update_time >= interval:
|
if time.time() - last_update_time >= interval:
|
||||||
await update_progress()
|
await update_progress()
|
||||||
|
|
||||||
|
os.rename(temp_file_path, file_path)
|
||||||
|
|
||||||
await update_progress()
|
await update_progress()
|
||||||
|
|
||||||
logging.info(f"Successfully downloaded {model_name}. Total downloaded: {downloaded}")
|
logging.info(f"Successfully downloaded {model_name}. Total downloaded: {downloaded}")
|
||||||
status = DownloadModelStatus(DownloadStatusType.COMPLETED, 100, f"Successfully downloaded {model_name}", False)
|
status = DownloadModelStatus(DownloadStatusType.COMPLETED, 100, f"Successfully downloaded {model_name}", False)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
|
|
||||||
return status
|
return status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in track_download_progress: {e}")
|
logging.error(f"Error in track_download_progress: {e}")
|
||||||
logging.error(traceback.format_exc())
|
logging.error(traceback.format_exc())
|
||||||
return await handle_download_error(e, model_name, progress_callback, relative_path)
|
return await handle_download_error(e, model_name, progress_callback)
|
||||||
|
|
||||||
async def handle_download_error(e: Exception,
|
|
||||||
model_name: str,
|
async def handle_download_error(e: Exception,
|
||||||
progress_callback: Callable[[str, DownloadModelStatus], Any],
|
model_name: str,
|
||||||
relative_path: str) -> DownloadModelStatus:
|
progress_callback: Callable[[str, DownloadModelStatus], Any]
|
||||||
|
) -> DownloadModelStatus:
|
||||||
error_message = f"Error downloading {model_name}: {str(e)}"
|
error_message = f"Error downloading {model_name}: {str(e)}"
|
||||||
status = DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
status = DownloadModelStatus(DownloadStatusType.ERROR, 0, error_message, False)
|
||||||
await progress_callback(relative_path, status)
|
await progress_callback(model_name, status)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def validate_model_subdirectory(model_subdirectory: str) -> bool:
|
|
||||||
"""
|
|
||||||
Validate that the model subdirectory is safe to install into.
|
|
||||||
Must not contain relative paths, nested paths or special characters
|
|
||||||
other than underscores and hyphens.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_subdirectory (str): The subdirectory for the specific model type.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the subdirectory is safe, False otherwise.
|
|
||||||
"""
|
|
||||||
if len(model_subdirectory) > 50:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if '..' in model_subdirectory or '/' in model_subdirectory:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not re.match(r'^[a-zA-Z0-9_-]+$', model_subdirectory):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate_filename(filename: str)-> bool:
|
def validate_filename(filename: str)-> bool:
|
||||||
"""
|
"""
|
||||||
Validate a filename to ensure it's safe and doesn't contain any path traversal attempts.
|
Validate a filename to ensure it's safe and doesn't contain any path traversal attempts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename (str): The filename to validate
|
filename (str): The filename to validate
|
||||||
|
|
||||||
|
|||||||
8
nodes.py
8
nodes.py
@ -787,6 +787,7 @@ class ControlNetApply:
|
|||||||
RETURN_TYPES = ("CONDITIONING",)
|
RETURN_TYPES = ("CONDITIONING",)
|
||||||
FUNCTION = "apply_controlnet"
|
FUNCTION = "apply_controlnet"
|
||||||
|
|
||||||
|
DEPRECATED = True
|
||||||
CATEGORY = "conditioning/controlnet"
|
CATEGORY = "conditioning/controlnet"
|
||||||
|
|
||||||
def apply_controlnet(self, conditioning, control_net, image, strength):
|
def apply_controlnet(self, conditioning, control_net, image, strength):
|
||||||
@ -816,7 +817,10 @@ class ControlNetApplyAdvanced:
|
|||||||
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||||
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
|
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
|
||||||
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
|
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
|
||||||
}}
|
},
|
||||||
|
"optional": {"vae": ("VAE", ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING","CONDITIONING")
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING")
|
||||||
RETURN_NAMES = ("positive", "negative")
|
RETURN_NAMES = ("positive", "negative")
|
||||||
@ -1918,7 +1922,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
|
"ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
|
||||||
"ConditioningSetMask": "Conditioning (Set Mask)",
|
"ConditioningSetMask": "Conditioning (Set Mask)",
|
||||||
"ControlNetApply": "Apply ControlNet (OLD)",
|
"ControlNetApply": "Apply ControlNet (OLD)",
|
||||||
"ControlNetApplyAdvanced": "Apply ControlNet (OLD Advanced)",
|
"ControlNetApplyAdvanced": "Apply ControlNet",
|
||||||
# Latent
|
# Latent
|
||||||
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
|
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
|
||||||
"SetLatentNoiseMask": "Set Latent Noise Mask",
|
"SetLatentNoiseMask": "Set Latent Noise Mask",
|
||||||
|
|||||||
@ -38,6 +38,9 @@ def get_images(ws, prompt):
|
|||||||
if data['node'] is None and data['prompt_id'] == prompt_id:
|
if data['node'] is None and data['prompt_id'] == prompt_id:
|
||||||
break #Execution is done
|
break #Execution is done
|
||||||
else:
|
else:
|
||||||
|
# If you want to be able to decode the binary stream for latent previews, here is how you can do it:
|
||||||
|
# bytesIO = BytesIO(out[8:])
|
||||||
|
# preview_image = Image.open(bytesIO) # This is your preview in PIL image format, store it in a global
|
||||||
continue #previews are binary data
|
continue #previews are binary data
|
||||||
|
|
||||||
history = get_history(prompt_id)[prompt_id]
|
history = get_history(prompt_id)[prompt_id]
|
||||||
@ -151,7 +154,7 @@ prompt["3"]["inputs"]["seed"] = 5
|
|||||||
ws = websocket.WebSocket()
|
ws = websocket.WebSocket()
|
||||||
ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
|
ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
|
||||||
images = get_images(ws, prompt)
|
images = get_images(ws, prompt)
|
||||||
|
ws.close() # for in case this example is used in an environment where it will be repeatedly called, like in a Gradio app. otherwise, you'll randomly receive connection timeouts
|
||||||
#Commented out code to display the output images:
|
#Commented out code to display the output images:
|
||||||
|
|
||||||
# for node_id in images:
|
# for node_id in images:
|
||||||
|
|||||||
@ -147,7 +147,7 @@ prompt["3"]["inputs"]["seed"] = 5
|
|||||||
ws = websocket.WebSocket()
|
ws = websocket.WebSocket()
|
||||||
ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
|
ws.connect("ws://{}/ws?clientId={}".format(server_address, client_id))
|
||||||
images = get_images(ws, prompt)
|
images = get_images(ws, prompt)
|
||||||
|
ws.close() # for in case this example is used in an environment where it will be repeatedly called, like in a Gradio app. otherwise, you'll randomly receive connection timeouts
|
||||||
#Commented out code to display the output images:
|
#Commented out code to display the output images:
|
||||||
|
|
||||||
# for node_id in images:
|
# for node_id in images:
|
||||||
|
|||||||
39
server.py
39
server.py
@ -490,12 +490,17 @@ class PromptServer():
|
|||||||
async def system_stats(request):
|
async def system_stats(request):
|
||||||
device = comfy.model_management.get_torch_device()
|
device = comfy.model_management.get_torch_device()
|
||||||
device_name = comfy.model_management.get_torch_device_name(device)
|
device_name = comfy.model_management.get_torch_device_name(device)
|
||||||
|
cpu_device = comfy.model_management.torch.device("cpu")
|
||||||
|
ram_total = comfy.model_management.get_total_memory(cpu_device)
|
||||||
|
ram_free = comfy.model_management.get_free_memory(cpu_device)
|
||||||
vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
|
vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
|
||||||
vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
|
vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
|
||||||
|
|
||||||
system_stats = {
|
system_stats = {
|
||||||
"system": {
|
"system": {
|
||||||
"os": os.name,
|
"os": os.name,
|
||||||
|
"ram_total": ram_total,
|
||||||
|
"ram_free": ram_free,
|
||||||
"comfyui_version": get_comfyui_version(),
|
"comfyui_version": get_comfyui_version(),
|
||||||
"python_version": sys.version,
|
"python_version": sys.version,
|
||||||
"pytorch_version": comfy.model_management.torch_version,
|
"pytorch_version": comfy.model_management.torch_version,
|
||||||
@ -674,6 +679,7 @@ class PromptServer():
|
|||||||
|
|
||||||
# Internal route. Should not be depended upon and is subject to change at any time.
|
# Internal route. Should not be depended upon and is subject to change at any time.
|
||||||
# TODO(robinhuang): Move to internal route table class once we refactor PromptServer to pass around Websocket.
|
# TODO(robinhuang): Move to internal route table class once we refactor PromptServer to pass around Websocket.
|
||||||
|
# NOTE: This was an experiment and WILL BE REMOVED
|
||||||
@routes.post("/internal/models/download")
|
@routes.post("/internal/models/download")
|
||||||
async def download_handler(request):
|
async def download_handler(request):
|
||||||
async def report_progress(filename: str, status: DownloadModelStatus):
|
async def report_progress(filename: str, status: DownloadModelStatus):
|
||||||
@ -684,10 +690,11 @@ class PromptServer():
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
url = data.get('url')
|
url = data.get('url')
|
||||||
model_directory = data.get('model_directory')
|
model_directory = data.get('model_directory')
|
||||||
|
folder_path = data.get('folder_path')
|
||||||
model_filename = data.get('model_filename')
|
model_filename = data.get('model_filename')
|
||||||
progress_interval = data.get('progress_interval', 1.0) # In seconds, how often to report download progress.
|
progress_interval = data.get('progress_interval', 1.0) # In seconds, how often to report download progress.
|
||||||
|
|
||||||
if not url or not model_directory or not model_filename:
|
if not url or not model_directory or not model_filename or not folder_path:
|
||||||
return web.json_response({"status": "error", "message": "Missing URL or folder path or filename"}, status=400)
|
return web.json_response({"status": "error", "message": "Missing URL or folder path or filename"}, status=400)
|
||||||
|
|
||||||
session = self.client_session
|
session = self.client_session
|
||||||
@ -695,7 +702,7 @@ class PromptServer():
|
|||||||
logging.error("Client session is not initialized")
|
logging.error("Client session is not initialized")
|
||||||
return web.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
|
||||||
task = asyncio.create_task(download_model(lambda url: session.get(url), model_filename, url, model_directory, report_progress, progress_interval))
|
task = asyncio.create_task(download_model(lambda url: session.get(url), model_filename, url, model_directory, folder_path, report_progress, progress_interval))
|
||||||
await task
|
await task
|
||||||
|
|
||||||
return web.json_response(task.result().to_dict())
|
return web.json_response(task.result().to_dict())
|
||||||
@ -812,6 +819,9 @@ class PromptServer():
|
|||||||
await self.send(*msg)
|
await self.send(*msg)
|
||||||
|
|
||||||
async def start(self, address, port, verbose=True, call_on_start=None):
|
async def start(self, address, port, verbose=True, call_on_start=None):
|
||||||
|
await self.start_multi_address([(address, port)], call_on_start=call_on_start)
|
||||||
|
|
||||||
|
async def start_multi_address(self, addresses, call_on_start=None):
|
||||||
runner = web.AppRunner(self.app, access_log=None)
|
runner = web.AppRunner(self.app, access_log=None)
|
||||||
await runner.setup()
|
await runner.setup()
|
||||||
ssl_ctx = None
|
ssl_ctx = None
|
||||||
@ -822,17 +832,26 @@ class PromptServer():
|
|||||||
keyfile=args.tls_keyfile)
|
keyfile=args.tls_keyfile)
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
|
|
||||||
site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
|
logging.info("Starting server\n")
|
||||||
await site.start()
|
for addr in addresses:
|
||||||
|
address = addr[0]
|
||||||
|
port = addr[1]
|
||||||
|
site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
|
||||||
|
await site.start()
|
||||||
|
|
||||||
self.address = address
|
if not hasattr(self, 'address'):
|
||||||
self.port = port
|
self.address = address #TODO: remove this
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
if ':' in address:
|
||||||
|
address_print = "[{}]".format(address)
|
||||||
|
else:
|
||||||
|
address_print = address
|
||||||
|
|
||||||
|
logging.info("To see the GUI go to: {}://{}:{}".format(scheme, address_print, port))
|
||||||
|
|
||||||
if verbose:
|
|
||||||
logging.info("Starting server\n")
|
|
||||||
logging.info("To see the GUI go to: {}://{}:{}".format(scheme, address, port))
|
|
||||||
if call_on_start is not None:
|
if call_on_start is not None:
|
||||||
call_on_start(scheme, address, port)
|
call_on_start(scheme, self.address, self.port)
|
||||||
|
|
||||||
def add_on_prompt_handler(self, handler):
|
def add_on_prompt_handler(self, handler):
|
||||||
self.on_prompt_handlers.append(handler)
|
self.on_prompt_handlers.append(handler)
|
||||||
|
|||||||
@ -6,9 +6,9 @@ from folder_paths import filter_files_content_types
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def file_extensions():
|
def file_extensions():
|
||||||
return {
|
return {
|
||||||
'image': ['bmp', 'cdr', 'gif', 'heif', 'ico', 'jpeg', 'jpg', 'pcx', 'png', 'pnm', 'ppm', 'psd', 'sgi', 'svg', 'tiff', 'webp', 'xbm', 'xcf', 'xpm'],
|
'image': ['gif', 'heif', 'ico', 'jpeg', 'jpg', 'png', 'pnm', 'ppm', 'svg', 'tiff', 'webp', 'xbm', 'xpm'],
|
||||||
'audio': ['aif', 'aifc', 'aiff', 'au', 'awb', 'flac', 'm4a', 'mp2', 'mp3', 'ogg', 'sd2', 'smp', 'snd', 'wav'],
|
'audio': ['aif', 'aifc', 'aiff', 'au', 'flac', 'm4a', 'mp2', 'mp3', 'ogg', 'snd', 'wav'],
|
||||||
'video': ['avi', 'flv', 'm2v', 'm4v', 'mj2', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'ogv', 'qt', 'webm', 'wmv']
|
'video': ['avi', 'm2v', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'ogv', 'qt', 'webm', 'wmv']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import tempfile
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import ClientResponse
|
from aiohttp import ClientResponse
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
from unittest.mock import AsyncMock, patch, MagicMock
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
from model_filemanager import download_model, validate_model_subdirectory, track_download_progress, create_model_path, check_file_exists, DownloadStatusType, DownloadModelStatus, validate_filename
|
from model_filemanager import download_model, track_download_progress, create_model_path, check_file_exists, DownloadStatusType, DownloadModelStatus, validate_filename
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_dir():
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
yield tmpdirname
|
||||||
|
|
||||||
class AsyncIteratorMock:
|
class AsyncIteratorMock:
|
||||||
"""
|
"""
|
||||||
@ -42,7 +49,7 @@ class ContentMock:
|
|||||||
return AsyncIteratorMock(self.chunks)
|
return AsyncIteratorMock(self.chunks)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_download_model_success():
|
async def test_download_model_success(temp_dir):
|
||||||
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
||||||
mock_response.status = 200
|
mock_response.status = 200
|
||||||
mock_response.headers = {'Content-Length': '1000'}
|
mock_response.headers = {'Content-Length': '1000'}
|
||||||
@ -53,15 +60,13 @@ async def test_download_model_success():
|
|||||||
mock_make_request = AsyncMock(return_value=mock_response)
|
mock_make_request = AsyncMock(return_value=mock_response)
|
||||||
mock_progress_callback = AsyncMock()
|
mock_progress_callback = AsyncMock()
|
||||||
|
|
||||||
# Mock file operations
|
|
||||||
mock_open = MagicMock()
|
|
||||||
mock_file = MagicMock()
|
|
||||||
mock_open.return_value.__enter__.return_value = mock_file
|
|
||||||
time_values = itertools.count(0, 0.1)
|
time_values = itertools.count(0, 0.1)
|
||||||
|
|
||||||
with patch('model_filemanager.create_model_path', return_value=('models/checkpoints/model.sft', 'checkpoints/model.sft')), \
|
fake_paths = {'checkpoints': ([temp_dir], folder_paths.supported_pt_extensions)}
|
||||||
|
|
||||||
|
with patch('model_filemanager.create_model_path', return_value=('models/checkpoints/model.sft', 'model.sft')), \
|
||||||
patch('model_filemanager.check_file_exists', return_value=None), \
|
patch('model_filemanager.check_file_exists', return_value=None), \
|
||||||
patch('builtins.open', mock_open), \
|
patch('folder_paths.folder_names_and_paths', fake_paths), \
|
||||||
patch('time.time', side_effect=time_values): # Simulate time passing
|
patch('time.time', side_effect=time_values): # Simulate time passing
|
||||||
|
|
||||||
result = await download_model(
|
result = await download_model(
|
||||||
@ -69,6 +74,7 @@ async def test_download_model_success():
|
|||||||
'model.sft',
|
'model.sft',
|
||||||
'http://example.com/model.sft',
|
'http://example.com/model.sft',
|
||||||
'checkpoints',
|
'checkpoints',
|
||||||
|
temp_dir,
|
||||||
mock_progress_callback
|
mock_progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -83,44 +89,48 @@ async def test_download_model_success():
|
|||||||
|
|
||||||
# Check initial call
|
# Check initial call
|
||||||
mock_progress_callback.assert_any_call(
|
mock_progress_callback.assert_any_call(
|
||||||
'checkpoints/model.sft',
|
'model.sft',
|
||||||
DownloadModelStatus(DownloadStatusType.PENDING, 0, "Starting download of model.sft", False)
|
DownloadModelStatus(DownloadStatusType.PENDING, 0, "Starting download of model.sft", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check final call
|
# Check final call
|
||||||
mock_progress_callback.assert_any_call(
|
mock_progress_callback.assert_any_call(
|
||||||
'checkpoints/model.sft',
|
'model.sft',
|
||||||
DownloadModelStatus(DownloadStatusType.COMPLETED, 100, "Successfully downloaded model.sft", False)
|
DownloadModelStatus(DownloadStatusType.COMPLETED, 100, "Successfully downloaded model.sft", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify file writing
|
mock_file_path = os.path.join(temp_dir, 'model.sft')
|
||||||
mock_file.write.assert_any_call(b'a' * 500)
|
assert os.path.exists(mock_file_path)
|
||||||
mock_file.write.assert_any_call(b'b' * 300)
|
with open(mock_file_path, 'rb') as mock_file:
|
||||||
mock_file.write.assert_any_call(b'c' * 200)
|
assert mock_file.read() == b''.join(chunks)
|
||||||
|
os.remove(mock_file_path)
|
||||||
|
|
||||||
# Verify request was made
|
# Verify request was made
|
||||||
mock_make_request.assert_called_once_with('http://example.com/model.sft')
|
mock_make_request.assert_called_once_with('http://example.com/model.sft')
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_download_model_url_request_failure():
|
async def test_download_model_url_request_failure(temp_dir):
|
||||||
# Mock dependencies
|
# Mock dependencies
|
||||||
mock_response = AsyncMock(spec=ClientResponse)
|
mock_response = AsyncMock(spec=ClientResponse)
|
||||||
mock_response.status = 404 # Simulate a "Not Found" error
|
mock_response.status = 404 # Simulate a "Not Found" error
|
||||||
mock_get = AsyncMock(return_value=mock_response)
|
mock_get = AsyncMock(return_value=mock_response)
|
||||||
mock_progress_callback = AsyncMock()
|
mock_progress_callback = AsyncMock()
|
||||||
|
|
||||||
|
fake_paths = {'checkpoints': ([temp_dir], folder_paths.supported_pt_extensions)}
|
||||||
|
|
||||||
# Mock the create_model_path function
|
# Mock the create_model_path function
|
||||||
with patch('model_filemanager.create_model_path', return_value=('/mock/path/model.safetensors', 'mock/path/model.safetensors')):
|
with patch('model_filemanager.create_model_path', return_value='/mock/path/model.safetensors'), \
|
||||||
# Mock the check_file_exists function to return None (file doesn't exist)
|
patch('model_filemanager.check_file_exists', return_value=None), \
|
||||||
with patch('model_filemanager.check_file_exists', return_value=None):
|
patch('folder_paths.folder_names_and_paths', fake_paths):
|
||||||
# Call the function
|
# Call the function
|
||||||
result = await download_model(
|
result = await download_model(
|
||||||
mock_get,
|
mock_get,
|
||||||
'model.safetensors',
|
'model.safetensors',
|
||||||
'http://example.com/model.safetensors',
|
'http://example.com/model.safetensors',
|
||||||
'mock_directory',
|
'checkpoints',
|
||||||
mock_progress_callback
|
temp_dir,
|
||||||
)
|
mock_progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
# Assert the expected behavior
|
# Assert the expected behavior
|
||||||
assert isinstance(result, DownloadModelStatus)
|
assert isinstance(result, DownloadModelStatus)
|
||||||
@ -130,7 +140,7 @@ async def test_download_model_url_request_failure():
|
|||||||
|
|
||||||
# Check that progress_callback was called with the correct arguments
|
# Check that progress_callback was called with the correct arguments
|
||||||
mock_progress_callback.assert_any_call(
|
mock_progress_callback.assert_any_call(
|
||||||
'mock_directory/model.safetensors',
|
'model.safetensors',
|
||||||
DownloadModelStatus(
|
DownloadModelStatus(
|
||||||
status=DownloadStatusType.PENDING,
|
status=DownloadStatusType.PENDING,
|
||||||
progress_percentage=0,
|
progress_percentage=0,
|
||||||
@ -139,7 +149,7 @@ async def test_download_model_url_request_failure():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
mock_progress_callback.assert_called_with(
|
mock_progress_callback.assert_called_with(
|
||||||
'mock_directory/model.safetensors',
|
'model.safetensors',
|
||||||
DownloadModelStatus(
|
DownloadModelStatus(
|
||||||
status=DownloadStatusType.ERROR,
|
status=DownloadStatusType.ERROR,
|
||||||
progress_percentage=0,
|
progress_percentage=0,
|
||||||
@ -153,98 +163,125 @@ async def test_download_model_url_request_failure():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_download_model_invalid_model_subdirectory():
|
async def test_download_model_invalid_model_subdirectory():
|
||||||
|
|
||||||
mock_make_request = AsyncMock()
|
mock_make_request = AsyncMock()
|
||||||
mock_progress_callback = AsyncMock()
|
mock_progress_callback = AsyncMock()
|
||||||
|
|
||||||
|
|
||||||
result = await download_model(
|
result = await download_model(
|
||||||
mock_make_request,
|
mock_make_request,
|
||||||
'model.sft',
|
'model.sft',
|
||||||
'http://example.com/model.sft',
|
'http://example.com/model.sft',
|
||||||
'../bad_path',
|
'../bad_path',
|
||||||
|
'../bad_path',
|
||||||
mock_progress_callback
|
mock_progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assert the result
|
# Assert the result
|
||||||
assert isinstance(result, DownloadModelStatus)
|
assert isinstance(result, DownloadModelStatus)
|
||||||
assert result.message == 'Invalid model subdirectory'
|
assert result.message.startswith('Invalid or unrecognized model directory')
|
||||||
assert result.status == 'error'
|
assert result.status == 'error'
|
||||||
assert result.already_existed is False
|
assert result.already_existed is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_model_invalid_folder_path():
|
||||||
|
mock_make_request = AsyncMock()
|
||||||
|
mock_progress_callback = AsyncMock()
|
||||||
|
|
||||||
|
result = await download_model(
|
||||||
|
mock_make_request,
|
||||||
|
'model.sft',
|
||||||
|
'http://example.com/model.sft',
|
||||||
|
'checkpoints',
|
||||||
|
'invalid_path',
|
||||||
|
mock_progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert the result
|
||||||
|
assert isinstance(result, DownloadModelStatus)
|
||||||
|
assert result.message.startswith("Invalid folder path")
|
||||||
|
assert result.status == 'error'
|
||||||
|
assert result.already_existed is False
|
||||||
|
|
||||||
# For create_model_path function
|
|
||||||
def test_create_model_path(tmp_path, monkeypatch):
|
def test_create_model_path(tmp_path, monkeypatch):
|
||||||
mock_models_dir = tmp_path / "models"
|
model_name = "model.safetensors"
|
||||||
monkeypatch.setattr('folder_paths.models_dir', str(mock_models_dir))
|
folder_path = os.path.join(tmp_path, "mock_dir")
|
||||||
|
|
||||||
model_name = "test_model.sft"
|
file_path = create_model_path(model_name, folder_path)
|
||||||
model_directory = "test_dir"
|
|
||||||
|
assert file_path == os.path.join(folder_path, "model.safetensors")
|
||||||
file_path, relative_path = create_model_path(model_name, model_directory, mock_models_dir)
|
|
||||||
|
|
||||||
assert file_path == str(mock_models_dir / model_directory / model_name)
|
|
||||||
assert relative_path == f"{model_directory}/{model_name}"
|
|
||||||
assert os.path.exists(os.path.dirname(file_path))
|
assert os.path.exists(os.path.dirname(file_path))
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Invalid model directory"):
|
||||||
|
create_model_path("../path_traversal.safetensors", folder_path)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Invalid model directory"):
|
||||||
|
create_model_path("/etc/some_root_path", folder_path)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_file_exists_when_file_exists(tmp_path):
|
async def test_check_file_exists_when_file_exists(tmp_path):
|
||||||
file_path = tmp_path / "existing_model.sft"
|
file_path = tmp_path / "existing_model.sft"
|
||||||
file_path.touch() # Create an empty file
|
file_path.touch() # Create an empty file
|
||||||
|
|
||||||
mock_callback = AsyncMock()
|
mock_callback = AsyncMock()
|
||||||
|
|
||||||
result = await check_file_exists(str(file_path), "existing_model.sft", mock_callback, "test/existing_model.sft")
|
result = await check_file_exists(str(file_path), "existing_model.sft", mock_callback)
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.status == "completed"
|
assert result.status == "completed"
|
||||||
assert result.message == "existing_model.sft already exists"
|
assert result.message == "existing_model.sft already exists"
|
||||||
assert result.already_existed is True
|
assert result.already_existed is True
|
||||||
|
|
||||||
mock_callback.assert_called_once_with(
|
mock_callback.assert_called_once_with(
|
||||||
"test/existing_model.sft",
|
"existing_model.sft",
|
||||||
DownloadModelStatus(DownloadStatusType.COMPLETED, 100, "existing_model.sft already exists", already_existed=True)
|
DownloadModelStatus(DownloadStatusType.COMPLETED, 100, "existing_model.sft already exists", already_existed=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_file_exists_when_file_does_not_exist(tmp_path):
|
async def test_check_file_exists_when_file_does_not_exist(tmp_path):
|
||||||
file_path = tmp_path / "non_existing_model.sft"
|
file_path = tmp_path / "non_existing_model.sft"
|
||||||
|
|
||||||
mock_callback = AsyncMock()
|
mock_callback = AsyncMock()
|
||||||
|
|
||||||
result = await check_file_exists(str(file_path), "non_existing_model.sft", mock_callback, "test/non_existing_model.sft")
|
result = await check_file_exists(str(file_path), "non_existing_model.sft", mock_callback)
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
mock_callback.assert_not_called()
|
mock_callback.assert_not_called()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_track_download_progress_no_content_length():
|
async def test_track_download_progress_no_content_length(temp_dir):
|
||||||
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
||||||
mock_response.headers = {} # No Content-Length header
|
mock_response.headers = {} # No Content-Length header
|
||||||
mock_response.content.iter_chunked.return_value = AsyncIteratorMock([b'a' * 500, b'b' * 500])
|
chunks = [b'a' * 500, b'b' * 500]
|
||||||
|
mock_response.content.iter_chunked.return_value = AsyncIteratorMock(chunks)
|
||||||
|
|
||||||
mock_callback = AsyncMock()
|
mock_callback = AsyncMock()
|
||||||
mock_open = MagicMock(return_value=MagicMock())
|
|
||||||
|
|
||||||
with patch('builtins.open', mock_open):
|
full_path = os.path.join(temp_dir, 'model.sft')
|
||||||
result = await track_download_progress(
|
|
||||||
mock_response, '/mock/path/model.sft', 'model.sft',
|
result = await track_download_progress(
|
||||||
mock_callback, 'models/model.sft', interval=0.1
|
mock_response, full_path, 'model.sft',
|
||||||
)
|
mock_callback, interval=0.1
|
||||||
|
)
|
||||||
|
|
||||||
assert result.status == "completed"
|
assert result.status == "completed"
|
||||||
|
|
||||||
|
assert os.path.exists(full_path)
|
||||||
|
with open(full_path, 'rb') as f:
|
||||||
|
assert f.read() == b''.join(chunks)
|
||||||
|
os.remove(full_path)
|
||||||
|
|
||||||
# Check that progress was reported even without knowing the total size
|
# Check that progress was reported even without knowing the total size
|
||||||
mock_callback.assert_any_call(
|
mock_callback.assert_any_call(
|
||||||
'models/model.sft',
|
'model.sft',
|
||||||
DownloadModelStatus(DownloadStatusType.IN_PROGRESS, 0, "Downloading model.sft", already_existed=False)
|
DownloadModelStatus(DownloadStatusType.IN_PROGRESS, 0, "Downloading model.sft", already_existed=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_track_download_progress_interval():
|
async def test_track_download_progress_interval(temp_dir):
|
||||||
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
mock_response = AsyncMock(spec=aiohttp.ClientResponse)
|
||||||
mock_response.headers = {'Content-Length': '1000'}
|
mock_response.headers = {'Content-Length': '1000'}
|
||||||
mock_response.content.iter_chunked.return_value = AsyncIteratorMock([b'a' * 100] * 10)
|
chunks = [b'a' * 100] * 10
|
||||||
|
mock_response.content.iter_chunked.return_value = AsyncIteratorMock(chunks)
|
||||||
|
|
||||||
mock_callback = AsyncMock()
|
mock_callback = AsyncMock()
|
||||||
mock_open = MagicMock(return_value=MagicMock())
|
mock_open = MagicMock(return_value=MagicMock())
|
||||||
@ -253,18 +290,18 @@ async def test_track_download_progress_interval():
|
|||||||
mock_time = MagicMock()
|
mock_time = MagicMock()
|
||||||
mock_time.side_effect = [i * 0.5 for i in range(30)] # This should be enough for 10 chunks
|
mock_time.side_effect = [i * 0.5 for i in range(30)] # This should be enough for 10 chunks
|
||||||
|
|
||||||
with patch('builtins.open', mock_open), \
|
full_path = os.path.join(temp_dir, 'model.sft')
|
||||||
patch('time.time', mock_time):
|
|
||||||
await track_download_progress(
|
|
||||||
mock_response, '/mock/path/model.sft', 'model.sft',
|
|
||||||
mock_callback, 'models/model.sft', interval=1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Print out the actual call count and the arguments of each call for debugging
|
with patch('time.time', mock_time):
|
||||||
print(f"mock_callback was called {mock_callback.call_count} times")
|
await track_download_progress(
|
||||||
for i, call in enumerate(mock_callback.call_args_list):
|
mock_response, full_path, 'model.sft',
|
||||||
args, kwargs = call
|
mock_callback, interval=1.0
|
||||||
print(f"Call {i + 1}: {args[1].status}, Progress: {args[1].progress_percentage:.2f}%")
|
)
|
||||||
|
|
||||||
|
assert os.path.exists(full_path)
|
||||||
|
with open(full_path, 'rb') as f:
|
||||||
|
assert f.read() == b''.join(chunks)
|
||||||
|
os.remove(full_path)
|
||||||
|
|
||||||
# Assert that progress was updated at least 3 times (start, at least one interval, and end)
|
# Assert that progress was updated at least 3 times (start, at least one interval, and end)
|
||||||
assert mock_callback.call_count >= 3, f"Expected at least 3 calls, but got {mock_callback.call_count}"
|
assert mock_callback.call_count >= 3, f"Expected at least 3 calls, but got {mock_callback.call_count}"
|
||||||
@ -279,27 +316,6 @@ async def test_track_download_progress_interval():
|
|||||||
assert last_call[0][1].status == "completed"
|
assert last_call[0][1].status == "completed"
|
||||||
assert last_call[0][1].progress_percentage == 100
|
assert last_call[0][1].progress_percentage == 100
|
||||||
|
|
||||||
def test_valid_subdirectory():
|
|
||||||
assert validate_model_subdirectory("valid-model123") is True
|
|
||||||
|
|
||||||
def test_subdirectory_too_long():
|
|
||||||
assert validate_model_subdirectory("a" * 51) is False
|
|
||||||
|
|
||||||
def test_subdirectory_with_double_dots():
|
|
||||||
assert validate_model_subdirectory("model/../unsafe") is False
|
|
||||||
|
|
||||||
def test_subdirectory_with_slash():
|
|
||||||
assert validate_model_subdirectory("model/unsafe") is False
|
|
||||||
|
|
||||||
def test_subdirectory_with_special_characters():
|
|
||||||
assert validate_model_subdirectory("model@unsafe") is False
|
|
||||||
|
|
||||||
def test_subdirectory_with_underscore_and_dash():
|
|
||||||
assert validate_model_subdirectory("valid_model-name") is True
|
|
||||||
|
|
||||||
def test_empty_subdirectory():
|
|
||||||
assert validate_model_subdirectory("") is False
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("filename, expected", [
|
@pytest.mark.parametrize("filename, expected", [
|
||||||
("valid_model.safetensors", True),
|
("valid_model.safetensors", True),
|
||||||
("valid_model.sft", True),
|
("valid_model.sft", True),
|
||||||
|
|||||||
@ -78,7 +78,9 @@ def test_load_extra_model_paths_expands_userpath(
|
|||||||
|
|
||||||
# Check if add_model_folder_path was called with the correct arguments
|
# Check if add_model_folder_path was called with the correct arguments
|
||||||
for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
|
for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
|
||||||
assert actual_call.args == expected_call
|
assert actual_call.args[0] == expected_call[0]
|
||||||
|
assert os.path.normpath(actual_call.args[1]) == os.path.normpath(expected_call[1]) # Normalize and check the path to check on multiple OS.
|
||||||
|
assert actual_call.args[2] == expected_call[2]
|
||||||
|
|
||||||
# Check if yaml.safe_load was called
|
# Check if yaml.safe_load was called
|
||||||
mock_yaml_safe_load.assert_called_once()
|
mock_yaml_safe_load.assert_called_once()
|
||||||
|
|||||||
1
web/assets/CREDIT.txt
generated
vendored
Normal file
1
web/assets/CREDIT.txt
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Thanks to OpenArt (https://openart.ai) for providing the sorted-custom-node-map data, captured in September 2024.
|
||||||
3142
web/assets/GraphView-DN9xGvF3.js
generated
vendored
Normal file
3142
web/assets/GraphView-DN9xGvF3.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/GraphView-DN9xGvF3.js.map
generated
vendored
Normal file
1
web/assets/GraphView-DN9xGvF3.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
158
web/assets/GraphView-DXU9yRen.css
generated
vendored
Normal file
158
web/assets/GraphView-DXU9yRen.css
generated
vendored
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
|
||||||
|
.group-title-editor.node-title-editor[data-v-fc3f26e3] {
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
[data-v-fc3f26e3] .editable-text {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
[data-v-fc3f26e3] .editable-text input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* Override the default font size */
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-button-icon {
|
||||||
|
font-size: var(--sidebar-icon-size) !important;
|
||||||
|
}
|
||||||
|
.side-bar-button-selected .side-bar-button-icon {
|
||||||
|
font-size: var(--sidebar-icon-size) !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-button[data-v-caa3ee9c] {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: var(--sidebar-width);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.comfyui-body-left .side-bar-button.side-bar-button-selected[data-v-caa3ee9c],
|
||||||
|
.comfyui-body-left .side-bar-button.side-bar-button-selected[data-v-caa3ee9c]:hover {
|
||||||
|
border-left: 4px solid var(--p-button-text-primary-color);
|
||||||
|
}
|
||||||
|
.comfyui-body-right .side-bar-button.side-bar-button-selected[data-v-caa3ee9c],
|
||||||
|
.comfyui-body-right .side-bar-button.side-bar-button-selected[data-v-caa3ee9c]:hover {
|
||||||
|
border-right: 4px solid var(--p-button-text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 64px;
|
||||||
|
--sidebar-icon-size: 1.5rem;
|
||||||
|
}
|
||||||
|
:root .small-sidebar {
|
||||||
|
--sidebar-width: 40px;
|
||||||
|
--sidebar-icon-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-tool-bar-container[data-v-ed7a1148] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--comfy-menu-bg);
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
.side-tool-bar-end[data-v-ed7a1148] {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.sidebar-content-container[data-v-ed7a1148] {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-splitter-gutter {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.gutter-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-panel[data-v-edca8328] {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.splitter-overlay[data-v-edca8328] {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
/* Set it the same as the ComfyUI menu */
|
||||||
|
/* Note: Lite-graph DOM widgets have the same z-index as the node id, so
|
||||||
|
999 should be sufficient to make sure splitter overlays on node's DOM
|
||||||
|
widgets */
|
||||||
|
z-index: 999;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-v-37f672ab] .highlight {
|
||||||
|
background-color: var(--p-primary-color);
|
||||||
|
color: var(--p-primary-contrast-color);
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0rem 0.125rem;
|
||||||
|
margin: -0.125rem 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-vue-node-search-container[data-v-2d409367] {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 26rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.comfy-vue-node-search-container[data-v-2d409367] * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.comfy-vue-node-preview-container[data-v-2d409367] {
|
||||||
|
position: absolute;
|
||||||
|
left: -350px;
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
.comfy-vue-node-search-box[data-v-2d409367] {
|
||||||
|
z-index: 10;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
._filter-button[data-v-2d409367] {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
._dialog[data-v-2d409367] {
|
||||||
|
min-width: 26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible-dialog-root {
|
||||||
|
width: 30%;
|
||||||
|
min-width: 24rem;
|
||||||
|
max-width: 48rem;
|
||||||
|
border: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
margin-top: 25vh;
|
||||||
|
}
|
||||||
|
.node-search-box-dialog-mask {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-tooltip[data-v-e0597bf9] {
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
|
||||||
|
color: var(--input-text);
|
||||||
|
font-family: sans-serif;
|
||||||
|
left: 0;
|
||||||
|
max-width: 30vw;
|
||||||
|
padding: 4px 8px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transform: translate(5px, calc(-100% - 5px));
|
||||||
|
white-space: pre-wrap;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
6426
web/assets/index-W4jP-SrU.css → web/assets/index-8NH3XvqK.css
generated
vendored
6426
web/assets/index-W4jP-SrU.css → web/assets/index-8NH3XvqK.css
generated
vendored
File diff suppressed because it is too large
Load Diff
378
web/assets/index-CrROdkG4.js → web/assets/index-BDBCRrlL.js
generated
vendored
378
web/assets/index-CrROdkG4.js → web/assets/index-BDBCRrlL.js
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||||
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as LGraphNode, e as applyTextReplacements, f as ComfyWidgets, g as addValueControlWidgets, D as DraggableList, h as api, i as LGraphGroup, u as useToastStore } from "./index-Dfv2aLsq.js";
|
import { aM as ComfyDialog, aN as $el, aO as ComfyApp, c as app, aH as LGraphCanvas, k as LiteGraph, e as LGraphNode, aP as applyTextReplacements, aQ as ComfyWidgets, aR as addValueControlWidgets, aS as DraggableList, av as useNodeDefStore, aT as api, L as LGraphGroup, aU as useToastStore, at as NodeSourceType, aV as NodeBadgeMode, u as useSettingStore, q as computed, w as watch, aW as BadgePosition, aJ as LGraphBadge$1, aX as _ } from "./index-Drc_oD2f.js";
|
||||||
class ClipspaceDialog extends ComfyDialog {
|
class ClipspaceDialog extends ComfyDialog {
|
||||||
static {
|
static {
|
||||||
__name(this, "ClipspaceDialog");
|
__name(this, "ClipspaceDialog");
|
||||||
@ -213,7 +213,9 @@ const colorPalettes = {
|
|||||||
WIDGET_SECONDARY_TEXT_COLOR: "#999",
|
WIDGET_SECONDARY_TEXT_COLOR: "#999",
|
||||||
LINK_COLOR: "#9A9",
|
LINK_COLOR: "#9A9",
|
||||||
EVENT_LINK_COLOR: "#A86",
|
EVENT_LINK_COLOR: "#A86",
|
||||||
CONNECTING_LINK_COLOR: "#AFA"
|
CONNECTING_LINK_COLOR: "#AFA",
|
||||||
|
BADGE_FG_COLOR: "#FFF",
|
||||||
|
BADGE_BG_COLOR: "#0F1F0F"
|
||||||
},
|
},
|
||||||
comfy_base: {
|
comfy_base: {
|
||||||
"fg-color": "#fff",
|
"fg-color": "#fff",
|
||||||
@ -283,7 +285,9 @@ const colorPalettes = {
|
|||||||
WIDGET_SECONDARY_TEXT_COLOR: "#555",
|
WIDGET_SECONDARY_TEXT_COLOR: "#555",
|
||||||
LINK_COLOR: "#4CAF50",
|
LINK_COLOR: "#4CAF50",
|
||||||
EVENT_LINK_COLOR: "#FF9800",
|
EVENT_LINK_COLOR: "#FF9800",
|
||||||
CONNECTING_LINK_COLOR: "#2196F3"
|
CONNECTING_LINK_COLOR: "#2196F3",
|
||||||
|
BADGE_FG_COLOR: "#000",
|
||||||
|
BADGE_BG_COLOR: "#FFF"
|
||||||
},
|
},
|
||||||
comfy_base: {
|
comfy_base: {
|
||||||
"fg-color": "#222",
|
"fg-color": "#222",
|
||||||
@ -621,6 +625,32 @@ const defaultColorPaletteId = "dark";
|
|||||||
const els = {
|
const els = {
|
||||||
select: null
|
select: null
|
||||||
};
|
};
|
||||||
|
const getCustomColorPalettes = /* @__PURE__ */ __name(() => {
|
||||||
|
return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
|
||||||
|
}, "getCustomColorPalettes");
|
||||||
|
const setCustomColorPalettes = /* @__PURE__ */ __name((customColorPalettes) => {
|
||||||
|
return app.ui.settings.setSettingValue(
|
||||||
|
idCustomColorPalettes,
|
||||||
|
customColorPalettes
|
||||||
|
);
|
||||||
|
}, "setCustomColorPalettes");
|
||||||
|
const defaultColorPalette = colorPalettes[defaultColorPaletteId];
|
||||||
|
const getColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
|
||||||
|
if (!colorPaletteId) {
|
||||||
|
colorPaletteId = app.ui.settings.getSettingValue(id$4, defaultColorPaletteId);
|
||||||
|
}
|
||||||
|
if (colorPaletteId.startsWith("custom_")) {
|
||||||
|
colorPaletteId = colorPaletteId.substr(7);
|
||||||
|
let customColorPalettes = getCustomColorPalettes();
|
||||||
|
if (customColorPalettes[colorPaletteId]) {
|
||||||
|
return customColorPalettes[colorPaletteId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colorPalettes[colorPaletteId];
|
||||||
|
}, "getColorPalette");
|
||||||
|
const setColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
|
||||||
|
app.ui.settings.setSettingValue(id$4, colorPaletteId);
|
||||||
|
}, "setColorPalette");
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: id$4,
|
name: id$4,
|
||||||
init() {
|
init() {
|
||||||
@ -695,28 +725,19 @@ app.registerExtension({
|
|||||||
comfy_base: {}
|
comfy_base: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const defaultColorPalette = colorPalettes[defaultColorPaletteId];
|
const defaultColorPalette2 = colorPalettes[defaultColorPaletteId];
|
||||||
for (const key in defaultColorPalette.colors.litegraph_base) {
|
for (const key in defaultColorPalette2.colors.litegraph_base) {
|
||||||
if (!colorPalette.colors.litegraph_base[key]) {
|
if (!colorPalette.colors.litegraph_base[key]) {
|
||||||
colorPalette.colors.litegraph_base[key] = "";
|
colorPalette.colors.litegraph_base[key] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key in defaultColorPalette.colors.comfy_base) {
|
for (const key in defaultColorPalette2.colors.comfy_base) {
|
||||||
if (!colorPalette.colors.comfy_base[key]) {
|
if (!colorPalette.colors.comfy_base[key]) {
|
||||||
colorPalette.colors.comfy_base[key] = "";
|
colorPalette.colors.comfy_base[key] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return completeColorPalette(colorPalette);
|
return completeColorPalette(colorPalette);
|
||||||
}, "getColorPaletteTemplate");
|
}, "getColorPaletteTemplate");
|
||||||
const getCustomColorPalettes = /* @__PURE__ */ __name(() => {
|
|
||||||
return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
|
|
||||||
}, "getCustomColorPalettes");
|
|
||||||
const setCustomColorPalettes = /* @__PURE__ */ __name((customColorPalettes) => {
|
|
||||||
return app.ui.settings.setSettingValue(
|
|
||||||
idCustomColorPalettes,
|
|
||||||
customColorPalettes
|
|
||||||
);
|
|
||||||
}, "setCustomColorPalettes");
|
|
||||||
const addCustomColorPalette = /* @__PURE__ */ __name(async (colorPalette) => {
|
const addCustomColorPalette = /* @__PURE__ */ __name(async (colorPalette) => {
|
||||||
if (typeof colorPalette !== "object") {
|
if (typeof colorPalette !== "object") {
|
||||||
alert("Invalid color palette.");
|
alert("Invalid color palette.");
|
||||||
@ -807,25 +828,6 @@ app.registerExtension({
|
|||||||
app.canvas.draw(true, true);
|
app.canvas.draw(true, true);
|
||||||
}
|
}
|
||||||
}, "loadColorPalette");
|
}, "loadColorPalette");
|
||||||
const getColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
|
|
||||||
if (!colorPaletteId) {
|
|
||||||
colorPaletteId = app.ui.settings.getSettingValue(
|
|
||||||
id$4,
|
|
||||||
defaultColorPaletteId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (colorPaletteId.startsWith("custom_")) {
|
|
||||||
colorPaletteId = colorPaletteId.substr(7);
|
|
||||||
let customColorPalettes = getCustomColorPalettes();
|
|
||||||
if (customColorPalettes[colorPaletteId]) {
|
|
||||||
return customColorPalettes[colorPaletteId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return colorPalettes[colorPaletteId];
|
|
||||||
}, "getColorPalette");
|
|
||||||
const setColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
|
|
||||||
app.ui.settings.setSettingValue(id$4, colorPaletteId);
|
|
||||||
}, "setColorPalette");
|
|
||||||
const fileInput = $el("input", {
|
const fileInput = $el("input", {
|
||||||
type: "file",
|
type: "file",
|
||||||
accept: ".json",
|
accept: ".json",
|
||||||
@ -994,6 +996,10 @@ app.registerExtension({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
window.comfyAPI = window.comfyAPI || {};
|
||||||
|
window.comfyAPI.colorPalette = window.comfyAPI.colorPalette || {};
|
||||||
|
window.comfyAPI.colorPalette.defaultColorPalette = defaultColorPalette;
|
||||||
|
window.comfyAPI.colorPalette.getColorPalette = getColorPalette;
|
||||||
const ext$2 = {
|
const ext$2 = {
|
||||||
name: "Comfy.ContextMenuFilter",
|
name: "Comfy.ContextMenuFilter",
|
||||||
init() {
|
init() {
|
||||||
@ -1360,7 +1366,7 @@ class PrimitiveNode extends LGraphNode {
|
|||||||
this.#mergeWidgetConfig();
|
this.#mergeWidgetConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onConnectionsChange(_, index, connected) {
|
onConnectionsChange(_2, index, connected) {
|
||||||
if (app.configuringGraph) {
|
if (app.configuringGraph) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1806,7 +1812,7 @@ app.registerExtension({
|
|||||||
convertToInput(this, widget, config);
|
convertToInput(this, widget, config);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
nodeType.prototype.getExtraMenuOptions = function(_, options) {
|
nodeType.prototype.getExtraMenuOptions = function(_2, options) {
|
||||||
const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : void 0;
|
const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : void 0;
|
||||||
if (this.widgets) {
|
if (this.widgets) {
|
||||||
let toInput = [];
|
let toInput = [];
|
||||||
@ -1862,6 +1868,7 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
nodeType.prototype.onGraphConfigured = function() {
|
nodeType.prototype.onGraphConfigured = function() {
|
||||||
if (!this.inputs) return;
|
if (!this.inputs) return;
|
||||||
|
this.widgets ??= [];
|
||||||
for (const input of this.inputs) {
|
for (const input of this.inputs) {
|
||||||
if (input.widget) {
|
if (input.widget) {
|
||||||
if (!input.widget[GET_CONFIG]) {
|
if (!input.widget[GET_CONFIG]) {
|
||||||
@ -1919,7 +1926,7 @@ app.registerExtension({
|
|||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
function isNodeAtPos(pos) {
|
function isNodeAtPos(pos) {
|
||||||
for (const n of app2.graph._nodes) {
|
for (const n of app2.graph.nodes) {
|
||||||
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
|
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -2308,7 +2315,7 @@ class ManageGroupDialog extends ComfyDialog {
|
|||||||
"button.comfy-btn",
|
"button.comfy-btn",
|
||||||
{
|
{
|
||||||
onclick: /* @__PURE__ */ __name((e) => {
|
onclick: /* @__PURE__ */ __name((e) => {
|
||||||
const node = app.graph._nodes.find(
|
const node = app.graph.nodes.find(
|
||||||
(n) => n.type === "workflow/" + this.selectedGroup
|
(n) => n.type === "workflow/" + this.selectedGroup
|
||||||
);
|
);
|
||||||
if (node) {
|
if (node) {
|
||||||
@ -2374,7 +2381,7 @@ class ManageGroupDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
types[g] = type2;
|
types[g] = type2;
|
||||||
if (!nodesByType) {
|
if (!nodesByType) {
|
||||||
nodesByType = app.graph._nodes.reduce((p, n) => {
|
nodesByType = app.graph.nodes.reduce((p, n) => {
|
||||||
p[n.type] ??= [];
|
p[n.type] ??= [];
|
||||||
p[n.type].push(n);
|
p[n.type].push(n);
|
||||||
return p;
|
return p;
|
||||||
@ -2424,7 +2431,7 @@ const Workflow = {
|
|||||||
isInUseGroupNode(name) {
|
isInUseGroupNode(name) {
|
||||||
const id2 = `workflow/${name}`;
|
const id2 = `workflow/${name}`;
|
||||||
if (app.graph.extra?.groupNodes?.[name]) {
|
if (app.graph.extra?.groupNodes?.[name]) {
|
||||||
if (app.graph._nodes.find((n) => n.type === id2)) {
|
if (app.graph.nodes.find((n) => n.type === id2)) {
|
||||||
return Workflow.InUse.InWorkflow;
|
return Workflow.InUse.InWorkflow;
|
||||||
} else {
|
} else {
|
||||||
return Workflow.InUse.Registered;
|
return Workflow.InUse.Registered;
|
||||||
@ -2576,6 +2583,8 @@ class GroupNodeConfig {
|
|||||||
display_name: this.name,
|
display_name: this.name,
|
||||||
category: "group nodes" + ("/" + source),
|
category: "group nodes" + ("/" + source),
|
||||||
input: { required: {} },
|
input: { required: {} },
|
||||||
|
description: `Group node combining ${this.nodeData.nodes.map((n) => n.type).join(", ")}`,
|
||||||
|
python_module: "custom_nodes." + this.name,
|
||||||
[GROUP]: this
|
[GROUP]: this
|
||||||
};
|
};
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
@ -2591,6 +2600,7 @@ class GroupNodeConfig {
|
|||||||
}
|
}
|
||||||
this.#convertedToProcess = null;
|
this.#convertedToProcess = null;
|
||||||
await app.registerNodeDef("workflow/" + this.name, this.nodeDef);
|
await app.registerNodeDef("workflow/" + this.name, this.nodeDef);
|
||||||
|
useNodeDefStore().addNodeDef(this.nodeDef);
|
||||||
}
|
}
|
||||||
getLinks() {
|
getLinks() {
|
||||||
this.linksFrom = {};
|
this.linksFrom = {};
|
||||||
@ -2775,7 +2785,7 @@ class GroupNodeConfig {
|
|||||||
checkPrimitiveConnection(link, inputName, inputs) {
|
checkPrimitiveConnection(link, inputName, inputs) {
|
||||||
const sourceNode = this.nodeData.nodes[link[0]];
|
const sourceNode = this.nodeData.nodes[link[0]];
|
||||||
if (sourceNode.type === "PrimitiveNode") {
|
if (sourceNode.type === "PrimitiveNode") {
|
||||||
const [sourceNodeId, _, targetNodeId, __] = link;
|
const [sourceNodeId, _2, targetNodeId, __] = link;
|
||||||
const primitiveDef = this.primitiveDefs[sourceNodeId];
|
const primitiveDef = this.primitiveDefs[sourceNodeId];
|
||||||
const targetWidget = inputs[inputName];
|
const targetWidget = inputs[inputName];
|
||||||
const primitiveConfig = primitiveDef.input.required.value;
|
const primitiveConfig = primitiveDef.input.required.value;
|
||||||
@ -3177,7 +3187,7 @@ class GroupNodeHandler {
|
|||||||
return newNodes;
|
return newNodes;
|
||||||
};
|
};
|
||||||
const getExtraMenuOptions = this.node.getExtraMenuOptions;
|
const getExtraMenuOptions = this.node.getExtraMenuOptions;
|
||||||
this.node.getExtraMenuOptions = function(_, options) {
|
this.node.getExtraMenuOptions = function(_2, options) {
|
||||||
getExtraMenuOptions?.apply(this, arguments);
|
getExtraMenuOptions?.apply(this, arguments);
|
||||||
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
||||||
if (optionIndex === -1) optionIndex = options.length;
|
if (optionIndex === -1) optionIndex = options.length;
|
||||||
@ -3353,7 +3363,7 @@ class GroupNodeHandler {
|
|||||||
} else if (innerNode.type === "Reroute") {
|
} else if (innerNode.type === "Reroute") {
|
||||||
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
||||||
if (rerouteLinks) {
|
if (rerouteLinks) {
|
||||||
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
for (const [_2, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
||||||
const node = this.innerNodes[targetNodeId];
|
const node = this.innerNodes[targetNodeId];
|
||||||
const input = node.inputs[targetSlot];
|
const input = node.inputs[targetSlot];
|
||||||
if (input.widget) {
|
if (input.widget) {
|
||||||
@ -3599,7 +3609,7 @@ function addNodesToGroup(group, nodes = []) {
|
|||||||
var node;
|
var node;
|
||||||
x1 = y1 = x2 = y2 = -1;
|
x1 = y1 = x2 = y2 = -1;
|
||||||
nx1 = ny1 = nx2 = ny2 = -1;
|
nx1 = ny1 = nx2 = ny2 = -1;
|
||||||
for (var n of [group._nodes, nodes]) {
|
for (var n of [group.nodes, nodes]) {
|
||||||
for (var i in n) {
|
for (var i in n) {
|
||||||
node = n[i];
|
node = n[i];
|
||||||
nx1 = node.pos[0];
|
nx1 = node.pos[0];
|
||||||
@ -3659,7 +3669,7 @@ app.registerExtension({
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
group.recomputeInsideNodes();
|
group.recomputeInsideNodes();
|
||||||
const nodesInGroup = group._nodes;
|
const nodesInGroup = group.nodes;
|
||||||
options.push({
|
options.push({
|
||||||
content: "Add Selected Nodes To Group",
|
content: "Add Selected Nodes To Group",
|
||||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||||
@ -4002,6 +4012,16 @@ function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
|
|||||||
maskCtx.putImageData(maskData, 0, 0);
|
maskCtx.putImageData(maskData, 0, 0);
|
||||||
}
|
}
|
||||||
__name(prepare_mask, "prepare_mask");
|
__name(prepare_mask, "prepare_mask");
|
||||||
|
var PointerType = /* @__PURE__ */ ((PointerType2) => {
|
||||||
|
PointerType2["Arc"] = "arc";
|
||||||
|
PointerType2["Rect"] = "rect";
|
||||||
|
return PointerType2;
|
||||||
|
})(PointerType || {});
|
||||||
|
var CompositionOperation = /* @__PURE__ */ ((CompositionOperation2) => {
|
||||||
|
CompositionOperation2["SourceOver"] = "source-over";
|
||||||
|
CompositionOperation2["DestinationOut"] = "destination-out";
|
||||||
|
return CompositionOperation2;
|
||||||
|
})(CompositionOperation || {});
|
||||||
class MaskEditorDialog extends ComfyDialog {
|
class MaskEditorDialog extends ComfyDialog {
|
||||||
static {
|
static {
|
||||||
__name(this, "MaskEditorDialog");
|
__name(this, "MaskEditorDialog");
|
||||||
@ -4030,6 +4050,8 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
mousedown_pan_x;
|
mousedown_pan_x;
|
||||||
mousedown_pan_y;
|
mousedown_pan_y;
|
||||||
last_pressure;
|
last_pressure;
|
||||||
|
pointer_type;
|
||||||
|
brush_pointer_type_select;
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
if (!MaskEditorDialog.instance) {
|
if (!MaskEditorDialog.instance) {
|
||||||
MaskEditorDialog.instance = new MaskEditorDialog();
|
MaskEditorDialog.instance = new MaskEditorDialog();
|
||||||
@ -4077,7 +4099,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
divElement.style.borderColor = "var(--border-color)";
|
divElement.style.borderColor = "var(--border-color)";
|
||||||
divElement.style.borderStyle = "solid";
|
divElement.style.borderStyle = "solid";
|
||||||
divElement.style.fontSize = "15px";
|
divElement.style.fontSize = "15px";
|
||||||
divElement.style.height = "21px";
|
divElement.style.height = "25px";
|
||||||
divElement.style.padding = "1px 6px";
|
divElement.style.padding = "1px 6px";
|
||||||
divElement.style.display = "flex";
|
divElement.style.display = "flex";
|
||||||
divElement.style.position = "relative";
|
divElement.style.position = "relative";
|
||||||
@ -4107,7 +4129,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
divElement.style.borderColor = "var(--border-color)";
|
divElement.style.borderColor = "var(--border-color)";
|
||||||
divElement.style.borderStyle = "solid";
|
divElement.style.borderStyle = "solid";
|
||||||
divElement.style.fontSize = "15px";
|
divElement.style.fontSize = "15px";
|
||||||
divElement.style.height = "21px";
|
divElement.style.height = "25px";
|
||||||
divElement.style.padding = "1px 6px";
|
divElement.style.padding = "1px 6px";
|
||||||
divElement.style.display = "flex";
|
divElement.style.display = "flex";
|
||||||
divElement.style.position = "relative";
|
divElement.style.position = "relative";
|
||||||
@ -4126,8 +4148,63 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
self.opacity_slider_input.addEventListener("input", callback);
|
self.opacity_slider_input.addEventListener("input", callback);
|
||||||
return divElement;
|
return divElement;
|
||||||
}
|
}
|
||||||
|
createPointerTypeSelect(self) {
|
||||||
|
const divElement = document.createElement("div");
|
||||||
|
divElement.id = "maskeditor-pointer-type";
|
||||||
|
divElement.style.cssFloat = "left";
|
||||||
|
divElement.style.fontFamily = "sans-serif";
|
||||||
|
divElement.style.marginRight = "4px";
|
||||||
|
divElement.style.color = "var(--input-text)";
|
||||||
|
divElement.style.backgroundColor = "var(--comfy-input-bg)";
|
||||||
|
divElement.style.borderRadius = "8px";
|
||||||
|
divElement.style.borderColor = "var(--border-color)";
|
||||||
|
divElement.style.borderStyle = "solid";
|
||||||
|
divElement.style.fontSize = "15px";
|
||||||
|
divElement.style.height = "25px";
|
||||||
|
divElement.style.padding = "1px 6px";
|
||||||
|
divElement.style.display = "flex";
|
||||||
|
divElement.style.position = "relative";
|
||||||
|
divElement.style.top = "2px";
|
||||||
|
divElement.style.pointerEvents = "auto";
|
||||||
|
const labelElement = document.createElement("label");
|
||||||
|
labelElement.textContent = "Pointer Type:";
|
||||||
|
const selectElement = document.createElement("select");
|
||||||
|
selectElement.style.borderRadius = "0";
|
||||||
|
selectElement.style.borderColor = "transparent";
|
||||||
|
selectElement.style.borderStyle = "unset";
|
||||||
|
selectElement.style.fontSize = "0.9em";
|
||||||
|
const optionArc = document.createElement("option");
|
||||||
|
optionArc.value = "arc";
|
||||||
|
optionArc.text = "Circle";
|
||||||
|
optionArc.selected = true;
|
||||||
|
const optionRect = document.createElement("option");
|
||||||
|
optionRect.value = "rect";
|
||||||
|
optionRect.text = "Square";
|
||||||
|
selectElement.appendChild(optionArc);
|
||||||
|
selectElement.appendChild(optionRect);
|
||||||
|
selectElement.addEventListener("change", (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
self.pointer_type = target.value;
|
||||||
|
this.setBrushBorderRadius(self);
|
||||||
|
});
|
||||||
|
divElement.appendChild(labelElement);
|
||||||
|
divElement.appendChild(selectElement);
|
||||||
|
return divElement;
|
||||||
|
}
|
||||||
|
setBrushBorderRadius(self) {
|
||||||
|
if (self.pointer_type === "rect") {
|
||||||
|
this.brush.style.borderRadius = "0%";
|
||||||
|
this.brush.style.MozBorderRadius = "0%";
|
||||||
|
this.brush.style.WebkitBorderRadius = "0%";
|
||||||
|
} else {
|
||||||
|
this.brush.style.borderRadius = "50%";
|
||||||
|
this.brush.style.MozBorderRadius = "50%";
|
||||||
|
this.brush.style.WebkitBorderRadius = "50%";
|
||||||
|
}
|
||||||
|
}
|
||||||
setlayout(imgCanvas, maskCanvas) {
|
setlayout(imgCanvas, maskCanvas) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
self.pointer_type = "arc";
|
||||||
var bottom_panel = document.createElement("div");
|
var bottom_panel = document.createElement("div");
|
||||||
bottom_panel.style.position = "absolute";
|
bottom_panel.style.position = "absolute";
|
||||||
bottom_panel.style.bottom = "0px";
|
bottom_panel.style.bottom = "0px";
|
||||||
@ -4140,13 +4217,11 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
brush.style.backgroundColor = "transparent";
|
brush.style.backgroundColor = "transparent";
|
||||||
brush.style.outline = "1px dashed black";
|
brush.style.outline = "1px dashed black";
|
||||||
brush.style.boxShadow = "0 0 0 1px white";
|
brush.style.boxShadow = "0 0 0 1px white";
|
||||||
brush.style.borderRadius = "50%";
|
|
||||||
brush.style.MozBorderRadius = "50%";
|
|
||||||
brush.style.WebkitBorderRadius = "50%";
|
|
||||||
brush.style.position = "absolute";
|
brush.style.position = "absolute";
|
||||||
brush.style.zIndex = "8889";
|
brush.style.zIndex = "8889";
|
||||||
brush.style.pointerEvents = "none";
|
brush.style.pointerEvents = "none";
|
||||||
this.brush = brush;
|
this.brush = brush;
|
||||||
|
this.setBrushBorderRadius(self);
|
||||||
this.element.appendChild(imgCanvas);
|
this.element.appendChild(imgCanvas);
|
||||||
this.element.appendChild(maskCanvas);
|
this.element.appendChild(maskCanvas);
|
||||||
this.element.appendChild(bottom_panel);
|
this.element.appendChild(bottom_panel);
|
||||||
@ -4177,6 +4252,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
this.brush_pointer_type_select = this.createPointerTypeSelect(self);
|
||||||
this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
|
this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
|
||||||
if (self.brush_color_mode === "black") {
|
if (self.brush_color_mode === "black") {
|
||||||
self.brush_color_mode = "white";
|
self.brush_color_mode = "white";
|
||||||
@ -4203,6 +4279,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
bottom_panel.appendChild(cancelButton);
|
bottom_panel.appendChild(cancelButton);
|
||||||
bottom_panel.appendChild(this.brush_size_slider);
|
bottom_panel.appendChild(this.brush_size_slider);
|
||||||
bottom_panel.appendChild(this.brush_opacity_slider);
|
bottom_panel.appendChild(this.brush_opacity_slider);
|
||||||
|
bottom_panel.appendChild(this.brush_pointer_type_select);
|
||||||
bottom_panel.appendChild(this.colorButton);
|
bottom_panel.appendChild(this.colorButton);
|
||||||
imgCanvas.style.position = "absolute";
|
imgCanvas.style.position = "absolute";
|
||||||
maskCanvas.style.position = "absolute";
|
maskCanvas.style.position = "absolute";
|
||||||
@ -4568,19 +4645,22 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
if (diff > 20 && !this.drawing_mode)
|
if (diff > 20 && !this.drawing_mode)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
self.maskCtx.beginPath();
|
self.init_shape(
|
||||||
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
self,
|
||||||
self.maskCtx.globalCompositeOperation = "source-over";
|
"source-over"
|
||||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
/* SourceOver */
|
||||||
self.maskCtx.fill();
|
);
|
||||||
|
self.draw_shape(self, x, y, brush_size);
|
||||||
self.lastx = x;
|
self.lastx = x;
|
||||||
self.lasty = y;
|
self.lasty = y;
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
self.maskCtx.beginPath();
|
self.init_shape(
|
||||||
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
self,
|
||||||
self.maskCtx.globalCompositeOperation = "source-over";
|
"source-over"
|
||||||
|
/* SourceOver */
|
||||||
|
);
|
||||||
var dx = x - self.lastx;
|
var dx = x - self.lastx;
|
||||||
var dy = y - self.lasty;
|
var dy = y - self.lasty;
|
||||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@ -4589,8 +4669,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
for (var i = 0; i < distance; i += 5) {
|
for (var i = 0; i < distance; i += 5) {
|
||||||
var px = self.lastx + directionX * i;
|
var px = self.lastx + directionX * i;
|
||||||
var py = self.lasty + directionY * i;
|
var py = self.lasty + directionY * i;
|
||||||
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
self.draw_shape(self, px, py, brush_size);
|
||||||
self.maskCtx.fill();
|
|
||||||
}
|
}
|
||||||
self.lastx = x;
|
self.lastx = x;
|
||||||
self.lasty = y;
|
self.lasty = y;
|
||||||
@ -4611,17 +4690,22 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
if (diff > 20 && !this.drawing_mode)
|
if (diff > 20 && !this.drawing_mode)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
self.maskCtx.beginPath();
|
self.init_shape(
|
||||||
self.maskCtx.globalCompositeOperation = "destination-out";
|
self,
|
||||||
self.maskCtx.arc(x2, y2, brush_size, 0, Math.PI * 2, false);
|
"destination-out"
|
||||||
self.maskCtx.fill();
|
/* DestinationOut */
|
||||||
|
);
|
||||||
|
self.draw_shape(self, x2, y2, brush_size);
|
||||||
self.lastx = x2;
|
self.lastx = x2;
|
||||||
self.lasty = y2;
|
self.lasty = y2;
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
self.maskCtx.beginPath();
|
self.init_shape(
|
||||||
self.maskCtx.globalCompositeOperation = "destination-out";
|
self,
|
||||||
|
"destination-out"
|
||||||
|
/* DestinationOut */
|
||||||
|
);
|
||||||
var dx = x2 - self.lastx;
|
var dx = x2 - self.lastx;
|
||||||
var dy = y2 - self.lasty;
|
var dy = y2 - self.lasty;
|
||||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@ -4630,8 +4714,7 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
for (var i = 0; i < distance; i += 5) {
|
for (var i = 0; i < distance; i += 5) {
|
||||||
var px = self.lastx + directionX * i;
|
var px = self.lastx + directionX * i;
|
||||||
var py = self.lasty + directionY * i;
|
var py = self.lasty + directionY * i;
|
||||||
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
self.draw_shape(self, px, py, brush_size);
|
||||||
self.maskCtx.fill();
|
|
||||||
}
|
}
|
||||||
self.lastx = x2;
|
self.lastx = x2;
|
||||||
self.lasty = y2;
|
self.lasty = y2;
|
||||||
@ -4665,20 +4748,47 @@ class MaskEditorDialog extends ComfyDialog {
|
|||||||
const maskRect = self.maskCanvas.getBoundingClientRect();
|
const maskRect = self.maskCanvas.getBoundingClientRect();
|
||||||
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
|
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
|
||||||
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
|
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
|
||||||
self.maskCtx.beginPath();
|
|
||||||
if (!event.altKey && event.button == 0) {
|
if (!event.altKey && event.button == 0) {
|
||||||
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
self.init_shape(
|
||||||
self.maskCtx.globalCompositeOperation = "source-over";
|
self,
|
||||||
|
"source-over"
|
||||||
|
/* SourceOver */
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
self.maskCtx.globalCompositeOperation = "destination-out";
|
self.init_shape(
|
||||||
|
self,
|
||||||
|
"destination-out"
|
||||||
|
/* DestinationOut */
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
self.draw_shape(self, x, y, brush_size);
|
||||||
self.maskCtx.fill();
|
|
||||||
self.lastx = x;
|
self.lastx = x;
|
||||||
self.lasty = y;
|
self.lasty = y;
|
||||||
self.lasttime = performance.now();
|
self.lasttime = performance.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
init_shape(self, compositionOperation) {
|
||||||
|
self.maskCtx.beginPath();
|
||||||
|
if (compositionOperation == "source-over") {
|
||||||
|
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
||||||
|
self.maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
} else if (compositionOperation == "destination-out") {
|
||||||
|
self.maskCtx.globalCompositeOperation = "destination-out";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw_shape(self, x, y, brush_size) {
|
||||||
|
if (self.pointer_type === "rect") {
|
||||||
|
self.maskCtx.rect(
|
||||||
|
x - brush_size,
|
||||||
|
y - brush_size,
|
||||||
|
brush_size * 2,
|
||||||
|
brush_size * 2
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
}
|
||||||
|
self.maskCtx.fill();
|
||||||
|
}
|
||||||
async save() {
|
async save() {
|
||||||
const backupCanvas = document.createElement("canvas");
|
const backupCanvas = document.createElement("canvas");
|
||||||
const backupCtx = backupCanvas.getContext("2d", {
|
const backupCtx = backupCanvas.getContext("2d", {
|
||||||
@ -5264,7 +5374,7 @@ app.registerExtension({
|
|||||||
updateNodes.push(node);
|
updateNodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
|
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
|
||||||
if (inputType && inputType !== "*" && nodeOutType !== inputType) {
|
if (inputType && !LiteGraph.isValidConnection(inputType, nodeOutType)) {
|
||||||
node.disconnectInput(link.target_slot);
|
node.disconnectInput(link.target_slot);
|
||||||
} else {
|
} else {
|
||||||
outputType = nodeOutType;
|
outputType = nodeOutType;
|
||||||
@ -5300,6 +5410,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
if (!targetWidget) {
|
if (!targetWidget) {
|
||||||
targetWidget = targetNode.widgets?.find(
|
targetWidget = targetNode.widgets?.find(
|
||||||
|
// @ts-expect-error fix widget types
|
||||||
(w) => w.name === targetInput.widget.name
|
(w) => w.name === targetInput.widget.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -5342,7 +5453,7 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
this.isVirtualNode = true;
|
this.isVirtualNode = true;
|
||||||
}
|
}
|
||||||
getExtraMenuOptions(_, options) {
|
getExtraMenuOptions(_2, options) {
|
||||||
options.unshift(
|
options.unshift(
|
||||||
{
|
{
|
||||||
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
||||||
@ -5564,8 +5675,7 @@ app.registerExtension({
|
|||||||
slot_types_default_in: {},
|
slot_types_default_in: {},
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app2) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app2) {
|
||||||
var nodeId = nodeData.name;
|
var nodeId = nodeData.name;
|
||||||
var inputs = [];
|
const inputs = nodeData["input"]["required"];
|
||||||
inputs = nodeData["input"]["required"];
|
|
||||||
for (const inputKey in inputs) {
|
for (const inputKey in inputs) {
|
||||||
var input = inputs[inputKey];
|
var input = inputs[inputKey];
|
||||||
if (typeof input[0] !== "string") continue;
|
if (typeof input[0] !== "string") continue;
|
||||||
@ -5637,7 +5747,7 @@ app.registerExtension({
|
|||||||
tooltip: "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
tooltip: "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
||||||
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
LiteGraph.CANVAS_GRID_SIZE = +value;
|
LiteGraph.CANVAS_GRID_SIZE = +value || 10;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const onNodeMoved = app.canvas.onNodeMoved;
|
const onNodeMoved = app.canvas.onNodeMoved;
|
||||||
@ -5697,7 +5807,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
||||||
this.recomputeInsideNodes();
|
this.recomputeInsideNodes();
|
||||||
for (const node of this._nodes) {
|
for (const node of this.nodes) {
|
||||||
node.alignToGrid();
|
node.alignToGrid();
|
||||||
}
|
}
|
||||||
LGraphNode.prototype.alignToGrid.apply(this);
|
LGraphNode.prototype.alignToGrid.apply(this);
|
||||||
@ -5730,7 +5840,7 @@ app.registerExtension({
|
|||||||
LGraphCanvas.onGroupAdd = function() {
|
LGraphCanvas.onGroupAdd = function() {
|
||||||
const v = onGroupAdd.apply(app.canvas, arguments);
|
const v = onGroupAdd.apply(app.canvas, arguments);
|
||||||
if (app.shiftDown) {
|
if (app.shiftDown) {
|
||||||
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
|
const lastGroup = app.graph.groups[app.graph.groups.length - 1];
|
||||||
if (lastGroup) {
|
if (lastGroup) {
|
||||||
roundVectorToGrid(lastGroup.pos);
|
roundVectorToGrid(lastGroup.pos);
|
||||||
roundVectorToGrid(lastGroup.size);
|
roundVectorToGrid(lastGroup.size);
|
||||||
@ -6026,4 +6136,108 @@ app.registerExtension({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=index-CrROdkG4.js.map
|
function getNodeSource(node) {
|
||||||
|
const nodeDef = node.constructor.nodeData;
|
||||||
|
if (!nodeDef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nodeDefStore = useNodeDefStore();
|
||||||
|
return nodeDefStore.nodeDefsByName[nodeDef.name]?.nodeSource ?? null;
|
||||||
|
}
|
||||||
|
__name(getNodeSource, "getNodeSource");
|
||||||
|
function isCoreNode(node) {
|
||||||
|
return getNodeSource(node)?.type === NodeSourceType.Core;
|
||||||
|
}
|
||||||
|
__name(isCoreNode, "isCoreNode");
|
||||||
|
function badgeTextVisible(node, badgeMode) {
|
||||||
|
return badgeMode === NodeBadgeMode.None || isCoreNode(node) && badgeMode === NodeBadgeMode.HideBuiltIn;
|
||||||
|
}
|
||||||
|
__name(badgeTextVisible, "badgeTextVisible");
|
||||||
|
function getNodeIdBadgeText(node, nodeIdBadgeMode) {
|
||||||
|
return badgeTextVisible(node, nodeIdBadgeMode) ? "" : `#${node.id}`;
|
||||||
|
}
|
||||||
|
__name(getNodeIdBadgeText, "getNodeIdBadgeText");
|
||||||
|
function getNodeSourceBadgeText(node, nodeSourceBadgeMode) {
|
||||||
|
const nodeSource = getNodeSource(node);
|
||||||
|
return badgeTextVisible(node, nodeSourceBadgeMode) ? "" : nodeSource?.badgeText ?? "";
|
||||||
|
}
|
||||||
|
__name(getNodeSourceBadgeText, "getNodeSourceBadgeText");
|
||||||
|
function getNodeLifeCycleBadgeText(node, nodeLifeCycleBadgeMode) {
|
||||||
|
let text = "";
|
||||||
|
const nodeDef = node.constructor.nodeData;
|
||||||
|
if (!nodeDef) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (nodeDef.deprecated) {
|
||||||
|
text = "[DEPR]";
|
||||||
|
}
|
||||||
|
if (nodeDef.experimental) {
|
||||||
|
text = "[BETA]";
|
||||||
|
}
|
||||||
|
return badgeTextVisible(node, nodeLifeCycleBadgeMode) ? "" : text;
|
||||||
|
}
|
||||||
|
__name(getNodeLifeCycleBadgeText, "getNodeLifeCycleBadgeText");
|
||||||
|
class NodeBadgeExtension {
|
||||||
|
static {
|
||||||
|
__name(this, "NodeBadgeExtension");
|
||||||
|
}
|
||||||
|
constructor(nodeIdBadgeMode = null, nodeSourceBadgeMode = null, nodeLifeCycleBadgeMode = null, colorPalette = null) {
|
||||||
|
this.nodeIdBadgeMode = nodeIdBadgeMode;
|
||||||
|
this.nodeSourceBadgeMode = nodeSourceBadgeMode;
|
||||||
|
this.nodeLifeCycleBadgeMode = nodeLifeCycleBadgeMode;
|
||||||
|
this.colorPalette = colorPalette;
|
||||||
|
}
|
||||||
|
name = "Comfy.NodeBadge";
|
||||||
|
init(app2) {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
this.nodeSourceBadgeMode = computed(
|
||||||
|
() => settingStore.get("Comfy.NodeBadge.NodeSourceBadgeMode")
|
||||||
|
);
|
||||||
|
this.nodeIdBadgeMode = computed(
|
||||||
|
() => settingStore.get("Comfy.NodeBadge.NodeIdBadgeMode")
|
||||||
|
);
|
||||||
|
this.nodeLifeCycleBadgeMode = computed(
|
||||||
|
() => settingStore.get(
|
||||||
|
"Comfy.NodeBadge.NodeLifeCycleBadgeMode"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.colorPalette = computed(
|
||||||
|
() => getColorPalette(settingStore.get("Comfy.ColorPalette"))
|
||||||
|
);
|
||||||
|
watch(this.nodeSourceBadgeMode, () => {
|
||||||
|
app2.graph.setDirtyCanvas(true, true);
|
||||||
|
});
|
||||||
|
watch(this.nodeIdBadgeMode, () => {
|
||||||
|
app2.graph.setDirtyCanvas(true, true);
|
||||||
|
});
|
||||||
|
watch(this.nodeLifeCycleBadgeMode, () => {
|
||||||
|
app2.graph.setDirtyCanvas(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nodeCreated(node, app2) {
|
||||||
|
node.badgePosition = BadgePosition.TopRight;
|
||||||
|
node.badge_enabled = true;
|
||||||
|
const badge = computed(
|
||||||
|
() => new LGraphBadge$1({
|
||||||
|
text: _.truncate(
|
||||||
|
[
|
||||||
|
getNodeIdBadgeText(node, this.nodeIdBadgeMode.value),
|
||||||
|
getNodeLifeCycleBadgeText(
|
||||||
|
node,
|
||||||
|
this.nodeLifeCycleBadgeMode.value
|
||||||
|
),
|
||||||
|
getNodeSourceBadgeText(node, this.nodeSourceBadgeMode.value)
|
||||||
|
].filter((s) => s.length > 0).join(" "),
|
||||||
|
{
|
||||||
|
length: 31
|
||||||
|
}
|
||||||
|
),
|
||||||
|
fgColor: this.colorPalette.value.colors.litegraph_base?.BADGE_FG_COLOR || defaultColorPalette.colors.litegraph_base.BADGE_FG_COLOR,
|
||||||
|
bgColor: this.colorPalette.value.colors.litegraph_base?.BADGE_BG_COLOR || defaultColorPalette.colors.litegraph_base.BADGE_BG_COLOR
|
||||||
|
})
|
||||||
|
);
|
||||||
|
node.badges.push(() => badge.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.registerExtension(new NodeBadgeExtension());
|
||||||
|
//# sourceMappingURL=index-BDBCRrlL.js.map
|
||||||
1
web/assets/index-BDBCRrlL.js.map
generated
vendored
Normal file
1
web/assets/index-BDBCRrlL.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/index-CrROdkG4.js.map
generated
vendored
1
web/assets/index-CrROdkG4.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1
web/assets/index-Dfv2aLsq.js.map
generated
vendored
1
web/assets/index-Dfv2aLsq.js.map
generated
vendored
File diff suppressed because one or more lines are too long
82289
web/assets/index-Dfv2aLsq.js → web/assets/index-Drc_oD2f.js
generated
vendored
82289
web/assets/index-Dfv2aLsq.js → web/assets/index-Drc_oD2f.js
generated
vendored
File diff suppressed because one or more lines are too long
1
web/assets/index-Drc_oD2f.js.map
generated
vendored
Normal file
1
web/assets/index-Drc_oD2f.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2602
web/assets/sorted-custom-node-map.json
generated
vendored
Normal file
2602
web/assets/sorted-custom-node-map.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
web/assets/userSelection-DSpF-zVD.js → web/assets/userSelection-BM5u5JIA.js
generated
vendored
4
web/assets/userSelection-DSpF-zVD.js → web/assets/userSelection-BM5u5JIA.js
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||||
import { j as createSpinner, h as api, $ as $el } from "./index-Dfv2aLsq.js";
|
import { aY as createSpinner, aT as api, aN as $el } from "./index-Drc_oD2f.js";
|
||||||
class UserSelectionScreen {
|
class UserSelectionScreen {
|
||||||
static {
|
static {
|
||||||
__name(this, "UserSelectionScreen");
|
__name(this, "UserSelectionScreen");
|
||||||
@ -117,4 +117,4 @@ window.comfyAPI.userSelection.UserSelectionScreen = UserSelectionScreen;
|
|||||||
export {
|
export {
|
||||||
UserSelectionScreen
|
UserSelectionScreen
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=userSelection-DSpF-zVD.js.map
|
//# sourceMappingURL=userSelection-BM5u5JIA.js.map
|
||||||
2
web/assets/userSelection-DSpF-zVD.js.map → web/assets/userSelection-BM5u5JIA.js.map
generated
vendored
2
web/assets/userSelection-DSpF-zVD.js.map → web/assets/userSelection-BM5u5JIA.js.map
generated
vendored
File diff suppressed because one or more lines are too long
BIN
web/dist.zip
vendored
BIN
web/dist.zip
vendored
Binary file not shown.
3
web/extensions/core/colorPalette.js
vendored
Normal file
3
web/extensions/core/colorPalette.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Shim for extensions/core/colorPalette.ts
|
||||||
|
export const defaultColorPalette = window.comfyAPI.colorPalette.defaultColorPalette;
|
||||||
|
export const getColorPalette = window.comfyAPI.colorPalette.getColorPalette;
|
||||||
94
web/index.html
vendored
94
web/index.html
vendored
@ -1,50 +1,50 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>ComfyUI</title>
|
<title>ComfyUI</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<!-- Browser Test Fonts -->
|
<!-- Browser Test Fonts -->
|
||||||
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||||
}
|
}
|
||||||
</style> -->
|
</style> -->
|
||||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||||
<script type="module" crossorigin src="./assets/index-Dfv2aLsq.js"></script>
|
<script type="module" crossorigin src="./assets/index-Drc_oD2f.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-W4jP-SrU.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-8NH3XvqK.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="litegraph">
|
<body class="litegraph">
|
||||||
<div id="vue-app"></div>
|
<div id="vue-app"></div>
|
||||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||||
<main class="comfy-user-selection-inner">
|
<main class="comfy-user-selection-inner">
|
||||||
<h1>ComfyUI</h1>
|
<h1>ComfyUI</h1>
|
||||||
<form>
|
<form>
|
||||||
<section>
|
<section>
|
||||||
<label>New user:
|
<label>New user:
|
||||||
<input placeholder="Enter a username" />
|
<input placeholder="Enter a username" />
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
<div class="comfy-user-existing">
|
<div class="comfy-user-existing">
|
||||||
<span class="or-separator">OR</span>
|
<span class="or-separator">OR</span>
|
||||||
<section>
|
<section>
|
||||||
<label>
|
<label>
|
||||||
Existing user:
|
Existing user:
|
||||||
<select>
|
<select>
|
||||||
<option hidden disabled selected value> Select a user </option>
|
<option hidden disabled selected value> Select a user </option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<span class="comfy-user-error"> </span>
|
<span class="comfy-user-error"> </span>
|
||||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
<button class="comfy-btn comfy-user-button-next">Next</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
1
web/scripts/workflows.js
vendored
1
web/scripts/workflows.js
vendored
@ -1,4 +1,3 @@
|
|||||||
// Shim for scripts/workflows.ts
|
// Shim for scripts/workflows.ts
|
||||||
export const trimJsonExt = window.comfyAPI.workflows.trimJsonExt;
|
|
||||||
export const ComfyWorkflowManager = window.comfyAPI.workflows.ComfyWorkflowManager;
|
export const ComfyWorkflowManager = window.comfyAPI.workflows.ComfyWorkflowManager;
|
||||||
export const ComfyWorkflow = window.comfyAPI.workflows.ComfyWorkflow;
|
export const ComfyWorkflow = window.comfyAPI.workflows.ComfyWorkflow;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user