Files
JeyzerMC-comfyui-flakes/flake_compose.py
T

128 lines
4.0 KiB
Python

from __future__ import annotations
import logging
import os
from typing import Any
import numpy as np
import torch
from PIL import Image, ImageOps
import folder_paths
from nodes import (
CLIPTextEncode,
ControlNetApplyAdvanced,
ControlNetLoader,
EmptyLatentImage,
LoraLoader,
)
from . import flake_io
def _resolve_lora_name(stem_or_name: str) -> str:
"""Accept either a full filename under models/loras/ or a stem; return the filename that LoraLoader expects."""
available = folder_paths.get_filename_list("loras")
available_set = set(available)
norm = stem_or_name.replace("\\", "/")
if norm in available_set:
return norm
for candidate in available:
cand_norm = candidate.replace("\\", "/")
stem, _ = os.path.splitext(cand_norm)
if stem == norm:
return candidate
raise FileNotFoundError(
f"LoRA '{stem_or_name}' not found in models/loras/. "
f"Provide the stem or full filename of an existing LoRA."
)
def _load_cn_image(image_name: str) -> torch.Tensor:
"""Load an image tensor shaped [1, H, W, 3] (ComfyUI's IMAGE format) from ComfyUI/input/.
`image_name` may be a stem ('standing_openpose') or a filename with extension.
Subdirectories under input/ are allowed ('cnet/standing_openpose').
"""
input_dir = folder_paths.get_input_directory()
norm = image_name.replace("\\", "/")
candidates: list[str] = []
direct = os.path.join(input_dir, norm)
if os.path.isfile(direct):
candidates.append(direct)
else:
for ext in (".png", ".jpg", ".jpeg", ".webp"):
alt = os.path.join(input_dir, f"{norm}{ext}")
if os.path.isfile(alt):
candidates.append(alt)
break
if not candidates:
raise FileNotFoundError(
f"ControlNet image '{image_name}' not found in {input_dir}. "
f"Place the image there (any of .png/.jpg/.jpeg/.webp) or use a full filename."
)
img = Image.open(candidates[0])
img = ImageOps.exif_transpose(img).convert("RGB")
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr)[None,]
def compose(
model: Any,
clip: Any,
entries: list[dict[str, Any]],
) -> tuple:
flakes = [flake_io.resolve(e) for e in entries]
lora_loader = LoraLoader()
for f in flakes:
if not f.lora_path:
continue
lora_name = _resolve_lora_name(f.lora_path)
model, clip = lora_loader.load_lora(model, clip, lora_name, f.strength, f.strength)
pos_text = " BREAK ".join(f.positive.strip() for f in flakes if f.positive and f.positive.strip())
neg_text = ", ".join(f.negative.strip() for f in flakes if f.negative and f.negative.strip())
encoder = CLIPTextEncode()
positive = encoder.encode(clip, pos_text)[0]
negative = encoder.encode(clip, neg_text)[0]
cn_model_cache: dict[str, Any] = {}
cn_loader = ControlNetLoader()
cn_apply = ControlNetApplyAdvanced()
for f in flakes:
for cn in f.controlnets:
if cn.strength == 0:
continue
if cn.model_name not in cn_model_cache:
cn_model_cache[cn.model_name] = cn_loader.load_controlnet(cn.model_name)[0]
cn_model = cn_model_cache[cn.model_name]
image = _load_cn_image(cn.image_name)
positive, negative = cn_apply.apply_controlnet(
positive, negative, cn_model, image,
cn.strength, cn.start_percent, cn.end_percent,
)
width, height = 1024, 1024
for f in flakes:
if f.resolution is not None:
width, height = f.resolution
break
latent = EmptyLatentImage().generate(width, height, 1)[0]
logging.info(
"[FlakeStack] composed %d flake(s), resolution %dx%d, %d controlnet(s)",
len(flakes), width, height,
sum(len(f.controlnets) for f in flakes),
)
return model, clip, positive, negative, latent, width, height