900 lines
28 KiB
Python
900 lines
28 KiB
Python
import contextlib
|
|
import random
|
|
from collections import Counter
|
|
|
|
import numpy as np
|
|
import torch
|
|
from compel import Compel, DiffusersTextualInversionManager
|
|
from diffusers import StableDiffusionPipeline
|
|
from diffusers.models import ControlNetModel
|
|
from diffusers.utils.torch_utils import randn_tensor
|
|
from PIL import Image
|
|
|
|
import comfy.model_management
|
|
import folder_paths
|
|
from comfy.utils import ProgressBar
|
|
|
|
from .pipelines import ControlNetUnit, ControlNetUnits, PipelineWrapper, schedulers
|
|
|
|
|
|
def resize_with_padding(image: Image.Image, target_size: tuple[int, int]):
|
|
# 打开图像
|
|
|
|
# 计算缩放比例
|
|
width_ratio = target_size[0] / image.width
|
|
height_ratio = target_size[1] / image.height
|
|
ratio = min(width_ratio, height_ratio)
|
|
|
|
# 计算调整后的尺寸
|
|
new_width = int(image.width * ratio)
|
|
new_height = int(image.height * ratio)
|
|
|
|
# 缩放图像
|
|
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
|
|
# 创建黑色背景图像
|
|
background = Image.new("RGBA", target_size, (0, 0, 0, 0))
|
|
|
|
# 计算粘贴位置
|
|
position = ((target_size[0] - new_width) // 2, (target_size[1] - new_height) // 2)
|
|
|
|
# 粘贴调整后的图像到黑色背景上
|
|
background.paste(image, position)
|
|
return background
|
|
|
|
|
|
def comfy_image_to_pil(image: torch.Tensor):
|
|
image = image.squeeze(0) # (1, H, W, C) => (H, W, C)
|
|
image = image * 255 # 0 ~ 1 => 0 ~ 255
|
|
image = image.to(dtype=torch.uint8) # float32 => uint8
|
|
return Image.fromarray(image.numpy()) # tensor => PIL.Image.Image
|
|
|
|
|
|
def get_prompt_embeds(pipe, prompt, negative_prompt):
|
|
textual_inversion_manager = DiffusersTextualInversionManager(pipe)
|
|
compel = Compel(
|
|
tokenizer=pipe.tokenizer,
|
|
text_encoder=pipe.text_encoder,
|
|
textual_inversion_manager=textual_inversion_manager,
|
|
truncate_long_prompts=False,
|
|
)
|
|
|
|
prompt_embeds = compel.build_conditioning_tensor(prompt)
|
|
negative_prompt_embeds = compel.build_conditioning_tensor(negative_prompt)
|
|
[
|
|
prompt_embeds,
|
|
negative_prompt_embeds,
|
|
] = compel.pad_conditioning_tensors_to_same_length(
|
|
[prompt_embeds, negative_prompt_embeds]
|
|
)
|
|
return prompt_embeds, negative_prompt_embeds
|
|
|
|
|
|
def latents_to_img_tensor(pipeline, latents):
|
|
# 1. 输入的 latents 是一个 -1 ~ 1 之间的 tensor
|
|
# 2. 先进行缩放
|
|
scaled_latents = latents / pipeline.vae.config.scaling_factor
|
|
# 转成 vae 类型
|
|
scaled_latents = scaled_latents.to(dtype=comfy.model_management.vae_dtype())
|
|
print(scaled_latents.dtype, pipeline.vae.dtype)
|
|
# 3. 解码,返回的是 -1 ~ 1 之间的 tensor
|
|
dec_tensor = pipeline.vae.decode(scaled_latents, return_dict=False)[0]
|
|
# 4. 缩放到 0 ~ 1 之间
|
|
dec_images = pipeline.image_processor.postprocess(
|
|
dec_tensor,
|
|
output_type="pt",
|
|
do_denormalize=[True for _ in range(scaled_latents.shape[0])],
|
|
)
|
|
# 5. 转换成 tensor,
|
|
res = torch.nan_to_num(dec_images).to(dtype=torch.float32)
|
|
# 6. 将 channel 放到最后
|
|
# res shape torch.Size([1, 3, 512, 512]) => torch.Size([1, 512, 512, 3])
|
|
res = res.permute(0, 2, 3, 1)
|
|
return res
|
|
|
|
|
|
def latents_to_mask_tensor(pipeline, latents):
|
|
# 1. 输入的 latents 是一个 -1 ~ 1 之间的 tensor
|
|
# 2. 先进行缩放
|
|
scaled_latents = latents / pipeline.vae.config.scaling_factor
|
|
# 3. 解码,返回的是 -1 ~ 1 之间的 tensor
|
|
dec_tensor = pipeline.vae.decode(scaled_latents, return_dict=False)[0]
|
|
# 4. 缩放到 0 ~ 1 之间
|
|
dec_images = pipeline.mask_processor.postprocess(
|
|
dec_tensor,
|
|
output_type="pt",
|
|
)
|
|
# 5. 转换成 tensor,
|
|
res = torch.nan_to_num(dec_images).to(dtype=torch.float32)
|
|
# 6. 将 channel 放到最后
|
|
# res shape torch.Size([1, 3, 512, 512]) => torch.Size([1, 512, 512, 3])
|
|
res = res.permute(0, 2, 3, 1)
|
|
return res
|
|
|
|
|
|
def prepare_latents(
|
|
pipe: StableDiffusionPipeline,
|
|
batch_size: int,
|
|
height: int,
|
|
width: int,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
generator: torch.Generator,
|
|
latents=None,
|
|
):
|
|
shape = (
|
|
batch_size,
|
|
pipe.unet.config.in_channels,
|
|
height // pipe.vae_scale_factor,
|
|
width // pipe.vae_scale_factor,
|
|
)
|
|
if isinstance(generator, list) and len(generator) != batch_size:
|
|
raise ValueError(
|
|
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
|
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
|
)
|
|
|
|
if latents is None:
|
|
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
|
|
else:
|
|
latents = latents.to(device)
|
|
|
|
# scale the initial noise by the standard deviation required by the scheduler
|
|
latents = latents * pipe.scheduler.init_noise_sigma
|
|
return latents
|
|
|
|
|
|
def prepare_image(
|
|
pipeline: StableDiffusionPipeline,
|
|
seed=47,
|
|
batch_size=1,
|
|
height=512,
|
|
width=512,
|
|
):
|
|
generator = torch.Generator()
|
|
generator.manual_seed(seed)
|
|
latents = prepare_latents(
|
|
pipe=pipeline,
|
|
batch_size=batch_size,
|
|
height=height,
|
|
width=width,
|
|
generator=generator,
|
|
device=comfy.model_management.get_torch_device(),
|
|
dtype=comfy.model_management.VAE_DTYPE,
|
|
)
|
|
return latents_to_img_tensor(pipeline, latents)
|
|
|
|
|
|
class GetFilledColorImage:
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "run"
|
|
CATEGORY = "Jannchie"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"width": (
|
|
"INT",
|
|
{
|
|
"default": 512,
|
|
"min": 0,
|
|
"max": 8192,
|
|
"step": 64,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"height": (
|
|
"INT",
|
|
{
|
|
"default": 512,
|
|
"min": 0,
|
|
"max": 8192,
|
|
"step": 64,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"red": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.1,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"green": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.1,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"blue": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.1,
|
|
"display": "number",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(self, width, height, red, green, blue):
|
|
image = torch.tensor(np.full((height, width, 3), (red, green, blue)))
|
|
# 再转换成 0 - 1 之间的浮点数
|
|
image = image
|
|
image = image.unsqueeze(0)
|
|
return (image,)
|
|
|
|
|
|
class DiffusersCompelPromptEmbedding:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("DIFFUSERS_PROMPT_EMBEDDING", "DIFFUSERS_PROMPT_EMBEDDING")
|
|
RETURN_NAMES = ("positive prompt embedding", "negative prompt embedding")
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipeline": ("DIFFUSERS_PIPELINE",),
|
|
"positive_prompt": (
|
|
"STRING",
|
|
{
|
|
"multiline": True,
|
|
"default": "(masterpiece)1.2, (best quality)1.4",
|
|
},
|
|
),
|
|
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
|
}
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
pipeline: StableDiffusionPipeline,
|
|
positive_prompt: str,
|
|
negative_prompt: str,
|
|
):
|
|
return get_prompt_embeds(pipeline, positive_prompt, negative_prompt)
|
|
|
|
|
|
class DiffusersTextureInversionLoader:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("DIFFUSERS_PIPELINE",)
|
|
RETURN_NAMES = ("pipeline",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipeline": ("DIFFUSERS_PIPELINE",),
|
|
"texture_inversion": (folder_paths.get_filename_list("embeddings"),),
|
|
},
|
|
}
|
|
|
|
def run(self, pipeline: StableDiffusionPipeline, texture_inversion: str):
|
|
with contextlib.suppress(Exception):
|
|
path = folder_paths.get_full_path("embeddings", texture_inversion)
|
|
token = texture_inversion.split(".")[0]
|
|
pipeline.load_textual_inversion(path, token=token)
|
|
print(f"Loaded {texture_inversion}")
|
|
return (pipeline,)
|
|
|
|
|
|
class GetAverageColorFromImage:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT")
|
|
RETURN_NAMES = ("red", "green", "blue")
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"average": ("STRING", {"default": "mean", "options": ["mean", "mode"]}),
|
|
},
|
|
"optional": {
|
|
"mask": ("MASK",),
|
|
},
|
|
}
|
|
|
|
def run(self, image: torch.Tensor, average: str, mask: torch.Tensor = None):
|
|
if average == "mean":
|
|
return self.run_avg(image, mask)
|
|
elif average == "mode":
|
|
return self.run_mode(image, mask)
|
|
|
|
def run_avg(self, image: torch.Tensor, mask: torch.Tensor = None):
|
|
if mask is not None:
|
|
mask = mask.unsqueeze(1)
|
|
masked_image = image * mask if mask is not None else image
|
|
pixel_sum = torch.sum(masked_image, dim=(2, 3))
|
|
pixel_count = (
|
|
torch.sum(mask, dim=(2, 3))
|
|
if mask is not None
|
|
else torch.prod(torch.tensor(image.shape[2:]))
|
|
)
|
|
average_rgb = pixel_sum / pixel_count.unsqueeze(1)
|
|
|
|
average_rgb = torch.round(average_rgb)
|
|
|
|
return tuple(average_rgb.squeeze().tolist())
|
|
|
|
def run_mode(self, image: torch.Tensor, mask: torch.Tensor = None):
|
|
image = image.permute(0, 3, 1, 2)
|
|
if mask is not None:
|
|
mask = mask.unsqueeze(1)
|
|
|
|
masked_image = image * mask if mask is not None else image
|
|
pixel_values = masked_image.view(
|
|
masked_image.shape[0], masked_image.shape[1], -1
|
|
)
|
|
pixel_values = pixel_values.permute(0, 2, 1)
|
|
pixel_values = pixel_values.reshape(-1, pixel_values.shape[2])
|
|
pixel_values = [
|
|
tuple(color.tolist()) for color in pixel_values.numpy() if color.max() > 0
|
|
]
|
|
|
|
if not pixel_values:
|
|
return (0, 0, 0)
|
|
|
|
color_counts = Counter(pixel_values)
|
|
|
|
return max(color_counts, key=color_counts.get)
|
|
|
|
|
|
class DiffusersXLPipeline:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("DIFFUSERS_PIPELINE",)
|
|
RETURN_NAMES = ("pipeline",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"ckpt_name": ([],),
|
|
},
|
|
"optional": {
|
|
"vae_name": (
|
|
folder_paths.get_filename_list("vae") + ["-"],
|
|
{"default": "-"},
|
|
),
|
|
"scheduler_name": (
|
|
list(schedulers.keys()) + ["-"],
|
|
{
|
|
"default": "-",
|
|
},
|
|
),
|
|
"use_tiny_vae": (
|
|
["disable", "enable"],
|
|
{
|
|
"default": "disable",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
ckpt_name: str,
|
|
vae_name: str = None,
|
|
scheduler_name: str = None,
|
|
use_tiny_vae: str = "disable",
|
|
):
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
|
if ckpt_path is None:
|
|
ckpt_path = ckpt_name
|
|
if vae_name == "-":
|
|
vae_path = None
|
|
else:
|
|
vae_path = folder_paths.get_full_path("vae", vae_name)
|
|
if scheduler_name == "-":
|
|
scheduler_name = None
|
|
|
|
self.pipeline_wrapper = PipelineWrapper(
|
|
ckpt_path,
|
|
vae_path,
|
|
scheduler_name,
|
|
pipeline=StableDiffusionPipeline,
|
|
use_tiny_vae=use_tiny_vae == "enable",
|
|
)
|
|
return (self.pipeline_wrapper.pipeline,)
|
|
|
|
|
|
class DiffusersPipeline:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("DIFFUSERS_PIPELINE",)
|
|
RETURN_NAMES = ("pipeline",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
|
|
},
|
|
"optional": {
|
|
"vae_name": (
|
|
folder_paths.get_filename_list("vae") + ["-"],
|
|
{"default": "-"},
|
|
),
|
|
"scheduler_name": (
|
|
list(schedulers.keys()) + ["-"],
|
|
{
|
|
"default": "-",
|
|
},
|
|
),
|
|
"use_tiny_vae": (
|
|
["disable", "enable"],
|
|
{
|
|
"default": "disable",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
ckpt_name: str,
|
|
vae_name: str = None,
|
|
scheduler_name: str = None,
|
|
use_tiny_vae: str = "disable",
|
|
):
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
|
if ckpt_path is None:
|
|
ckpt_path = ckpt_name
|
|
if vae_name == "-":
|
|
vae_path = None
|
|
else:
|
|
vae_path = folder_paths.get_full_path("vae", vae_name)
|
|
if scheduler_name == "-":
|
|
scheduler_name = None
|
|
|
|
self.pipeline_wrapper = PipelineWrapper(
|
|
ckpt_path, vae_path, scheduler_name, use_tiny_vae=use_tiny_vae == "enable"
|
|
)
|
|
return (self.pipeline_wrapper.pipeline,)
|
|
|
|
|
|
class DiffusersPrepareLatents:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("latents",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipeline": ("DIFFUSERS_PIPELINE",),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 16, "step": 1}),
|
|
"height": ("INT", {"default": 512, "min": 0, "max": 8192, "step": 64}),
|
|
"width": ("INT", {"default": 512, "min": 0, "max": 8192, "step": 64}),
|
|
},
|
|
"optional": {
|
|
"latents": ("LATENT", {"default": None}),
|
|
"seed": (
|
|
"INT",
|
|
{"default": None, "min": 0, "step": 1, "max": 999999999},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
pipeline: StableDiffusionPipeline,
|
|
batch_size: int = 1,
|
|
height: int = 512,
|
|
width: int = 512,
|
|
latents: torch.Tensor | None = None,
|
|
seed: int | None = None,
|
|
):
|
|
if seed is None:
|
|
seed = random.randint(0, 999999999)
|
|
device = comfy.model_management.get_torch_device()
|
|
generator = torch.Generator(device)
|
|
generator.manual_seed(seed)
|
|
latents = prepare_latents(
|
|
pipe=pipeline,
|
|
batch_size=batch_size,
|
|
height=height,
|
|
width=width,
|
|
dtype=comfy.model_management.VAE_DTYPE,
|
|
device=device,
|
|
generator=generator,
|
|
latents=latents,
|
|
)
|
|
return (latents,)
|
|
|
|
|
|
class DiffusersDecoder:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("images",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipeline": ("DIFFUSERS_PIPELINE",),
|
|
"latents": ("LATENT",),
|
|
},
|
|
}
|
|
|
|
def run(self, pipeline: StableDiffusionPipeline, latents: torch.Tensor):
|
|
res = latents_to_img_tensor(pipeline, latents)
|
|
return (res,)
|
|
|
|
|
|
# 'https://huggingface.co/lllyasviel/ControlNet-v1-1/blob/main/control_v11p_sd15_canny.pth'
|
|
|
|
controlnet_list = [
|
|
"canny",
|
|
"openpose",
|
|
"depth",
|
|
"tile",
|
|
"ip2p",
|
|
"shuffle",
|
|
"inpaint",
|
|
"lineart",
|
|
"mlsd",
|
|
"normalbae",
|
|
"scribble",
|
|
"seg",
|
|
"softedge",
|
|
"lineart_anime",
|
|
"other",
|
|
]
|
|
|
|
|
|
class DiffusersControlNetLoader:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("DIFFUSERS_CONTROLNET",)
|
|
RETURN_NAMES = ("controlnet",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"controlnet_model_name": (controlnet_list,),
|
|
},
|
|
"optional": {
|
|
"controlnet_model_file": (folder_paths.get_filename_list("controlnet"),)
|
|
},
|
|
}
|
|
|
|
def run(self, controlnet_model_name: str, controlnet_model_file: str = ""):
|
|
file_list = folder_paths.get_filename_list("controlnet")
|
|
if controlnet_model_name == "other":
|
|
controlnet_model_path = folder_paths.get_full_path(
|
|
"controlnet", controlnet_model_file
|
|
)
|
|
else:
|
|
if controlnet_model_name == "depth":
|
|
file_name = f"control_v11f1p_sd15_{controlnet_model_name}.pth"
|
|
elif controlnet_model_name == "tile":
|
|
file_name = f"control_v11f1e_sd15_{controlnet_model_name}.pth"
|
|
else:
|
|
file_name = f"control_v11p_sd15_{controlnet_model_name}.pth"
|
|
controlnet_model_path = next(
|
|
(
|
|
folder_paths.get_full_path("controlnet", file)
|
|
for file in file_list
|
|
if file_name in file
|
|
),
|
|
None,
|
|
)
|
|
if controlnet_model_path is None:
|
|
controlnet_model_path = f"https://huggingface.co/lllyasviel/ControlNet-v1-1/blob/main/{file_name}"
|
|
controlnet = ControlNetModel.from_single_file(
|
|
controlnet_model_path,
|
|
cache_dir=folder_paths.get_folder_paths("controlnet")[0],
|
|
).to(
|
|
device=comfy.model_management.get_torch_device(),
|
|
dtype=comfy.model_management.unet_dtype(),
|
|
)
|
|
return (controlnet,)
|
|
|
|
|
|
class DiffusersControlNetUnit:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("CONTROLNET_UNIT",)
|
|
RETURN_NAMES = ("controlnet unit",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"controlnet": ("DIFFUSERS_CONTROLNET",),
|
|
"image": ("IMAGE",),
|
|
"scale": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1},
|
|
),
|
|
"start": (
|
|
"FLOAT",
|
|
{"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.1},
|
|
),
|
|
"end": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
controlnet: ControlNetModel,
|
|
image: torch.Tensor,
|
|
scale: float,
|
|
start: float,
|
|
end: float,
|
|
):
|
|
unit = ControlNetUnit(
|
|
controlnet=controlnet,
|
|
image=comfy_image_to_pil(image),
|
|
scale=scale,
|
|
start=start,
|
|
end=end,
|
|
)
|
|
return ((unit,),)
|
|
|
|
|
|
class DiffusersControlNetUnitStack:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("CONTROLNET_UNIT",)
|
|
RETURN_NAMES = ("controlnet unit",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"controlnet_unit_1": ("CONTROLNET_UNIT",),
|
|
},
|
|
"optional": {
|
|
"controlnet_unit_2": (
|
|
"CONTROLNET_UNIT",
|
|
{
|
|
"default": None,
|
|
},
|
|
),
|
|
"controlnet_unit_3": (
|
|
"CONTROLNET_UNIT",
|
|
{
|
|
"default": None,
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
controlnet_unit_1: tuple[ControlNetModel],
|
|
controlnet_unit_2: tuple[ControlNetModel] | None,
|
|
controlnet_unit_3: tuple[ControlNetModel] | None,
|
|
):
|
|
stack = []
|
|
if controlnet_unit_1:
|
|
stack += controlnet_unit_1
|
|
if controlnet_unit_2:
|
|
stack += controlnet_unit_2
|
|
if controlnet_unit_3:
|
|
stack += controlnet_unit_3
|
|
return (stack,)
|
|
|
|
|
|
class DiffusersGenerator:
|
|
CATEGORY = "Jannchie"
|
|
FUNCTION = "run"
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("images",)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pipeline": ("DIFFUSERS_PIPELINE",),
|
|
"positive_prompt_embedding": ("DIFFUSERS_PROMPT_EMBEDDING",),
|
|
"negative_prompt_embedding": ("DIFFUSERS_PROMPT_EMBEDDING",),
|
|
"strength": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.02},
|
|
),
|
|
"num_inference_steps": (
|
|
"INT",
|
|
{"default": 30, "min": 1, "max": 100, "step": 1},
|
|
),
|
|
"guidance_scale": (
|
|
"FLOAT",
|
|
{"default": 7.0, "min": 0.0, "max": 30.0, "step": 0.02},
|
|
),
|
|
"seed": (
|
|
"INT",
|
|
{"default": 0, "min": 0, "step": 1, "max": 999999999999},
|
|
),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 16, "step": 1}),
|
|
"width": (
|
|
"INT",
|
|
{
|
|
"default": 512,
|
|
"min": 64,
|
|
"max": 8192,
|
|
"step": 64,
|
|
},
|
|
),
|
|
"height": (
|
|
"INT",
|
|
{
|
|
"default": 512,
|
|
"min": 64,
|
|
"max": 8192,
|
|
"step": 64,
|
|
},
|
|
),
|
|
"reference_style_fidelity": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.5,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.1,
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"images": ("IMAGE",),
|
|
"mask": ("MASK",),
|
|
"controlnet_units": ("CONTROLNET_UNIT",),
|
|
"reference_image": (
|
|
"IMAGE",
|
|
{"default": None},
|
|
),
|
|
"reference_only": (
|
|
["disable", "enable"],
|
|
{
|
|
"default": "disable",
|
|
},
|
|
),
|
|
"reference_only_adain": (
|
|
["disable", "enable"],
|
|
{
|
|
"default": "disable",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
def run(
|
|
self,
|
|
pipeline: StableDiffusionPipeline,
|
|
positive_prompt_embedding: torch.Tensor,
|
|
negative_prompt_embedding: torch.Tensor,
|
|
width: int,
|
|
height: int,
|
|
batch_size: int,
|
|
images: torch.Tensor | None = None,
|
|
num_inference_steps: int = 30,
|
|
strength: float = 1.0,
|
|
guidance_scale: float = 7.0,
|
|
controlnet_units: tuple[ControlNetUnit] = None,
|
|
seed=None,
|
|
mask: torch.Tensor | None = None,
|
|
reference_only: str = "disable",
|
|
reference_only_adain: str = "disable",
|
|
reference_image: torch.Tensor | None = None,
|
|
reference_style_fidelity: float = 0.5,
|
|
):
|
|
reference_only = reference_only == "enable"
|
|
reference_only_adain = reference_only_adain == "enable"
|
|
latents = None
|
|
pbar = ProgressBar(int(num_inference_steps * strength))
|
|
device = comfy.model_management.get_torch_device()
|
|
if not seed:
|
|
seed = random.randint(0, 999999999999)
|
|
generator = torch.Generator(device)
|
|
generator.manual_seed(seed)
|
|
# (B, H, W, C) to (B, C, H, W)
|
|
if images is None:
|
|
latents = prepare_latents(
|
|
pipe=pipeline,
|
|
batch_size=batch_size,
|
|
height=height,
|
|
width=width,
|
|
generator=generator,
|
|
device=device,
|
|
dtype=comfy.model_management.VAE_DTYPE,
|
|
)
|
|
images = latents_to_img_tensor(pipeline, latents)
|
|
else:
|
|
images = images
|
|
|
|
# positive_prompt_embedding 和 negative_prompt_embedding 需要匹配 batch_size
|
|
positive_prompt_embedding = positive_prompt_embedding.repeat(batch_size, 1, 1)
|
|
negative_prompt_embedding = negative_prompt_embedding.repeat(batch_size, 1, 1)
|
|
width = images.shape[2]
|
|
height = images.shape[1]
|
|
|
|
def callback(*_):
|
|
pbar.update(1)
|
|
|
|
if controlnet_units is not None:
|
|
for unit in controlnet_units:
|
|
target_image_shape = (width, height)
|
|
unit_img = resize_with_padding(unit.image, target_image_shape)
|
|
unit.image = unit_img
|
|
controlnet_units = ControlNetUnits(controlnet_units)
|
|
result = pipeline(
|
|
image=images,
|
|
mask_image=mask,
|
|
ref_image=reference_image if reference_image is not None else images,
|
|
generator=generator,
|
|
width=width,
|
|
height=height,
|
|
prompt_embeds=positive_prompt_embedding,
|
|
negative_prompt_embeds=negative_prompt_embedding,
|
|
num_inference_steps=num_inference_steps,
|
|
guidance_scale=guidance_scale,
|
|
callback_steps=1,
|
|
strength=strength,
|
|
controlnet_units=controlnet_units,
|
|
callback=callback,
|
|
reference_attn=reference_only,
|
|
reference_adain=reference_only_adain,
|
|
style_fidelity=reference_style_fidelity,
|
|
return_dict=True,
|
|
)
|
|
# image = result["images"][0]
|
|
# images to torch.Tensor
|
|
imgs = [np.array(img) for img in result["images"]]
|
|
imgs = torch.tensor(imgs)
|
|
result["images"][0].save("1.png")
|
|
# 0 ~ 255 to 0 ~ 1
|
|
imgs = imgs / 255
|
|
# (B, C, H, W) to (B, H, W, C)
|
|
return (imgs,)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GetFilledColorImage": GetFilledColorImage,
|
|
"GetAverageColorFromImage": GetAverageColorFromImage,
|
|
"DiffusersPipeline": DiffusersPipeline,
|
|
"DiffusersXLPipeline": DiffusersXLPipeline,
|
|
"DiffusersGenerator": DiffusersGenerator,
|
|
"DiffusersPrepareLatents": DiffusersPrepareLatents,
|
|
"DiffusersDecoder": DiffusersDecoder,
|
|
"DiffusersCompelPromptEmbedding": DiffusersCompelPromptEmbedding,
|
|
"DiffusersTextureInversionLoader": DiffusersTextureInversionLoader,
|
|
"DiffusersControlnetLoader": DiffusersControlNetLoader,
|
|
"DiffusersControlnetUnit": DiffusersControlNetUnit,
|
|
"DiffusersControlnetUnitStack": DiffusersControlNetUnitStack,
|
|
}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GetFilledColorImage": "Get Filled Color Image Jannchie",
|
|
"GetAverageColorFromImage": "Get Average Color From Image Jannchie",
|
|
"DiffusersPipeline": "🤗 Diffusers Pipeline",
|
|
"DiffusersXLPipeline": "🤗 Diffusers XL Pipeline",
|
|
"DiffusersGenerator": "🤗 Diffusers Generator",
|
|
"DiffusersPrepareLatents": "🤗 Diffusers Prepare Latents",
|
|
"DiffusersDecoder": "🤗 Diffusers Decoder",
|
|
"DiffusersCompelPromptEmbedding": "🤗 Diffusers Compel Prompt Embedding",
|
|
"DiffusersTextureInversionLoader": "🤗 Diffusers Texture Inversion Embedding Loader",
|
|
"DiffusersControlnetLoader": "🤗 Diffusers Controlnet Loader",
|
|
"DiffusersControlnetUnit": "🤗 Diffusers Controlnet Unit",
|
|
"DiffusersControlnetUnitStack": "🤗 Diffusers Controlnet Unit Stack",
|
|
}
|