Files
laksjdjf-cgem156-ComfyUI/scripts/lora_xy/node.py
T

329 lines
13 KiB
Python

import comfy
import comfy.samplers
import comfy.sd
import comfy.utils
from comfy_extras.nodes_custom_sampler import SamplerCustom
import nodes
import folder_paths
from ... import ROOT_NAME, NODE_SURFIX, SYMBOL
from comfy_api.v0_0_2 import io, ui
import torch
from PIL import Image, ImageFont, ImageDraw
import numpy as np
import os
import matplotlib.pyplot as plt
from PIL import Image
from io import BytesIO
def generate_image_matrix(images, xy_list):
num_images = len(images)
cols = len(xy_list) # 列数
rows = num_images // cols
fig, axes = plt.subplots(rows, cols, figsize=(cols * 2, rows * 2))
axes = axes.flatten() # 1次元配列化
for i in range(len(axes)):
if i < num_images:
axes[i].imshow(images[i])
axes[i].set_title(xy_list[i], fontsize=8)
axes[i].axis("off")
else:
axes[i].axis("off") # 余ったスペースを空白にする
plt.tight_layout()
# Figure をバイナリデータとして保存し、PIL画像に変換
buf = BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
plt.close(fig)
buf.seek(0)
return Image.open(buf)
CATEGORY_NAME = ROOT_NAME + "lora_xy"
# module-level cache replacing the old per-instance `self.loaded_lora` state from
# nodes.py's LoraLoader (execute() is a classmethod, no `self` to cache on).
_lora_xy_cache = {"loaded_lora": None}
def _load_lora_model_only(model, lora_name, strength_model):
# Mirrors nodes.py LoraLoader.load_lora(model, clip=None, lora_name, strength_model, strength_clip=0).
if strength_model == 0:
return model
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = None
lora_metadata = None
loaded_lora = _lora_xy_cache["loaded_lora"]
if loaded_lora is not None:
if loaded_lora[0] == lora_path:
lora = loaded_lora[1]
lora_metadata = loaded_lora[2] if len(loaded_lora) > 2 else None
else:
_lora_xy_cache["loaded_lora"] = None
if lora is None:
lora, lora_metadata = comfy.utils.load_torch_file(lora_path, safe_load=True, return_metadata=True)
_lora_xy_cache["loaded_lora"] = (lora_path, lora, lora_metadata)
model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0, lora_metadata=lora_metadata)
return model_lora
class LoraLoaderModelOnlyXY(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id=f"LoraLoaderModelOnlyXY{NODE_SURFIX}",
display_name=f"Lora Loader Model Only XY {SYMBOL}",
category=CATEGORY_NAME,
inputs=[
io.Model.Input("model"),
io.Combo.Input("lora_name", options=folder_paths.get_filename_list("loras")),
io.String.Input("strength_list", multiline=True),
],
outputs=[
io.Custom("XY_MODEL").Output(),
io.Custom("XY_LIST").Output(),
],
)
@classmethod
def execute(cls, model, lora_name, strength_list) -> io.NodeOutput:
models = []
xy_list = []
weights = [float(x.strip()) for x in strength_list.strip().strip(",").split(",")]
for value in weights:
models.append(_load_lora_model_only(model, lora_name, value))
xy_list.append(f"{lora_name.split('.')[0]}:{value}")
return io.NodeOutput(models, xy_list)
class SamplerCustomXY(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id=f"SamplerCustomXY{NODE_SURFIX}",
display_name=f"Sampler Custom XY {SYMBOL}",
category=CATEGORY_NAME,
inputs=[
io.Custom("XY_MODEL").Input("model_xy"),
io.Boolean.Input("add_noise", default=True),
io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff),
io.Float.Input("cfg", default=8.0, min=0.0, max=100.0, step=0.1, round=0.01),
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Sampler.Input("sampler"),
io.Sigmas.Input("sigmas"),
io.Latent.Input("latent_image"),
],
outputs=[
io.Latent.Output(display_name="output"),
io.Latent.Output(display_name="denoised_output"),
],
)
@classmethod
def execute(cls, model_xy, add_noise, noise_seed, cfg, positive, negative, sampler, sigmas, latent_image) -> io.NodeOutput:
outputs = []
denoised_outputs = []
# Composition, not inheritance: SamplerCustom is itself a V3 io.ComfyNode now, so we
# call its public `execute` classmethod per model instead of subclassing it. This keeps
# us in sync with upstream's noise/x0-output/nested-tensor handling without duplicating it.
for model in model_xy:
result = SamplerCustom.execute(
model=model,
add_noise=add_noise,
noise_seed=noise_seed,
cfg=cfg,
positive=positive,
negative=negative,
sampler=sampler,
sigmas=sigmas,
latent_image=latent_image,
)
output, denoised_output = result.result
outputs.append(output["samples"])
denoised_outputs.append(denoised_output["samples"])
return io.NodeOutput({"samples": torch.cat(outputs)}, {"samples": torch.cat(denoised_outputs)})
class KSamplerXY(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id=f"KSamplerXY{NODE_SURFIX}",
display_name=f"KSampler XY {SYMBOL}",
category=CATEGORY_NAME,
inputs=[
io.Custom("XY_MODEL").Input("model_xy"),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=20, min=1, max=10000),
io.Float.Input("cfg", default=8.0, min=0.0, max=100.0, step=0.1, round=0.01),
io.Combo.Input("sampler_name", options=comfy.samplers.KSampler.SAMPLERS),
io.Combo.Input("scheduler", options=comfy.samplers.KSampler.SCHEDULERS),
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Latent.Input("latent_image"),
io.Float.Input("denoise", default=1.0, min=0.0, max=1.0, step=0.01),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, model_xy, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise) -> io.NodeOutput:
outputs = []
# Composition: nodes.common_ksampler is the stable module-level function that both
# KSampler and KSamplerAdvanced wrap; calling it directly avoids depending on the
# KSampler node class itself.
for model in model_xy:
output = nodes.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)[0]
outputs.append(output["samples"])
return io.NodeOutput({"samples": torch.cat(outputs)})
class KSamplerAdvancedXY(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id=f"KSamplerAdvancedXY{NODE_SURFIX}",
display_name=f"KSampler Advanced XY {SYMBOL}",
category=CATEGORY_NAME,
inputs=[
io.Custom("XY_MODEL").Input("model_xy"),
io.Combo.Input("add_noise", options=["enable", "disable"]),
io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=20, min=1, max=10000),
io.Float.Input("cfg", default=8.0, min=0.0, max=100.0, step=0.1, round=0.01),
io.Combo.Input("sampler_name", options=comfy.samplers.KSampler.SAMPLERS),
io.Combo.Input("scheduler", options=comfy.samplers.KSampler.SCHEDULERS),
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Latent.Input("latent_image"),
io.Int.Input("start_at_step", default=0, min=0, max=10000),
io.Int.Input("end_at_step", default=10000, min=0, max=10000),
io.Combo.Input("return_with_leftover_noise", options=["disable", "enable"]),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, model_xy, add_noise, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
start_at_step, end_at_step, return_with_leftover_noise) -> io.NodeOutput:
outputs = []
force_full_denoise = True
if return_with_leftover_noise == "enable":
force_full_denoise = False
disable_noise = False
if add_noise == "disable":
disable_noise = True
# Composition: same nodes.common_ksampler function that KSamplerAdvanced.sample wraps.
for model in model_xy:
output = nodes.common_ksampler(model, noise_seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step,
force_full_denoise=force_full_denoise)[0]
outputs.append(output["samples"])
return io.NodeOutput({"samples": torch.cat(outputs)})
class XYImage:
@classmethod
def INPUT_TYPES(s):
return {
"required":{"images": ("IMAGE", ), "xy_list": ("XY_LIST", )},
}
FUNCTION = "xy_images"
CATEGORY_NAME = ROOT_NAME
def xy_images(self, images, xy_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)
imgs = generate_image_matrix(pil_images, xy_list)
img = np.array(imgs).astype(np.float32) / 255.
img = img * 2. - 1.
img = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0)
return {"images": img}
def _xy_text_to_image(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
def _xy_plot(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 = [_xy_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
class PreviewXY(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id=f"PreviewXY{NODE_SURFIX}",
display_name=f"Preview XY {SYMBOL}",
category=CATEGORY_NAME,
inputs=[
io.Image.Input("images"),
io.Custom("XY_LIST").Input("xy_list"),
],
outputs=[],
is_output_node=True,
)
@classmethod
def execute(cls, images, xy_list) -> io.NodeOutput:
pil_images = []
for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
pil_images.append(img)
canvas = _xy_plot(pil_images, xy_list)
canvas_np = np.array(canvas).astype(np.float32) / 255.
canvas_tensor = torch.from_numpy(canvas_np).unsqueeze(0)
return io.NodeOutput(ui=ui.PreviewImage(canvas_tensor, cls=cls))