2227 lines
75 KiB
Python
2227 lines
75 KiB
Python
class GetImageNthCount:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE", {"tooltip": "Input image batch\n输入图像批次"}),
|
|
"Nth_count": (
|
|
"INT",
|
|
{
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 2 * 31 - 1,
|
|
"step": 1,
|
|
"tooltip": "The index of the image to extract (starting from 1)\n要提取的图像索引(从1开始)",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "GetImageNthCount"
|
|
CATEGORY = "MakkiTools"
|
|
DEPRECATED = True
|
|
DESCRIPTION = (
|
|
"Extract the Nth image from an image batch.\n从图像批次中提取第N张图像。"
|
|
)
|
|
|
|
def GetImageNthCount(self, image, Nth_count):
|
|
return (image[Nth_count - 1 : Nth_count],)
|
|
|
|
|
|
class ImageChannelSeparate:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE", {"tooltip": "Input image\n输入图像"}),
|
|
"channel": (
|
|
["red", "green", "blue", "alpha"],
|
|
{
|
|
"default": "red",
|
|
"tooltip": "Select which channel to extract\n选择要提取的通道",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "ImageChannelSeparate"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = (
|
|
"Extract a specific color channel from an image.\n从图像中提取特定颜色通道。"
|
|
)
|
|
|
|
def ImageChannelSeparate(self, image, channel):
|
|
channel_map = {"red": 0, "green": 1, "blue": 2, "alpha": 3}
|
|
channel_index = channel_map[channel]
|
|
num_channels = image.shape[3]
|
|
if num_channels < 4 and channel_index == 3:
|
|
return (image,)
|
|
else:
|
|
import torch
|
|
|
|
separate = torch.zeros_like(image)
|
|
separate[..., channel_index] = image[..., channel_index]
|
|
if num_channels >= 4 and channel_index != 3:
|
|
separate[..., 3] = image[..., 3]
|
|
return (separate,)
|
|
|
|
|
|
class MergeImageChannels:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"optional": {
|
|
"red_channel": (
|
|
"IMAGE",
|
|
{"tooltip": "Red channel image\n红色通道图像"},
|
|
),
|
|
"green_channel": (
|
|
"IMAGE",
|
|
{"tooltip": "Green channel image\n绿色通道图像"},
|
|
),
|
|
"blue_channel": (
|
|
"IMAGE",
|
|
{"tooltip": "Blue channel image\n蓝色通道图像"},
|
|
),
|
|
"alpha_channel": (
|
|
"IMAGE",
|
|
{"tooltip": "Alpha channel image\nAlpha通道图像"},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "MergeImageChannels"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Merge separate color channels into a single image.\n将分离的颜色通道合并为单个图像。"
|
|
|
|
def MergeImageChannels(
|
|
self,
|
|
red_channel=None,
|
|
green_channel=None,
|
|
blue_channel=None,
|
|
alpha_channel=None,
|
|
):
|
|
ref_tensor = next(
|
|
ch
|
|
for ch in [red_channel, green_channel, blue_channel, alpha_channel]
|
|
if ch is not None
|
|
)
|
|
base_shape = ref_tensor.shape[:-1]
|
|
device, dtype = ref_tensor.device, ref_tensor.dtype
|
|
has_alpha = alpha_channel is not None
|
|
num_channels = 4 if has_alpha else 3
|
|
|
|
def _rebuild_channel(input_tensor, target_idx):
|
|
import torch
|
|
|
|
if input_tensor is None:
|
|
return torch.zeros(
|
|
*base_shape, num_channels, device=device, dtype=dtype
|
|
)
|
|
source_idx = min(target_idx, input_tensor.shape[-1] - 1)
|
|
rebuilt = torch.zeros(*base_shape, num_channels, device=device, dtype=dtype)
|
|
rebuilt[..., target_idx] = input_tensor[..., source_idx]
|
|
return rebuilt
|
|
|
|
final_red = _rebuild_channel(red_channel, 0)
|
|
final_green = _rebuild_channel(green_channel, 1)
|
|
final_blue = _rebuild_channel(blue_channel, 2)
|
|
final_alpha = _rebuild_channel(alpha_channel, 3) if has_alpha else None
|
|
merged = final_red + final_green + final_blue
|
|
if has_alpha:
|
|
merged += final_alpha
|
|
return (merged,)
|
|
|
|
|
|
class ImageCountConcatenate:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "ImageCountConcatenate"
|
|
CATEGORY = "MakkiTools"
|
|
DEPRECATED = True
|
|
DESCRIPTION = "Concatenate multiple image batches into a single batch.\n将多个图像批次连接成一个批次。"
|
|
|
|
def ImageCountConcatenate(self, **kwargs):
|
|
images = list(kwargs.values())
|
|
ref_h, ref_w, ref_c = images[0].shape[1], images[0].shape[2], images[0].shape[3]
|
|
processed_images = []
|
|
import torch
|
|
|
|
for img in images:
|
|
current_c = img.shape[3]
|
|
if current_c != ref_c:
|
|
if current_c == 4 and ref_c == 3:
|
|
rgb, alpha = img[..., :3], img[..., 3:]
|
|
white = torch.ones_like(rgb)
|
|
img = rgb * alpha + white * (1 - alpha)
|
|
elif current_c == 3 and ref_c == 4:
|
|
alpha = torch.ones(
|
|
(*img.shape[:-1], 1), dtype=img.dtype, device=img.device
|
|
)
|
|
img = torch.cat([img, alpha], dim=-1)
|
|
elif current_c == 1 and ref_c == 3:
|
|
img = img.expand(-1, -1, -1, 3)
|
|
elif current_c == 3 and ref_c == 1:
|
|
img = (
|
|
0.299 * img[..., 0] + 0.587 * img[..., 1] + 0.114 * img[..., 2]
|
|
).unsqueeze(-1)
|
|
current_h, current_w = img.shape[1], img.shape[2]
|
|
if (current_h, current_w) != (ref_h, ref_w):
|
|
import comfy.utils
|
|
|
|
image = img.movedim(-1, 1)
|
|
new_image = comfy.utils.common_upscale(
|
|
image, ref_w, ref_h, "bicubic", "center"
|
|
)
|
|
img = new_image.movedim(1, -1)
|
|
|
|
processed_images.append(img)
|
|
|
|
combined = torch.cat(processed_images, dim=0)
|
|
current_count = combined.shape[0]
|
|
return (combined[:current_count],)
|
|
|
|
|
|
class ImageWidthStitch:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "ImageWidthStitch"
|
|
CATEGORY = "MakkiTools"
|
|
DEPRECATED = True
|
|
DESCRIPTION = "Stitch images horizontally.\n水平拼接图像。"
|
|
|
|
def ImageWidthStitch(self, **kwargs):
|
|
images = list(kwargs.values())
|
|
first_img = images[0]
|
|
ref_H = first_img.shape[1]
|
|
|
|
resized_images = []
|
|
for img in images:
|
|
W = img.shape[2]
|
|
H = img.shape[1]
|
|
aspect_ratio = W / H
|
|
|
|
new_H = ref_H
|
|
new_W = int(ref_H * aspect_ratio)
|
|
|
|
if H != new_H or W != new_W:
|
|
import comfy.utils
|
|
|
|
image = img.movedim(-1, 1)
|
|
new_image = comfy.utils.common_upscale(
|
|
image, new_W, new_H, "bicubic", "disabled"
|
|
)
|
|
img = new_image.movedim(1, -1)
|
|
resized_images.append(img)
|
|
import torch
|
|
|
|
concatenated = torch.cat(resized_images, dim=2)
|
|
return (concatenated,)
|
|
|
|
|
|
class ImageHeigthStitch:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "ImageHeigthStitch"
|
|
CATEGORY = "MakkiTools"
|
|
DEPRECATED = True
|
|
DESCRIPTION = "Stitch images vertically.\n垂直拼接图像。"
|
|
|
|
def ImageHeigthStitch(self, **kwargs):
|
|
images = list(kwargs.values())
|
|
first_img = images[0]
|
|
ref_W = first_img.shape[2]
|
|
|
|
resized_images = []
|
|
for img in images:
|
|
W = img.shape[2]
|
|
H = img.shape[1]
|
|
aspect_ratio = W / H
|
|
|
|
new_H = int(ref_W / aspect_ratio)
|
|
new_W = ref_W
|
|
|
|
if H != new_H or W != new_W:
|
|
import comfy.utils
|
|
|
|
image = img.movedim(-1, 1)
|
|
new_image = comfy.utils.common_upscale(
|
|
image, new_W, new_H, "bicubic", "disabled"
|
|
)
|
|
img = new_image.movedim(1, -1)
|
|
resized_images.append(img)
|
|
import torch
|
|
|
|
concatenated = torch.cat(resized_images, dim=1)
|
|
return (concatenated,)
|
|
|
|
|
|
class AutoLoop_create_pseudo_loop_video:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"frames": ("IMAGE", {"tooltip": "Input video frames\n输入视频帧"}),
|
|
"transition_duration": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.2,
|
|
"min": 0,
|
|
"max": 0.5,
|
|
"step": 0.01,
|
|
"tooltip": "Transition duration as a proportion of total frames\n过渡持续时间占总帧数的比例",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "AutoLoop_create_pseudo_loop_video"
|
|
CATEGORY = "MakkiTools"
|
|
DEPRECATED = True
|
|
DESCRIPTION = "Create a pseudo loop video.\n创建伪循环视频。"
|
|
|
|
def ease_in_out(self, t):
|
|
return t * t * (3 - 2 * t)
|
|
|
|
def AutoLoop_create_pseudo_loop_video(self, frames, transition_duration):
|
|
B, H, W, C = frames.shape
|
|
assert B >= 4, "required 4+ frames."
|
|
|
|
L = max(1, min(B // 2, int(B * transition_duration)))
|
|
|
|
best_score = -float("inf")
|
|
best_idx = B // 2
|
|
|
|
search_start = max(L, B // 4)
|
|
search_end = min(B - L, 3 * B // 4)
|
|
|
|
for idx in range(search_start, search_end):
|
|
front_end = frames[idx - 1 : idx + 1].flatten(1)
|
|
back_start = frames[idx : idx + 2].flatten(1)
|
|
|
|
import torch.nn.functional as F
|
|
|
|
score = F.cosine_similarity(front_end, back_start).mean()
|
|
|
|
if score > best_score:
|
|
best_score = score
|
|
best_idx = idx
|
|
|
|
split_index = best_idx
|
|
|
|
front = frames[:split_index]
|
|
back = frames[split_index:]
|
|
|
|
back_no_tail = back[:-L] if L < len(back) else back[0:0]
|
|
front_no_head = front[L:] if L < len(front) else front[0:0]
|
|
|
|
import torch
|
|
|
|
alphas = self.ease_in_out(torch.linspace(0, 1, L))
|
|
alphas = alphas.view(L, 1, 1, 1)
|
|
|
|
transition = (1 - alphas) * back[-L:] + alphas * front[:L]
|
|
|
|
loop = torch.cat([back_no_tail, transition, front_no_head], dim=0)
|
|
|
|
return (loop,)
|
|
|
|
|
|
class Environment_INFO:
|
|
from .environment_info import AlwaysEqualProxy
|
|
|
|
any_type = AlwaysEqualProxy("*")
|
|
|
|
def __init__(self):
|
|
from .environment_info import format_environment_info, get_environment_info
|
|
|
|
self.format_environment_info = format_environment_info
|
|
self.get_environment_info = get_environment_info
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"SYSTEM_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display system information\n显示系统信息",
|
|
},
|
|
),
|
|
"HARDWARE_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display hardware information\n显示硬件信息",
|
|
},
|
|
),
|
|
"GPU_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display GPU information\n显示GPU信息",
|
|
},
|
|
),
|
|
"DEEP_LEARNING_FRAMEWORKS_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display deep learning framework information\n显示深度学习框架信息",
|
|
},
|
|
),
|
|
"ALL_INSTALLED_PACKAGES_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display all installed packages information\n显示所有已安装包的信息",
|
|
},
|
|
),
|
|
"CUSTOM_NODES_FOLDERS_INFO": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": True,
|
|
"tooltip": "Display custom nodes folders information\n显示自定义节点文件夹信息",
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"anything": (
|
|
s.any_type,
|
|
{
|
|
"tooltip": "Any input (not used, just for chaining)\n任意输入(不使用,仅用于链式连接)"
|
|
},
|
|
)
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", any_type)
|
|
RETURN_NAMES = ("INFO", "anything")
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "Environment_INFO"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Display system environment information.\n显示系统环境信息。"
|
|
|
|
def Environment_INFO(
|
|
self,
|
|
SYSTEM_INFO,
|
|
HARDWARE_INFO,
|
|
GPU_INFO,
|
|
DEEP_LEARNING_FRAMEWORKS_INFO,
|
|
ALL_INSTALLED_PACKAGES_INFO,
|
|
CUSTOM_NODES_FOLDERS_INFO,
|
|
anything=None,
|
|
):
|
|
env_info = self.get_environment_info()
|
|
full_report = self.format_environment_info(
|
|
env_info,
|
|
SYSTEM_INFO,
|
|
HARDWARE_INFO,
|
|
GPU_INFO,
|
|
DEEP_LEARNING_FRAMEWORKS_INFO,
|
|
ALL_INSTALLED_PACKAGES_INFO,
|
|
CUSTOM_NODES_FOLDERS_INFO,
|
|
)
|
|
|
|
return {"ui": {"info": (full_report,)}, "result": (full_report, anything)}
|
|
|
|
|
|
class translators:
|
|
def __init__(self):
|
|
import os
|
|
|
|
os.environ["translators_default_region"] = "EN"
|
|
import translators as ts
|
|
from .translators_map import LANGUAGE_MAPPING
|
|
|
|
self._pre_acceleration_done = False
|
|
self.ts = ts
|
|
self.LANGUAGE_MAPPING = LANGUAGE_MAPPING
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
import os
|
|
|
|
os.environ["translators_default_region"] = "EN"
|
|
import translators as ts
|
|
from .translators_map import Supported_Languages
|
|
|
|
return {
|
|
"required": {
|
|
"query_text": (
|
|
"STRING",
|
|
{"multiline": True, "tooltip": "Text to translate\n要翻译的文本"},
|
|
),
|
|
"translator": (
|
|
list(ts.translators_pool),
|
|
{
|
|
"default": list(ts.translators_pool)[0],
|
|
"tooltip": "Translation service to use\n要使用的翻译服务",
|
|
},
|
|
),
|
|
"from_language": (
|
|
["auto"] + Supported_Languages,
|
|
{
|
|
"default": "auto",
|
|
"tooltip": "Source language (auto for automatic detection)\n源语言(auto为自动检测)",
|
|
},
|
|
),
|
|
"to_language": (
|
|
Supported_Languages,
|
|
{
|
|
"default": Supported_Languages[0],
|
|
"tooltip": "Target language\n目标语言",
|
|
},
|
|
),
|
|
"if_use_preacceleration": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": "Enable pre-acceleration for faster translation\n启用预加速以提高翻译速度",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "translators"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Translate text using various online translation services.\n使用各种在线翻译服务翻译文本。"
|
|
|
|
def normalize_language_code(self, translator, Language):
|
|
return self.LANGUAGE_MAPPING.get(translator, {}).get(Language, Language)
|
|
|
|
def translators(
|
|
self,
|
|
query_text,
|
|
translator,
|
|
from_language,
|
|
to_language,
|
|
if_use_preacceleration,
|
|
):
|
|
pattern = r"\(([^()]+)\)[^()]*$"
|
|
import re
|
|
|
|
if from_language != "auto":
|
|
match = re.search(pattern, from_language)
|
|
from_language = match.group(1)
|
|
from_language = self.normalize_language_code(translator, from_language)
|
|
|
|
match = re.search(pattern, to_language)
|
|
to_language = match.group(1)
|
|
to_language = self.normalize_language_code(translator, to_language)
|
|
|
|
if if_use_preacceleration and not self._pre_acceleration_done:
|
|
_ = self.ts.preaccelerate_and_speedtest()
|
|
self._pre_acceleration_done = True
|
|
|
|
try:
|
|
import threading
|
|
import queue
|
|
|
|
result_queue = queue.Queue()
|
|
|
|
def translate_worker():
|
|
try:
|
|
output = self.ts.translate_text(
|
|
query_text,
|
|
translator=translator,
|
|
from_language=from_language,
|
|
to_language=to_language,
|
|
if_use_preacceleration=if_use_preacceleration,
|
|
if_ignore_limit_of_length=True,
|
|
)
|
|
result_queue.put(("success", output))
|
|
except Exception as e:
|
|
result_queue.put(("error", e))
|
|
|
|
thread = threading.Thread(target=translate_worker)
|
|
thread.start()
|
|
|
|
try:
|
|
result_type, result = result_queue.get(timeout=10)
|
|
if result_type == "error":
|
|
raise result
|
|
output = result
|
|
except queue.Empty:
|
|
print(
|
|
"[Translators Node] Translation timed out, returning original text"
|
|
)
|
|
return (query_text,)
|
|
|
|
except Exception as e:
|
|
error_str = str(e)
|
|
print(f"[Translators Node] Chunk translation failed: {error_str}")
|
|
|
|
if "Unsupported" in error_str or "same" in error_str:
|
|
print(
|
|
f"[Translators Node] Fallback to original text for chunk.Please check the settings of 'from_language' and 'to_language'"
|
|
)
|
|
|
|
return (query_text,)
|
|
|
|
return (output,)
|
|
|
|
|
|
class translator_m2m100:
|
|
def __init__(self):
|
|
from .m2m100 import M2M100Translator
|
|
|
|
self.M2M100Translator = M2M100Translator
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
from .m2m100 import m2m100map
|
|
|
|
return {
|
|
"required": {
|
|
"query_text": (
|
|
"STRING",
|
|
{"multiline": True, "tooltip": "Text to translate\n要翻译的文本"},
|
|
),
|
|
"model": (
|
|
[
|
|
"facebook/m2m100_418M",
|
|
"facebook/m2m100_1.2B",
|
|
"facebook/m2m100-12B-avg-5-ckpt",
|
|
"facebook/m2m100-12B-avg-10-ckpt",
|
|
"facebook/m2m100-12B-last-ckpt",
|
|
],
|
|
{
|
|
"default": "facebook/m2m100_418M",
|
|
"tooltip": "M2M100 model to use\n要使用的M2M100模型",
|
|
},
|
|
),
|
|
"from_language": (
|
|
["auto"] + m2m100map,
|
|
{
|
|
"default": "auto",
|
|
"tooltip": "Source language (auto for automatic detection)\n源语言(auto为自动检测)",
|
|
},
|
|
),
|
|
"to_language": (
|
|
m2m100map,
|
|
{"default": "English (en)", "tooltip": "Target language\n目标语言"},
|
|
),
|
|
"quantization": (
|
|
["none", "4bit", "8bit"],
|
|
{
|
|
"default": "8bit",
|
|
"tooltip": "Model quantization level\n模型量化级别",
|
|
},
|
|
),
|
|
"attention": (
|
|
["flash_attention_2", "sdpa", "eager"],
|
|
{
|
|
"default": "sdpa",
|
|
"tooltip": "Attention implementation to use\n要使用的注意力实现",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "translator_m2m100"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Translate text using the M2M100 model.\n使用M2M100模型翻译文本。"
|
|
|
|
def translator_m2m100(
|
|
self,
|
|
query_text,
|
|
model,
|
|
from_language,
|
|
to_language,
|
|
quantization,
|
|
attention,
|
|
):
|
|
pattern = r"\(([^()]+)\)[^()]*$"
|
|
import re
|
|
|
|
if from_language != "auto":
|
|
match = re.search(pattern, from_language)
|
|
from_language = match.group(1)
|
|
|
|
match = re.search(pattern, to_language)
|
|
to_language = match.group(1)
|
|
|
|
translator = self.M2M100Translator(
|
|
model_repo=model, quantization=quantization, attention=attention
|
|
)
|
|
output = translator.translate_preserve_format(
|
|
query_text, from_language, to_language
|
|
)
|
|
|
|
return (output,)
|
|
|
|
|
|
class random_any:
|
|
from .environment_info import AlwaysEqualProxy
|
|
|
|
any_type = AlwaysEqualProxy("*")
|
|
|
|
def __init__(self):
|
|
import random
|
|
|
|
self.random = random
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"optional": {
|
|
"anything": (
|
|
s.any_type,
|
|
{
|
|
"tooltip": "Any input (passed through unchanged)\n任意输入(原样传递)"
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type, "INT", "FLOAT")
|
|
RETURN_NAMES = ("any", "int", "float")
|
|
FUNCTION = "random_any"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Generate random integer and float values.\n生成随机整数和浮点数值。"
|
|
|
|
def random_any(self, anything=None):
|
|
return (
|
|
anything,
|
|
self.random.Random().randint(0, 0xFFFFFFFFFFFFFFFF),
|
|
self.random.Random().random(),
|
|
)
|
|
|
|
|
|
class AnyImageStitch:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
|
|
crop_methods = ["disabled", "center"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"dimension": (
|
|
["horizontal", "vertical"],
|
|
{
|
|
"default": "horizontal",
|
|
"tooltip": "Direction to stitch images\n拼接图像的方向",
|
|
},
|
|
),
|
|
"reference_type": (
|
|
["first image", "custom"],
|
|
{
|
|
"default": "first image",
|
|
"tooltip": "Reference size type\n参考尺寸类型",
|
|
},
|
|
),
|
|
"reference_value": (
|
|
"INT",
|
|
{
|
|
"default": 512,
|
|
"min": 1,
|
|
"max": 4096,
|
|
"step": 1,
|
|
"tooltip": "Custom reference size\n自定义参考尺寸",
|
|
},
|
|
),
|
|
"upscale_method": (
|
|
s.upscale_methods,
|
|
{
|
|
"default": "bicubic",
|
|
"tooltip": "Method to upscale images\n图像放大方法",
|
|
},
|
|
),
|
|
"crop": (
|
|
s.crop_methods,
|
|
{
|
|
"default": "disabled",
|
|
"tooltip": "Crop method for upscaled images\n放大图像的裁剪方法",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "AnyImageStitch"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Stitch any number of images in horizontal or vertical direction.\n在水平或垂直方向上拼接任意数量的图像。"
|
|
|
|
def AnyImageStitch(
|
|
self, dimension, reference_type, reference_value, upscale_method, crop, **kwargs
|
|
):
|
|
import torch
|
|
|
|
# 获取图像列表并过滤非图像输入
|
|
images = [
|
|
img
|
|
for img in kwargs.values()
|
|
if isinstance(img, torch.Tensor) and img.dim() == 4
|
|
]
|
|
|
|
# 确定参考尺寸
|
|
if reference_type == "first image":
|
|
ref_dim = (
|
|
images[0].shape[1] if dimension == "horizontal" else images[0].shape[2]
|
|
)
|
|
else:
|
|
ref_dim = reference_value
|
|
|
|
resized_images = []
|
|
for img in images:
|
|
batch, H, W, channels = img.shape
|
|
|
|
# 计算目标尺寸
|
|
if dimension == "horizontal":
|
|
target_H = ref_dim
|
|
aspect_ratio = W / H
|
|
target_W = int(target_H * aspect_ratio)
|
|
else: # vertical
|
|
target_W = ref_dim
|
|
aspect_ratio = H / W
|
|
target_H = int(target_W * aspect_ratio)
|
|
|
|
# 调整图像尺寸(如果需要)
|
|
if H != target_H or W != target_W:
|
|
import comfy.utils
|
|
|
|
image = img.movedim(-1, 1) # [batch, channels, H, W]
|
|
new_image = comfy.utils.common_upscale(
|
|
image, target_W, target_H, upscale_method, crop
|
|
)
|
|
img = new_image.movedim(1, -1) # 恢复原始维度
|
|
|
|
resized_images.append(img)
|
|
|
|
# 确定拼接维度
|
|
dim = 2 if dimension == "horizontal" else 1
|
|
|
|
# 拼接所有图像
|
|
concatenated = torch.cat(resized_images, dim=dim)
|
|
return (concatenated,)
|
|
|
|
|
|
class AnyImagetoConditioning_flux_kontext:
|
|
PREFERED_KONTEXT_RESOLUTIONS = [
|
|
(672, 1568),
|
|
(688, 1504),
|
|
(720, 1456),
|
|
(752, 1392),
|
|
(800, 1328),
|
|
(832, 1248),
|
|
(880, 1184),
|
|
(944, 1104),
|
|
(1024, 1024),
|
|
(1104, 944),
|
|
(1184, 880),
|
|
(1248, 832),
|
|
(1328, 800),
|
|
(1392, 752),
|
|
(1456, 720),
|
|
(1504, 688),
|
|
(1568, 672),
|
|
]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"conditioning": (
|
|
"CONDITIONING",
|
|
{"tooltip": "Input conditioning\n输入条件"},
|
|
),
|
|
"vae": ("VAE", {"tooltip": "VAE model\nVAE模型"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("CONDITIONING",)
|
|
FUNCTION = "AnyImagetoConditioning_flux_kontext"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Convert any number of images to conditioning for flux kontext.\n将任意数量的图像转换为flux kontext的条件。"
|
|
|
|
def AnyImagetoConditioning_flux_kontext(self, conditioning, vae, **kwargs):
|
|
for img in kwargs.values():
|
|
pixels = self.scale(img)
|
|
t = vae.encode(pixels[:, :, :, :3])
|
|
latent = {"samples": t}
|
|
import node_helpers
|
|
|
|
conditioning = node_helpers.conditioning_set_values(
|
|
conditioning, {"reference_latents": [latent["samples"]]}, append=True
|
|
)
|
|
|
|
return (conditioning,)
|
|
|
|
def scale(self, image):
|
|
width = image.shape[2]
|
|
height = image.shape[1]
|
|
aspect_ratio = width / height
|
|
_, width, height = min(
|
|
(abs(aspect_ratio - w / h), w, h)
|
|
for w, h in self.PREFERED_KONTEXT_RESOLUTIONS
|
|
)
|
|
import comfy.utils
|
|
|
|
image = comfy.utils.common_upscale(
|
|
image.movedim(-1, 1), width, height, "lanczos", "center"
|
|
).movedim(1, -1)
|
|
return image
|
|
|
|
|
|
class show_type:
|
|
from .environment_info import AlwaysEqualProxy
|
|
|
|
any_type = AlwaysEqualProxy("*")
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"optional": {
|
|
"anything": (
|
|
s.any_type,
|
|
{
|
|
"tooltip": "Input of any type to show its type\n任意类型的输入以显示其类型"
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "show_type"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Show the type of input data.\n显示输入数据的类型。"
|
|
|
|
def show_type(self, anything):
|
|
type_name = type(anything).__name__.lower()
|
|
return {"ui": {"info": (type_name,)}, "result": (type_name,)}
|
|
|
|
|
|
class timer:
|
|
from .environment_info import AlwaysEqualProxy
|
|
|
|
any_type = AlwaysEqualProxy("*")
|
|
|
|
def __init__(self):
|
|
import time
|
|
|
|
self.time = time
|
|
|
|
class Timer:
|
|
def __init__(self):
|
|
self.start_time = None
|
|
self.elapsed = 0
|
|
self.elapsed_str = "0ms"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"mode": (["start", "stop"], {"tooltip": "Timer mode\n计时器模式"}),
|
|
"format": (
|
|
["ms", "s", "m", "auto"],
|
|
{"default": "auto", "tooltip": "Time display format\n时间显示格式"},
|
|
),
|
|
},
|
|
"optional": {
|
|
"any": (
|
|
s.any_type,
|
|
{"tooltip": "Any input to pass through\n任意输入通过"},
|
|
),
|
|
"timer": ("TIMER_MAKKI", {"tooltip": "Timer object\n计时器对象"}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type, "TIMER_MAKKI", "INT", "STRING")
|
|
RETURN_NAMES = ("any", "timer", "time_ms", "time_str")
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "timer"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Measure execution time between nodes.\n测量节点间的执行时间。"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, **kwargs):
|
|
return float("nan")
|
|
|
|
def format_time(self, ms, format_type):
|
|
if format_type == "ms" or (format_type == "auto" and ms < 1000):
|
|
return f"{ms}ms"
|
|
|
|
seconds = ms / 1000.0
|
|
if format_type == "s" or (format_type == "auto" and seconds < 60):
|
|
return f"{seconds:.3f}s"
|
|
|
|
minutes = int(seconds // 60)
|
|
remaining_sec = seconds % 60
|
|
return f"{minutes}m {remaining_sec:.1f}s"
|
|
|
|
def timer(self, mode, format, any=None, timer=None):
|
|
if mode == "start":
|
|
if timer is None or timer.start_time is not None:
|
|
timer = self.Timer()
|
|
timer.start_time = self.time.time()
|
|
timer.elapsed = 0
|
|
timer.elapsed_str = "0ms"
|
|
return (any, timer, 0, "0ms")
|
|
|
|
elif mode == "stop":
|
|
if timer is None:
|
|
raise ValueError("Stop mode requires a valid timer input")
|
|
if timer.start_time is None:
|
|
return (any, timer, timer.elapsed)
|
|
|
|
end_time = self.time.time()
|
|
elapsed_ms = int((end_time - timer.start_time) * 1000)
|
|
timer.elapsed = elapsed_ms
|
|
timer.elapsed_str = self.format_time(elapsed_ms, format)
|
|
timer.start_time = None
|
|
return {
|
|
"ui": {"info": (timer.elapsed_str,)},
|
|
"result": (any, timer, timer.elapsed, timer.elapsed_str),
|
|
}
|
|
|
|
|
|
class Image_Resize:
|
|
upscale_methods = [
|
|
"auto",
|
|
"nearest-exact",
|
|
"bilinear",
|
|
"area",
|
|
"bicubic",
|
|
"lanczos",
|
|
]
|
|
crop_methods = ["disabled", "center"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"upscale_method": (
|
|
s.upscale_methods,
|
|
{
|
|
"default": "auto",
|
|
"tooltip": "Method to upscale the image\n图像放大方法",
|
|
},
|
|
),
|
|
"crop_method": (
|
|
s.crop_methods,
|
|
{
|
|
"default": "center",
|
|
"tooltip": "Method to crop the image\n图像裁剪方法",
|
|
},
|
|
),
|
|
"size": (
|
|
"INT",
|
|
{
|
|
"default": 1024,
|
|
"min": 1,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"tooltip": "Target size (based on selected dimension)\n目标尺寸(基于选定的维度)",
|
|
},
|
|
),
|
|
"GCD": (
|
|
"INT",
|
|
{
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 512,
|
|
"step": 1,
|
|
"tooltip": "Greatest Common Divisor constraint\n最大公约数约束",
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"image": ("IMAGE", {"tooltip": "Input image\n输入图像"}),
|
|
"width": (
|
|
"INT",
|
|
{
|
|
"min": 1,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"defaultInput": True,
|
|
"tooltip": "Target width\n目标宽度",
|
|
},
|
|
),
|
|
"height": (
|
|
"INT",
|
|
{
|
|
"min": 1,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"defaultInput": True,
|
|
"tooltip": "Target height\n目标高度",
|
|
},
|
|
),
|
|
"short_side": (
|
|
"INT",
|
|
{
|
|
"min": 1,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"defaultInput": True,
|
|
"tooltip": "Short side length\n短边长度",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
|
RETURN_NAMES = ("image", "width", "height")
|
|
FUNCTION = "Image_Resize"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Resize images with various methods and constraints.\n使用多种方法和约束调整图像大小。"
|
|
|
|
def Image_Resize(
|
|
self,
|
|
upscale_method,
|
|
crop_method,
|
|
size,
|
|
GCD,
|
|
image=None,
|
|
width=None,
|
|
height=None,
|
|
short_side=None,
|
|
):
|
|
if image is not None:
|
|
original_height, original_width = image.shape[1:3]
|
|
else:
|
|
original_width = original_height = size
|
|
|
|
original_pixels = original_width * original_height
|
|
target_pixels = size**2
|
|
|
|
aspect_ratio = original_width / original_height
|
|
|
|
if short_side is not None and width is None and height is None:
|
|
if original_width <= original_height:
|
|
new_width = short_side
|
|
new_height = short_side / aspect_ratio
|
|
new_height = max(GCD, round(new_height / GCD) * GCD)
|
|
else:
|
|
new_height = short_side
|
|
new_width = short_side * aspect_ratio
|
|
new_width = max(GCD, round(new_width / GCD) * GCD)
|
|
new_width = int(new_width)
|
|
new_height = int(new_height)
|
|
|
|
elif width is not None and height is not None:
|
|
new_width, new_height = width, height
|
|
elif width is not None:
|
|
new_width = width
|
|
new_height = int(new_width / aspect_ratio)
|
|
elif height is not None:
|
|
new_height = height
|
|
new_width = int(new_height * aspect_ratio)
|
|
else:
|
|
new_height = int((target_pixels / aspect_ratio) ** 0.5)
|
|
new_width = int(new_height * aspect_ratio)
|
|
|
|
if short_side is None or width is not None or height is not None:
|
|
new_width = max(GCD, round(new_width / GCD) * GCD)
|
|
new_height = max(GCD, round(new_height / GCD) * GCD)
|
|
|
|
if image is not None:
|
|
import comfy.utils
|
|
|
|
if upscale_method == "auto":
|
|
if original_pixels > target_pixels:
|
|
upscale_method = "area"
|
|
elif original_pixels < target_pixels:
|
|
upscale_method = "lanczos"
|
|
else:
|
|
upscale_method = "nearest-exact"
|
|
|
|
image = image.movedim(-1, 1)
|
|
new_image = comfy.utils.common_upscale(
|
|
image, new_width, new_height, upscale_method, crop_method
|
|
)
|
|
new_image = new_image.movedim(1, -1)
|
|
else:
|
|
import torch
|
|
|
|
new_image = torch.zeros((1, new_height, new_width, 3), dtype=torch.float32)
|
|
|
|
return (new_image, new_width, new_height)
|
|
|
|
|
|
class Prism_Mirage:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"exterior_image": ("IMAGE", {"tooltip": "Exterior image\n外部图像"}),
|
|
"interior_image": ("IMAGE", {"tooltip": "Interior image\n内部图像"}),
|
|
"exterior_level": (
|
|
"INT",
|
|
{
|
|
"default": 42,
|
|
"min": 0,
|
|
"max": 255,
|
|
"step": 1,
|
|
"tooltip": "Exterior transparency level\n外部透明度级别",
|
|
},
|
|
),
|
|
"interior_level": (
|
|
"INT",
|
|
{
|
|
"default": 24,
|
|
"min": 0,
|
|
"max": 255,
|
|
"step": 1,
|
|
"tooltip": "Interior transparency level\n内部透明度级别",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "Prism_Mirage"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Apply a prism mirage effect by combining two images.\n通过合并两张图像应用棱镜幻影效果。"
|
|
|
|
def Prism_Mirage(
|
|
self, exterior_image, interior_image, exterior_level, interior_level
|
|
):
|
|
if exterior_level < interior_level:
|
|
interior_level == exterior_level
|
|
from .Prism_Mirage import process_images
|
|
|
|
image = process_images(
|
|
exterior_image, interior_image, exterior_level, interior_level
|
|
)
|
|
|
|
return (image,)
|
|
|
|
|
|
class int_calculate_statistics:
|
|
from .environment_info import AlwaysEqualProxy
|
|
|
|
any_type = AlwaysEqualProxy("*")
|
|
|
|
def __init__(self):
|
|
import math
|
|
from collections import Counter
|
|
|
|
self.math = math
|
|
self.Counter = Counter
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"stat_mode": (
|
|
[
|
|
"count",
|
|
"sum",
|
|
"mean",
|
|
"min",
|
|
"max",
|
|
"range",
|
|
"median",
|
|
"quartiles",
|
|
"iqr",
|
|
"mode",
|
|
"variance",
|
|
"std_dev",
|
|
"mad",
|
|
"skewness",
|
|
"kurtosis",
|
|
"frequency",
|
|
"sorted_list",
|
|
"unique_values",
|
|
"cumulative_sum",
|
|
"proportions",
|
|
"z_scores",
|
|
],
|
|
{
|
|
"default": "count",
|
|
"tooltip": "Statistical measure to calculate\n要计算的统计量",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
FUNCTION = "int_calculate_statistics"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Calculate various statistical measures from integer inputs.\n从整数输入计算各种统计量。"
|
|
|
|
def int_calculate_statistics(self, stat_mode, **kwargs):
|
|
return (self.calculate_statistics(stat_mode, **kwargs),)
|
|
|
|
def calculate_statistics(self, stat_mode, **kwargs):
|
|
"""
|
|
计算并返回指定的整数统计结果
|
|
|
|
参数:
|
|
stat_mode: 要计算的统计量名称
|
|
**kwargs: 任意数量的整数关键字参数
|
|
|
|
返回:
|
|
指定的统计量结果
|
|
"""
|
|
# 提取所有整数值
|
|
ints = list(kwargs.values())
|
|
n = len(ints)
|
|
|
|
# 空输入处理
|
|
if n == 0:
|
|
if stat_mode == "count":
|
|
return 0
|
|
return None
|
|
|
|
# 计算基本统计量
|
|
def calculate_basic():
|
|
total = sum(ints)
|
|
mean = total / n
|
|
sorted_ints = sorted(ints)
|
|
min_val = sorted_ints[0]
|
|
max_val = sorted_ints[-1]
|
|
|
|
return {
|
|
"count": n,
|
|
"sum": total,
|
|
"mean": mean,
|
|
"min": min_val,
|
|
"max": max_val,
|
|
"sorted_list": sorted_ints,
|
|
}
|
|
|
|
# 计算中位数和四分位数
|
|
def calculate_quartiles(sorted_ints):
|
|
def get_quartile_index(p: float) -> float:
|
|
pos = (n - 1) * p
|
|
i = int(pos)
|
|
frac = pos - i
|
|
return (
|
|
sorted_ints[i] + frac * (sorted_ints[i + 1] - sorted_ints[i])
|
|
if i < n - 1
|
|
else sorted_ints[i]
|
|
)
|
|
|
|
q1 = get_quartile_index(0.25) if n > 1 else sorted_ints[0]
|
|
median = get_quartile_index(0.5) if n > 1 else sorted_ints[0]
|
|
q3 = get_quartile_index(0.75) if n > 1 else sorted_ints[0]
|
|
return q1, median, q3
|
|
|
|
# 计算离散度指标
|
|
def calculate_dispersion(mean: float):
|
|
variance = sum((x - mean) ** 2 for x in ints) / n
|
|
std_dev = self.math.sqrt(variance)
|
|
mad = sum(abs(x - mean) for x in ints) / n
|
|
return {"variance": variance, "std_dev": std_dev, "mad": mad}
|
|
|
|
# 计算分布特征
|
|
def calculate_distribution(mean: float, std_dev: float):
|
|
if std_dev == 0:
|
|
return {"skewness": 0, "kurtosis": 0}
|
|
|
|
skewness = (sum((x - mean) ** 3 for x in ints) / n) / (std_dev**3)
|
|
kurtosis = (sum((x - mean) ** 4 for x in ints) / n) / (std_dev**4)
|
|
return {"skewness": skewness, "kurtosis": kurtosis - 3} # 返回超额峰度
|
|
|
|
# 根据请求的统计量计算并返回结果
|
|
basic = calculate_basic()
|
|
sorted_ints = basic["sorted_list"]
|
|
|
|
if stat_mode == "count":
|
|
return basic["count"]
|
|
|
|
if stat_mode == "sum":
|
|
return basic["sum"]
|
|
|
|
if stat_mode == "mean":
|
|
return basic["mean"]
|
|
|
|
if stat_mode == "min":
|
|
return basic["min"]
|
|
|
|
if stat_mode == "max":
|
|
return basic["max"]
|
|
|
|
if stat_mode == "range":
|
|
return basic["max"] - basic["min"]
|
|
|
|
if stat_mode == "median":
|
|
return calculate_quartiles(sorted_ints)[1]
|
|
|
|
if stat_mode == "quartiles":
|
|
return calculate_quartiles(sorted_ints)
|
|
|
|
if stat_mode == "iqr":
|
|
q1, _, q3 = calculate_quartiles(sorted_ints)
|
|
return q3 - q1
|
|
|
|
if stat_mode == "mode":
|
|
count_dict = self.Counter(ints)
|
|
max_count = max(count_dict.values())
|
|
return [k for k, v in count_dict.items() if v == max_count]
|
|
|
|
if stat_mode == "variance":
|
|
return calculate_dispersion(basic["mean"])["variance"]
|
|
|
|
if stat_mode == "std_dev":
|
|
return calculate_dispersion(basic["mean"])["std_dev"]
|
|
|
|
if stat_mode == "mad":
|
|
return calculate_dispersion(basic["mean"])["mad"]
|
|
|
|
if stat_mode == "skewness":
|
|
dispersion = calculate_dispersion(basic["mean"])
|
|
return calculate_distribution(basic["mean"], dispersion["std_dev"])[
|
|
"skewness"
|
|
]
|
|
|
|
if stat_mode == "kurtosis":
|
|
dispersion = calculate_dispersion(basic["mean"])
|
|
return calculate_distribution(basic["mean"], dispersion["std_dev"])[
|
|
"kurtosis"
|
|
]
|
|
|
|
if stat_mode == "frequency":
|
|
return dict(self.Counter(ints))
|
|
|
|
if stat_mode == "sorted_list":
|
|
return sorted_ints
|
|
|
|
if stat_mode == "unique_values":
|
|
return sorted(set(ints))
|
|
|
|
if stat_mode == "cumulative_sum":
|
|
return [sum(ints[: i + 1]) for i in range(n)]
|
|
|
|
if stat_mode == "proportions":
|
|
total = basic["sum"]
|
|
return [x / total for x in ints]
|
|
|
|
if stat_mode == "z_scores":
|
|
dispersion = calculate_dispersion(basic["mean"])
|
|
std_dev = dispersion["std_dev"]
|
|
if std_dev == 0:
|
|
return [0] * n
|
|
return [(x - basic["mean"]) / std_dev for x in ints]
|
|
|
|
raise ValueError(f"未知的统计量名称: {stat_mode}")
|
|
|
|
|
|
class BatchLoraLoader:
|
|
import folder_paths
|
|
import comfy.utils
|
|
import comfy.sd
|
|
|
|
def __init__(self):
|
|
self.loaded_lora = None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
loras = ["None"] + s.folder_paths.get_filename_list("loras")
|
|
inputs = {
|
|
"required": {
|
|
"model": (
|
|
"MODEL",
|
|
{
|
|
"tooltip": "The diffusion model the LoRA will be applied to.\nLoRA将应用到的扩散模型。"
|
|
},
|
|
),
|
|
"clip": (
|
|
"CLIP",
|
|
{
|
|
"tooltip": "The CLIP model the LoRA will be applied to.\nLoRA将应用到的CLIP模型。"
|
|
},
|
|
),
|
|
"loras_count": (
|
|
"INT",
|
|
{"default": 3, "min": 1, "max": 50, "step": 1},
|
|
),
|
|
"loras_num": (
|
|
"INT",
|
|
{"default": -1, "min": -1, "max": 50, "step": 1},
|
|
),
|
|
}
|
|
}
|
|
|
|
for i in range(1, 50):
|
|
inputs["required"][f"lora_name_{i}"] = (
|
|
loras,
|
|
{"tooltip": f"LoRA model {i}\nLoRA模型 {i}"},
|
|
)
|
|
inputs["required"][f"strength_model_{i}"] = (
|
|
"FLOAT",
|
|
{
|
|
"default": 1.0,
|
|
"min": -100.0,
|
|
"max": 100.0,
|
|
"step": 0.01,
|
|
"tooltip": "How strongly to modify the diffusion model. This value can be negative.\n修改扩散模型的强度。此值可以为负。",
|
|
},
|
|
)
|
|
inputs["required"][f"strength_clip_{i}"] = (
|
|
"FLOAT",
|
|
{
|
|
"default": 1.0,
|
|
"min": -100.0,
|
|
"max": 100.0,
|
|
"step": 0.01,
|
|
"tooltip": "How strongly to modify the CLIP model. This value can be negative.\n修改CLIP模型的强度。此值可以为负。",
|
|
},
|
|
)
|
|
|
|
return inputs
|
|
|
|
RETURN_TYPES = ("MODEL", "CLIP")
|
|
OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.")
|
|
FUNCTION = "BatchLoraLoader"
|
|
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Load and apply multiple LoRA models to diffusion and CLIP models.\n加载并将多个LoRA模型应用于扩散和CLIP模型。"
|
|
|
|
def BatchLoraLoader(self, model, clip, loras_count, loras_num, **kwargs):
|
|
if loras_num == 0: # 为0时全部跳过
|
|
return (model, clip)
|
|
|
|
if loras_num > loras_count: # 越界时加载全部LoRA
|
|
loras_num = -1
|
|
|
|
# 提取所有LoRA参数
|
|
selected_lora_name = [
|
|
kwargs[f"lora_name_{i}"] for i in range(1, loras_count + 1)
|
|
]
|
|
selected_strength_model = [
|
|
kwargs[f"strength_model_{i}"] for i in range(1, loras_count + 1)
|
|
]
|
|
selected_strength_clip = [
|
|
kwargs[f"strength_clip_{i}"] for i in range(1, loras_count + 1)
|
|
]
|
|
|
|
# 确定要加载的LoRA索引
|
|
if loras_num == -1: # 加载全部
|
|
lora_indices = range(loras_count)
|
|
else: # 加载指定索引的LoRA
|
|
lora_indices = [loras_num - 1] # 转换为0-based索引
|
|
|
|
current_model, current_clip = model, clip
|
|
|
|
# 遍历所有需要加载的LoRA
|
|
for i in lora_indices:
|
|
lora_name = selected_lora_name[i]
|
|
strength_model = selected_strength_model[i]
|
|
strength_clip = selected_strength_clip[i]
|
|
|
|
# 跳过None
|
|
if lora_name == "None":
|
|
continue
|
|
|
|
# 跳过强度为0的LoRA
|
|
if strength_model == 0 and strength_clip == 0:
|
|
continue
|
|
|
|
# 获取LoRA路径
|
|
lora_path = self.folder_paths.get_full_path_or_raise("loras", lora_name)
|
|
lora = None
|
|
|
|
# 检查缓存
|
|
if self.loaded_lora is not None:
|
|
if self.loaded_lora[0] == lora_path:
|
|
lora = self.loaded_lora[1]
|
|
else:
|
|
self.loaded_lora = None # 缓存不匹配时清除
|
|
|
|
# 需要时加载LoRA
|
|
if lora is None:
|
|
lora = self.comfy.utils.load_torch_file(lora_path, safe_load=True)
|
|
self.loaded_lora = (lora_path, lora) # 更新缓存
|
|
|
|
# 应用LoRA到当前模型
|
|
current_model, current_clip = self.comfy.sd.load_lora_for_models(
|
|
current_model, current_clip, lora, strength_model, strength_clip
|
|
)
|
|
|
|
return (current_model, current_clip)
|
|
|
|
|
|
class UniversalInstaller:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"package_name": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"tooltip": "The name of the package to install, such as 'requests' or 'tipo-kgen==0.2.0'.\n要安装的包名称,例如'requests'或'tipo-kgen==0.2.0'。",
|
|
},
|
|
),
|
|
"backend": (
|
|
["pip", "uv"],
|
|
{
|
|
"default": "pip",
|
|
"tooltip": "Package manager to use\n要使用的包管理器",
|
|
},
|
|
),
|
|
"mirror_source": (
|
|
["default", "tsinghua", "aliyun", "douban", "ustc", "custom"],
|
|
{
|
|
"default": "default",
|
|
"tooltip": "Select the mirror source, use the content in extra_index_url when custom.\n选择镜像源,自定义时使用extra_index_url中的内容。",
|
|
},
|
|
),
|
|
"update_if_no_version": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": "When the package name does not specify a version, should the -U parameter be used to update to the latest version.\n当包名称未指定版本时,是否应使用-U参数更新到最新版本。",
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"extra_index_url": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"tooltip": "Additional index URLs (e.g., private sources), optional. Only used when 'mirror_source' is set to 'custom'.\n附加索引URL(例如私有源),可选。仅在'mirror_source'设置为'custom'时使用。",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "UniversalInstaller"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Install Python packages dynamically.\n动态安装Python包。"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, **kwargs):
|
|
return float("nan")
|
|
|
|
def UniversalInstaller(
|
|
self,
|
|
package_name,
|
|
backend,
|
|
mirror_source,
|
|
update_if_no_version,
|
|
extra_index_url="",
|
|
):
|
|
"""
|
|
安装指定的Python库
|
|
"""
|
|
import subprocess
|
|
import sys
|
|
|
|
if not package_name.strip():
|
|
return (
|
|
"❌ 错误:请输入要安装的库名称。\n❌ Error: Please enter the name of the package to install.",
|
|
)
|
|
|
|
install_cmd = []
|
|
|
|
if backend == "uv":
|
|
install_cmd = [sys.executable, "-m", "uv", "pip", "install"]
|
|
else: # pip
|
|
install_cmd = [sys.executable, "-m", "pip", "install"]
|
|
|
|
mirror_urls = {
|
|
"tsinghua": "https://pypi.tuna.tsinghua.edu.cn/simple",
|
|
"aliyun": "https://mirrors.aliyun.com/pypi/simple/",
|
|
"douban": "https://pypi.douban.com/simple/",
|
|
"ustc": "https://pypi.mirrors.ustc.edu.cn/simple/",
|
|
}
|
|
|
|
if mirror_source != "default":
|
|
if mirror_source == "custom" and extra_index_url:
|
|
install_cmd.extend(["--extra-index-url", extra_index_url.strip()])
|
|
elif mirror_source in mirror_urls:
|
|
install_cmd.extend(["-i", mirror_urls[mirror_source]])
|
|
|
|
if (
|
|
update_if_no_version
|
|
and "==" not in package_name
|
|
and ">" not in package_name
|
|
and "<" not in package_name
|
|
):
|
|
install_cmd.append("-U")
|
|
|
|
install_cmd.append(package_name.strip())
|
|
|
|
try:
|
|
print(
|
|
f"开始安装库: \nStart installing package: \n{package_name}\n使用后端: \nUsing backend: \n{backend}\n"
|
|
)
|
|
print(f"{' '.join(install_cmd)}")
|
|
|
|
process = subprocess.Popen(
|
|
install_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
|
|
output_log = []
|
|
|
|
for line in iter(process.stdout.readline, ""):
|
|
print(line, end="")
|
|
output_log.append(line)
|
|
process.wait()
|
|
|
|
if process.returncode == 0:
|
|
status_message = f"✅ 成功安装Successfully installed: {package_name}\n\n请完全重启 ComfyUI 以使更改生效。Please fully restart ComfyUI to apply the changes.\n\n--- 安装日志Installation Log ---\n{''.join(output_log)}"
|
|
else:
|
|
full_log = "".join(output_log)
|
|
status_message = f"❌ 安装失败 (Installation failed)\n返回码 (Return): {process.returncode}:\n{full_log}"
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
status_message = f"❌ 安装过程出错 (CalledProcessError):\n{str(e)}"
|
|
except Exception as e:
|
|
status_message = f"❌ 发生意外错误 (Exception):\n{str(e)}"
|
|
|
|
return {"ui": {"info": (status_message,)}, "result": (status_message,)}
|
|
|
|
|
|
class get_folder_info:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"folder_path": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"tooltip": "要扫描的文件夹路径,例如 'C:/Users/MyUser/Documents' 或 '/home/user/documents'\nThe folder path to scan, e.g. 'C:/Users/MyUser/Documents' or '/home/user/documents'",
|
|
},
|
|
),
|
|
"file_index": (
|
|
"INT",
|
|
{
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 1000,
|
|
"step": 1,
|
|
"tooltip": "要获取的第几个文件名(按名称排序),从1开始计数\nThe index of the file to get (sorted by name), counting from 1",
|
|
},
|
|
),
|
|
"output_full_path": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": "如果为True,输出完整路径;否则只输出文件名\nIf True, output full path; otherwise output only filename",
|
|
},
|
|
),
|
|
"include_subfolders": (
|
|
"BOOLEAN",
|
|
{
|
|
"default": False,
|
|
"tooltip": "是否包含子文件夹中的文件\nWhether to include files in subfolders",
|
|
},
|
|
),
|
|
"file_filter": (
|
|
"STRING",
|
|
{
|
|
"default": "*",
|
|
"tooltip": "文件筛选器,例如 '*.txt' 或 'image*.png'\nFile filter, e.g. '*.txt' or 'image*.png'",
|
|
},
|
|
),
|
|
"file_types": (
|
|
["all", "image", "text", "audio", "video"],
|
|
{
|
|
"default": "all",
|
|
"tooltip": "文件类型筛选\nFile type filter",
|
|
},
|
|
),
|
|
"sort_by": (
|
|
["name", "date_modified", "size"],
|
|
{
|
|
"default": "name",
|
|
"tooltip": "排序方式\nSort method",
|
|
},
|
|
),
|
|
"sort_order": (
|
|
["ascending", "descending"],
|
|
{
|
|
"default": "ascending",
|
|
"tooltip": "排序顺序\nSort order",
|
|
},
|
|
),
|
|
"limit": (
|
|
"INT",
|
|
{
|
|
"default": 0,
|
|
"min": 0,
|
|
"tooltip": "限制返回的文件数量,0表示无限制\nLimit the number of files returned, 0 means no limit",
|
|
},
|
|
),
|
|
"regex_filter": (
|
|
"STRING",
|
|
{
|
|
"default": "",
|
|
"tooltip": "正则表达式筛选,例如 '^image.*\\.png$'\nRegular expression filter, e.g. '^image.*\\.png$'",
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "LIST", "STRING", "STRING", "INT", "BOOLEAN", "STRING")
|
|
RETURN_NAMES = (
|
|
"output_string",
|
|
"output_list",
|
|
"nth_item",
|
|
"tree_structure",
|
|
"file_count",
|
|
"has_files",
|
|
"error_message",
|
|
)
|
|
FUNCTION = "get_folder_info"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Get information about files in a folder.\n获取文件夹中文件的信息。"
|
|
|
|
def get_folder_info(
|
|
self,
|
|
folder_path,
|
|
file_index,
|
|
output_full_path=False,
|
|
include_subfolders=False,
|
|
file_filter="*",
|
|
file_types="all",
|
|
sort_by="name",
|
|
sort_order="ascending",
|
|
limit=0,
|
|
regex_filter="",
|
|
):
|
|
import os
|
|
|
|
error_msg = ""
|
|
files = []
|
|
|
|
try:
|
|
# 检查文件夹是否存在
|
|
if not os.path.exists(folder_path):
|
|
error_msg = f"The folder does not exist: {folder_path}"
|
|
return self.return_error(error_msg)
|
|
|
|
if not os.path.isdir(folder_path):
|
|
error_msg = f"The path is not a folder: {folder_path}"
|
|
return self.return_error(error_msg)
|
|
|
|
# 收集文件
|
|
if include_subfolders:
|
|
all_items = []
|
|
for root, dirs, walk_files in os.walk(folder_path):
|
|
for file in walk_files:
|
|
full_path = os.path.join(root, file)
|
|
rel_path = os.path.relpath(full_path, folder_path)
|
|
|
|
# 应用所有筛选条件
|
|
if self.apply_filters(
|
|
file, full_path, file_filter, file_types, regex_filter
|
|
):
|
|
all_items.append(
|
|
(
|
|
rel_path,
|
|
full_path,
|
|
os.path.getmtime(full_path),
|
|
os.path.getsize(full_path),
|
|
)
|
|
)
|
|
else:
|
|
all_items = []
|
|
for item in os.listdir(folder_path):
|
|
full_path = os.path.join(folder_path, item)
|
|
if os.path.isfile(full_path):
|
|
# 应用所有筛选条件
|
|
if self.apply_filters(
|
|
item, full_path, file_filter, file_types, regex_filter
|
|
):
|
|
all_items.append(
|
|
(
|
|
item,
|
|
full_path,
|
|
os.path.getmtime(full_path),
|
|
os.path.getsize(full_path),
|
|
)
|
|
)
|
|
|
|
# 排序
|
|
if sort_by == "name":
|
|
index = 0
|
|
elif sort_by == "date_modified":
|
|
index = 2
|
|
elif sort_by == "size":
|
|
index = 3
|
|
|
|
all_items.sort(key=lambda x: x[index], reverse=(sort_order == "descending"))
|
|
|
|
# 限制数量
|
|
if limit > 0:
|
|
all_items = all_items[:limit]
|
|
|
|
# 提取文件名和路径
|
|
files = [item[0] for item in all_items] # 相对路径或文件名
|
|
full_paths = [item[1] for item in all_items] # 完整路径
|
|
|
|
# 根据 output_full_path 参数选择输出格式
|
|
if output_full_path:
|
|
output_items = full_paths
|
|
else:
|
|
output_items = files
|
|
|
|
# 输出第n个文件
|
|
nth_item = (
|
|
output_items[file_index - 1]
|
|
if 0 < file_index <= len(output_items)
|
|
else "Index out of range."
|
|
)
|
|
|
|
# 生成表状文件夹结构字符串
|
|
tree_structure = self.generate_tree_structure(
|
|
folder_path, files, include_subfolders
|
|
)
|
|
|
|
# 返回所有信息
|
|
return (
|
|
"\n".join(output_items), # 所有文件(字符串,每行一个)
|
|
output_items, # 所有文件(列表)
|
|
nth_item, # 第n个文件
|
|
tree_structure, # 表状文件夹结构
|
|
len(output_items), # 文件总数
|
|
len(output_items) > 0, # 是否有文件
|
|
"", # 错误信息(空表示无错误)
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error: {str(e)}"
|
|
return self.return_error(error_msg)
|
|
|
|
def return_error(self, error_msg):
|
|
"""返回错误信息"""
|
|
return (
|
|
"", # output_string
|
|
[], # output_list
|
|
"", # nth_item
|
|
"", # tree_structure
|
|
0, # file_count
|
|
False, # has_files
|
|
error_msg, # error_message
|
|
)
|
|
|
|
def apply_filters(self, filename, full_path, file_filter, file_types, regex_filter):
|
|
"""应用所有筛选条件"""
|
|
import os
|
|
import re
|
|
import fnmatch
|
|
|
|
# 通配符筛选
|
|
if not fnmatch.fnmatch(filename, file_filter):
|
|
return False
|
|
|
|
# 正则表达式筛选
|
|
if regex_filter and not re.search(regex_filter, filename):
|
|
return False
|
|
|
|
# 文件类型筛选
|
|
if file_types != "all":
|
|
ext = os.path.splitext(filename)[1].lower()
|
|
if file_types == "image" and ext not in [
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".gif",
|
|
".bmp",
|
|
".tiff",
|
|
".webp",
|
|
]:
|
|
return False
|
|
elif file_types == "text" and ext not in [
|
|
".txt",
|
|
".csv",
|
|
".json",
|
|
".xml",
|
|
".html",
|
|
".htm",
|
|
".md",
|
|
]:
|
|
return False
|
|
elif file_types == "audio" and ext not in [
|
|
".mp3",
|
|
".wav",
|
|
".ogg",
|
|
".flac",
|
|
".aac",
|
|
]:
|
|
return False
|
|
elif file_types == "video" and ext not in [
|
|
".mp4",
|
|
".avi",
|
|
".mov",
|
|
".wmv",
|
|
".flv",
|
|
".webm",
|
|
]:
|
|
return False
|
|
|
|
return True
|
|
|
|
def generate_tree_structure(self, folder_path, items, include_subfolders=False):
|
|
"""生成文件夹的表状树结构字符串"""
|
|
import os
|
|
|
|
lines = [os.path.basename(folder_path) + "/"]
|
|
|
|
if include_subfolders:
|
|
# 对于包含子文件夹的情况,需要更复杂的树结构生成
|
|
dir_structure = {}
|
|
for item in items:
|
|
parts = item.split(os.sep)
|
|
current = dir_structure
|
|
for part in parts[:-1]:
|
|
if part not in current:
|
|
current[part] = {}
|
|
current = current[part]
|
|
if "files" not in current:
|
|
current["files"] = []
|
|
current["files"].append(parts[-1])
|
|
|
|
# 递归生成树结构
|
|
def build_tree(structure, prefix=""):
|
|
result = []
|
|
keys = sorted(structure.keys())
|
|
for i, key in enumerate(keys):
|
|
is_last = i == len(keys) - 1
|
|
new_prefix = prefix + (" " if is_last else "│ ")
|
|
result.append(prefix + ("└── " if is_last else "├── ") + key + "/")
|
|
|
|
if isinstance(structure[key], dict):
|
|
result.extend(build_tree(structure[key], new_prefix))
|
|
elif key == "files":
|
|
files = sorted(structure[key])
|
|
for j, file in enumerate(files):
|
|
file_is_last = j == len(files) - 1
|
|
result.append(
|
|
new_prefix + ("└── " if file_is_last else "├── ") + file
|
|
)
|
|
return result
|
|
|
|
lines.extend(build_tree(dir_structure))
|
|
else:
|
|
# 简单情况:只显示当前文件夹
|
|
for i, item in enumerate(sorted(items)):
|
|
is_last = i == len(items) - 1
|
|
prefix = "└── " if is_last else "├── "
|
|
lines.append(prefix + item)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
class BooleanSplitter:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"input_bool": (
|
|
"BOOLEAN",
|
|
{"default": True, "tooltip": "Input boolean value\n输入布尔值"},
|
|
)
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("BOOLEAN", "BOOLEAN")
|
|
RETURN_NAMES = ("input", "opposite")
|
|
FUNCTION = "BooleanSplitter"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Split a boolean value into itself and its opposite.\n将布尔值拆分为自身和其相反值。"
|
|
|
|
def BooleanSplitter(self, input_bool):
|
|
opposite = not input_bool
|
|
return (input_bool, opposite)
|
|
|
|
|
|
class ImageTPSWarp:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"source_image": (
|
|
"IMAGE",
|
|
{"tooltip": "Source image to be warped\n源图像,将被变形"},
|
|
),
|
|
"target_mask": (
|
|
"MASK",
|
|
{
|
|
"tooltip": "Target mask to warp the source image to\n目标遮罩,将源图像变形到此形状"
|
|
},
|
|
),
|
|
"n_points": (
|
|
"INT",
|
|
{
|
|
"default": 150,
|
|
"min": 10,
|
|
"max": 500,
|
|
"step": 10,
|
|
"tooltip": "Number of points to sample from contours\n从轮廓采样的点数",
|
|
},
|
|
),
|
|
"smoothness": (
|
|
"FLOAT",
|
|
{
|
|
"default": 0.1,
|
|
"min": 0.0,
|
|
"max": 10.0,
|
|
"step": 0.1,
|
|
"tooltip": "Smoothness factor for TPS warping (lower = more precise to contours)\nTPS变形的平滑系数(越低=对轮廓越精确)",
|
|
},
|
|
),
|
|
},
|
|
"optional": {
|
|
"source_mask": (
|
|
"MASK",
|
|
{
|
|
"tooltip": "Mask for the source image (optional, if not provided, the whole image will be used)\n源图像的遮罩(可选,如果不提供,将使用整个图像)"
|
|
},
|
|
),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "ImageTPSWarp"
|
|
CATEGORY = "MakkiTools"
|
|
DESCRIPTION = "Warp an image using Thin Plate Spline (TPS) based on source and target masks.\n基于源遮罩和目标遮罩使用薄板样条(TPS)对图像进行变形。"
|
|
|
|
def ImageTPSWarp(
|
|
self, source_image, target_mask, n_points, smoothness, source_mask=None
|
|
):
|
|
import comfy.utils
|
|
import torch
|
|
|
|
# 如果没有提供source_mask,则创建一个与source_image同样大小的全白mask
|
|
if source_mask is None:
|
|
source_mask = torch.ones_like(source_image[..., 0])
|
|
|
|
# 获取目标尺寸(以target_mask为准)
|
|
target_h, target_w = target_mask.shape[-2], target_mask.shape[-1]
|
|
|
|
# 统一调整图像和mask的尺寸
|
|
# 调整source_image尺寸
|
|
if source_image.shape[1] != target_h or source_image.shape[2] != target_w:
|
|
source_image = source_image.permute(
|
|
0, 3, 1, 2
|
|
) # [B, H, W, C] -> [B, C, H, W]
|
|
source_image = comfy.utils.common_upscale(
|
|
source_image, target_w, target_h, "bicubic", "center"
|
|
)
|
|
source_image = source_image.permute(
|
|
0, 2, 3, 1
|
|
) # [B, C, H, W] -> [B, H, W, C]
|
|
|
|
# 调整source_mask尺寸
|
|
if source_mask.shape[-2] != target_h or source_mask.shape[-1] != target_w:
|
|
source_mask = source_mask.unsqueeze(
|
|
1
|
|
) # 添加通道维度 [B, H, W] -> [B, 1, H, W]
|
|
source_mask = comfy.utils.common_upscale(
|
|
source_mask, target_w, target_h, "bicubic", "center"
|
|
)
|
|
source_mask = source_mask.squeeze(
|
|
1
|
|
) # 移除通道维度 [B, 1, H, W] -> [B, H, W]
|
|
|
|
# 调整target_mask尺寸(以防万一)
|
|
if target_mask.shape[-2] != target_h or target_mask.shape[-1] != target_w:
|
|
target_mask = target_mask.unsqueeze(
|
|
1
|
|
) # 添加通道维度 [B, H, W] -> [B, 1, H, W]
|
|
target_mask = comfy.utils.common_upscale(
|
|
target_mask, target_w, target_h, "bicubic", "center"
|
|
)
|
|
target_mask = target_mask.squeeze(
|
|
1
|
|
) # 移除通道维度 [B, 1, H, W] -> [B, H, W]
|
|
|
|
result = self._execute_tps_warping(
|
|
source_image, source_mask, target_mask, n_points, smoothness
|
|
)
|
|
return (result,)
|
|
|
|
def _execute_tps_warping(
|
|
self, source_image, source_mask, target_mask, n_points, smoothness
|
|
):
|
|
from .warp_tps import warp_image_tps_comfyui
|
|
|
|
# 调整mask维度为4D以适配函数(如果需要)
|
|
if source_mask.dim() == 3:
|
|
source_mask = source_mask.unsqueeze(0)
|
|
if target_mask.dim() == 3:
|
|
target_mask = target_mask.unsqueeze(0)
|
|
|
|
# 如果source_image维度是3(单张图片),也添加批次维度
|
|
if source_image.dim() == 3:
|
|
source_image = source_image.unsqueeze(0)
|
|
|
|
result = warp_image_tps_comfyui(
|
|
source_image, source_mask, target_mask, n_points, smoothness
|
|
)
|
|
return result
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GetImageNthCount_makki": GetImageNthCount,
|
|
"ImageChannelSeparate_makki": ImageChannelSeparate,
|
|
"ImageCountConcatenate_makki": ImageCountConcatenate,
|
|
"MergeImageChannels_makki": MergeImageChannels,
|
|
"ImageWidthStitch_makki": ImageWidthStitch,
|
|
"ImageHeigthStitch_makki": ImageHeigthStitch,
|
|
"AutoLoop_create_pseudo_loop_video_makki": AutoLoop_create_pseudo_loop_video,
|
|
"Environment_INFO_makki": Environment_INFO,
|
|
"translators_makki": translators,
|
|
"translator_m2m100_makki": translator_m2m100,
|
|
"random_any_makki": random_any,
|
|
"AnyImageStitch_makki": AnyImageStitch,
|
|
"AnyImagetoConditioning_flux_kontext_makki": AnyImagetoConditioning_flux_kontext,
|
|
"show_type_makki": show_type,
|
|
"timer_makki": timer,
|
|
"Image_Resize_makki": Image_Resize,
|
|
"Prism_Mirage_makki": Prism_Mirage,
|
|
"int_calculate_statistics_makki": int_calculate_statistics,
|
|
"BatchLoraLoader_makki": BatchLoraLoader,
|
|
"UniversalInstaller_makki": UniversalInstaller,
|
|
"get_folder_info_makki": get_folder_info,
|
|
"BooleanSplitter_makki": BooleanSplitter,
|
|
"ImageTPSWarp_makki": ImageTPSWarp,
|
|
}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GetImageNthCount_makki": "GetImageNthCount(已弃用-mki-获取第N张图像)",
|
|
"ImageChannelSeparate_makki": "ImageChannelSeparate(mki-图像通道分离)",
|
|
"ImageCountConcatenate_makki": "ImageCountConcatenate(已弃用-mki-图像批次拼接)",
|
|
"MergeImageChannels_makki": "MergeImageChannels(mki-图像通道合并)",
|
|
"ImageWidthStitch_makki": "ImageWidthStitch(已弃用-mki-图像横向拼接)",
|
|
"ImageHeigthStitch_makki": "ImageHeigthStitch(已弃用-mki-图像纵向拼接)",
|
|
"AutoLoop_create_pseudo_loop_video_makki": "AutoLoop_create_pseudo_loop_video(已弃用-mki-创建伪循环视频)",
|
|
"Environment_INFO_makki": "Environment_INFO(mki-环境信息显示)",
|
|
"translators_makki": "translators(mki-多语言翻译)",
|
|
"translator_m2m100_makki": "translator_m2m100(mki-多语言翻译-m2m100)",
|
|
"random_any_makki": "random_any(mki-随机数输出)",
|
|
"AnyImageStitch_makki": "AnyImageStitch(mki-任意数量图像拼接)",
|
|
"AnyImagetoConditioning_flux_kontext_makki": "AnyImagetoConditioning_flux_kontext(mki-任意数量图像转条件-flux_kontext)",
|
|
"show_type_makki": "show_type(mki-数据类型显示)",
|
|
"timer_makki": "timer(mki-计时器)",
|
|
"Image_Resize_makki": "Image_Resize(mki-图像大小调整)",
|
|
"Prism_Mirage_makki": "Prism_Mirage(mki-光棱坦克)",
|
|
"int_calculate_statistics_makki": "int_calculate_statistics(mki-整数计算统计)",
|
|
"BatchLoraLoader_makki": "BatchLoraLoader(mki-批量LoRA加载)",
|
|
"UniversalInstaller_makki": "UniversalInstaller(mki-安装Python包)",
|
|
"get_folder_info_makki": "get_folder_info(mki-获取文件夹文件信息)",
|
|
"BooleanSplitter_makki": "BooleanSplitter(mki-布尔值拆分)",
|
|
"ImageTPSWarp_makki": "ImageTPSWarp(mki-TPS图像变形)",
|
|
}
|