diff --git a/__init__.py b/__init__.py index 5793f73..e2a4395 100644 --- a/__init__.py +++ b/__init__.py @@ -6,7 +6,21 @@ SYMBOL = "🍌" NODE_SURFIX = f"|cgem156" ROOT_NAME = f"cgem156 {SYMBOL}/" -scripts = ["batch_condition", "dart", "lortnoc", "attention_couple", "cd_tuner", "lora_merger", "multiple_lora_loader", "custom_samplers", "custom_schedulers", "scale_crafter", "aesthetic_shadow", "for_test"] +scripts = [ + "batch_condition", + "dart", + "lortnoc", + "attention_couple", + "cd_tuner", + "lora_merger", + "multiple_lora_loader", + "custom_samplers", + "custom_schedulers", + "scale_crafter", + "aesthetic_shadow", + "for_test", + "lora_xy", +] try: import timm diff --git a/scripts/lora_xy/__init__.py b/scripts/lora_xy/__init__.py new file mode 100644 index 0000000..bb96b9d --- /dev/null +++ b/scripts/lora_xy/__init__.py @@ -0,0 +1,19 @@ +from .node import LoraLoaderModelOnlyXY, SamplerCustomXY, KSamplerXY, KSamplerAdvancedXY, PreviewXY +from ... import SYMBOL, NODE_SURFIX + +NODE_CLASS_MAPPINGS = { + f"LoraLoaderModelOnlyXY{NODE_SURFIX}": LoraLoaderModelOnlyXY, + f"SamplerCustomXY{NODE_SURFIX}": SamplerCustomXY, + f"KSamplerXY{NODE_SURFIX}": KSamplerXY, + f"KSamplerAdvancedXY{NODE_SURFIX}": KSamplerAdvancedXY, + f"PreviewXY{NODE_SURFIX}": PreviewXY +} + +NODE_DISPLAY_NAME_MAPPINGS = { + f"LoraLoaderModelOnlyXY{NODE_SURFIX}": f"Lora Loader Model Only XY {SYMBOL}", + f"SamplerCustomXY{NODE_SURFIX}": f"Sampler Custom XY {SYMBOL}", + f"KSamplerXY{NODE_SURFIX}": f"KSampler XY {SYMBOL}", + f"KSamplerAdvancedXY{NODE_SURFIX}": f"KSampler Advanced XY {SYMBOL}", + f"PreviewXY{NODE_SURFIX}": f"Preview XY {SYMBOL}" +} + diff --git a/scripts/lora_xy/node.py b/scripts/lora_xy/node.py new file mode 100644 index 0000000..b25fa5e --- /dev/null +++ b/scripts/lora_xy/node.py @@ -0,0 +1,202 @@ +import comfy +from comfy_extras.nodes_custom_sampler import SamplerCustom +import folder_paths +from nodes import LoraLoader, PreviewImage, KSampler, KSamplerAdvanced +from ... import ROOT_NAME +import torch +from PIL import Image, ImageFont, ImageDraw +import numpy as np +import os + +CATEGORY_NAME = ROOT_NAME + "lora_xy" + +class LoraLoaderModelOnlyXY(LoraLoader): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "lora_name": (folder_paths.get_filename_list("loras"), ), + "strength_list": ("STRING", {"multiline": True}), + } + } + RETURN_TYPES = ("XY_MODEL","XY_LIST", ) + FUNCTION = "load_lora_model_only_xy" + CATEGORY = CATEGORY_NAME + + + def load_lora_model_only_xy(self, model, lora_name, strength_list): + models = [] + xy_list = [] + + weights = [float(x.strip()) for x in strength_list.strip().strip(",").split(",")] + for value in weights: + models.append(self.load_lora(model, None, lora_name, value, 0)[0]) + xy_list.append(f"{lora_name.split('.')[0]}:{value}") + + return (models, xy_list) + +class SamplerCustomXY(SamplerCustom): + @classmethod + def INPUT_TYPES(s): + return {"required": + {"model_xy": ("XY_MODEL",), + "add_noise": ("BOOLEAN", {"default": True}), + "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "sampler": ("SAMPLER", ), + "sigmas": ("SIGMAS", ), + "latent_image": ("LATENT", ), + } + } + + FUNCTION = "sample_xy" + CATEGORY = CATEGORY_NAME + + def sample_xy(self, model_xy, **kwargs): + outputs = [] + denoised_outputs = [] + + for model in model_xy: + output, denoised_output = self.sample(model, **kwargs) + outputs.append(output["samples"]) + denoised_outputs.append(denoised_output["samples"]) + + return ({"samples":torch.cat(outputs)}, {"samples":torch.cat(denoised_outputs)}) + +class KSamplerXY(KSampler): + @classmethod + def INPUT_TYPES(s): + return {"required": + {"model_xy": ("XY_MODEL",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "latent_image": ("LATENT", ), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + } + } + + FUNCTION = "sample_xy" + CATEGORY = CATEGORY_NAME + + def sample_xy(self, model_xy, **kwargs): + outputs = [] + + for model in model_xy: + output = self.sample(model, **kwargs)[0] + outputs.append(output["samples"]) + + return ({"samples":torch.cat(outputs)},) + +class KSamplerAdvancedXY(KSamplerAdvanced): + @classmethod + def INPUT_TYPES(s): + return {"required": + {"model_xy": ("XY_MODEL",), + "add_noise": (["enable", "disable"], ), + "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "latent_image": ("LATENT", ), + "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}), + "return_with_leftover_noise": (["disable", "enable"], ), + } + } + + FUNCTION = "sample_xy" + CATEGORY = CATEGORY_NAME + + def sample_xy(self, model_xy, **kwargs): + outputs = [] + + for model in model_xy: + output = self.sample(model, **kwargs)[0] + outputs.append(output["samples"]) + + return ({"samples":torch.cat(outputs)},) + + +class PreviewXY(PreviewImage): + @classmethod + def INPUT_TYPES(s): + return { + "required":{"images": ("IMAGE", ), "xy_list": ("XY_LIST", )}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + CATEGORY_NAME = ROOT_NAME + "preview_xy" + + def save_images(self, images, xy_list, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + results = list() + + pil_images = [] + for (batch_number, image) in enumerate(images): + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + pil_images.append(img) + + img = self.xy_plot(pil_images, xy_list) + + + file = "lora_xy_.png" + img.save(os.path.join(full_output_folder, file), compress_level=self.compress_level) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + counter += 1 + + return { "ui": { "images": results } } + + def xy_plot(self, images, xy_list, text_height=100): + n = len(xy_list) + m = len(images) // n + + image_width, image_height = images[0].width, images[0].height + + # キャンバスのサイズを再計算(全画像が同じサイズの場合) + canvas_width = image_width * n + canvas_height = (image_height * m) + text_height # 文字列の高さ分を追加 + + # キャンバスを再作成 + canvas = Image.new('RGB', (canvas_width, canvas_height), 'white') + + # 画像と文字列の画像をキャンバスに配置(全画像が同じサイズの場合の最適化) + for i, img in enumerate(images): + # 画像を配置する位置を計算 + x_offset = (i // m) * image_width + y_offset = (i % m) * (image_height) + text_height # 文字列の高さ分をオフセットして再計算 + canvas.paste(img, (x_offset, y_offset)) + + text_images = [self.text_to_image(title).resize((image_width, text_height)) for title in xy_list] + + # 文字列の画像をキャンバスに配置(各列の上部に) + for i, text_img in enumerate(text_images): + canvas.paste(text_img, (i * image_width, 0)) + + return canvas + + def text_to_image(self, text): + font = ImageFont.load_default() + img = Image.new('RGB', (256, 20), 'white') + draw = ImageDraw.Draw(img) + text_width, text_height = draw.textbbox((0,0), text, font=font)[2:] + text_x = (256 - text_width) / 2 + text_y = (20 - text_height) / 2 + draw.text((text_x, text_y), text, font=font, fill='black') + return img