Files
saftle-uber_comfy_nodes/nodes.py
T

835 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from nodes import SaveImage
import json
from PIL import Image
import numpy as np
from PIL.PngImagePlugin import PngInfo
from comfy.cli_args import args # type: ignore
import folder_paths # type: ignore
from folder_paths import get_filename_list # type: ignore
import comfy
import os
import re
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import psutil
import time
from typing import Any, Dict, List, Tuple, Optional
from collections import OrderedDict, defaultdict
import threading
class ControlNetSelector:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"control_net_name": (get_filename_list("controlnet"),)
}
}
RETURN_TYPES = (folder_paths.get_filename_list("controlnet"), )
RETURN_NAMES = ("control_net_name",)
FUNCTION = "get_control_net_name"
CATEGORY = 'Uber Comfy'
def get_control_net_name(self, control_net_name):
return (control_net_name,)
class ControlNetOptionalLoader:
@classmethod
def INPUT_TYPES(s):
# Extending the file list with a 'None' option for manual selection
return {"required": { "control_net_name": (["None"] + folder_paths.get_filename_list("controlnet"), )}}
RETURN_TYPES = ("CONTROL_NET",)
FUNCTION = "load_controlnet"
CATEGORY = "Uber Comfy"
def load_controlnet(self, control_net_name):
# Only proceed if a control_net_name is provided and it is not 'None'
if control_net_name and control_net_name != "None":
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
controlnet = comfy.controlnet.load_controlnet(controlnet_path)
return (controlnet,)
# Return None or skip the operation if 'None' is selected or no input is provided
return (None,)
class DiffusersSelector:
CATEGORY = 'Uber Comfy'
RETURN_TYPES = (folder_paths.get_folder_paths("diffusers"), )
RETURN_NAMES = ("model_path",)
FUNCTION = "select_model_path"
@classmethod
def INPUT_TYPES(cls):
paths = []
for search_path in folder_paths.get_folder_paths("diffusers"):
if os.path.exists(search_path):
for root, subdirs, files in os.walk(search_path, followlinks=True):
if "model_index.json" in files:
paths.append(os.path.relpath(root, start=search_path))
return {"required": {"model_path": (paths,), }}
def select_model_path(self, model_path):
# This function simply returns the model path that was selected
return (model_path,)
class SaveImageJPGNoMeta(SaveImage):
@classmethod
def INPUT_TYPES(s):
output = {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
"quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
},
}
return output
CATEGORY = "Uber Comfy"
RETURN_TYPES = ()
FUNCTION = "suplex_save_images"
def suplex_save_images(
self,
images,
filename_prefix="ComfyUI",
format="jpeg",
quality=92,
):
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()
for batch_number, image in enumerate(images):
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{format}"
results.append(
{"filename": file, "subfolder": subfolder, "type": self.type}
)
counter += 1
img.save(os.path.join(full_output_folder, file), quality=quality, optimize=True)
return {"ui": {"images": results}}
class MultiInputVariableRewrite:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True}),
},
"optional": {
"a": ("STRING", {"forceInput": True}),
"b": ("STRING", {"forceInput": True}),
"c": ("STRING", {"forceInput": True}),
"d": ("STRING", {"forceInput": True}),
"e": ("STRING", {"forceInput": True}),
# ... add more up to 'z' if needed
}
}
CATEGORY = "Uber Comfy"
FUNCTION = "multicombinetext"
RETURN_NAMES = ("TEXT",)
RETURN_TYPES = ("STRING",)
def multicombinetext(self, text="", **kwargs):
for key, value in kwargs.items():
if value:
text = text.replace(f"{{{key}}}", value)
return (text,)
class TextRegexOperations:
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"text": ("STRING", {"multiline": True, "forceInput": True}),
"num_operations": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}),
},
"optional": {}
}
# Create inputs in interleaved order (pattern_1, replacement_1, multiline_1, pattern_2, etc.)
for i in range(1, 21):
inputs["optional"][f"pattern_{i}"] = ("STRING", {"multiline": True})
inputs["optional"][f"replacement_{i}"] = ("STRING", {"multiline": True})
inputs["optional"][f"use_multiline_{i}"] = ("BOOLEAN", {"default": True})
return inputs
RETURN_TYPES = ("STRING",)
FUNCTION = "process_text"
CATEGORY = "Uber Comfy"
def process_text(self, text, num_operations, **kwargs):
processed_text = text
for i in range(1, num_operations + 1):
pattern = kwargs.get(f"pattern_{i}", "")
replacement = kwargs.get(f"replacement_{i}", "")
use_multiline = kwargs.get(f"use_multiline_{i}", True)
if pattern:
try:
flags = re.MULTILINE if use_multiline else 0
processed_text = re.sub(pattern, replacement, processed_text, flags=flags)
except re.error as e:
print(f"Regex error in operation {i}: {str(e)}")
print(f"Pattern: {pattern}")
return (processed_text,)
class VideoSegmentCalculator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"duration": ("FLOAT", {
"default": 30.0,
"min": 1.0,
"max": 3600.0,
"step": 1.0,
"tooltip": "Duration of each segment in seconds"
}),
"frame_rate": ("FLOAT", {
"default": 25.0,
"min": 1.0,
"max": 120.0,
"step": 0.1,
"tooltip": "Frame rate of the video"
}),
"index": ("INT", {
"default": 0,
"min": 0,
"max": 1000,
"step": 1,
"tooltip": "Current segment index (0-based)"
}),
},
"optional": {
"overlap_frames": ("INT", {
"default": 0,
"min": 0,
"max": 10,
"step": 1,
"tooltip": "Number of frames to overlap between segments"
}),
"precise_timing": ("BOOLEAN", {
"default": True,
"tooltip": "Use precise decimal timing for audio trimming"
}),
}
}
RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("frame_load_cap", "skip_first_frames", "force_rate", "start_time", "end_time")
FUNCTION = "calculate_segment"
CATEGORY = "Uber Comfy"
def calculate_segment(self, duration, frame_rate, index, overlap_frames=0, precise_timing=True):
# Calculate the exact frames for the given duration
exact_frames = duration * frame_rate
# Calculate the number of frames in each segment (using ceiling to prevent gaps)
frames_per_segment = math.ceil(exact_frames)
# Calculate skip_first_frames based on index, with optional overlap
if index == 0:
skip_first_frames = 0
else:
skip_first_frames = index * frames_per_segment - overlap_frames
# Ensure we don't go negative
skip_first_frames = max(0, skip_first_frames)
# Calculate start and end times based on exact frame positions
start_time = skip_first_frames / frame_rate
end_time = (skip_first_frames + frames_per_segment) / frame_rate
# Round to 2 decimal places for audio timing if needed
if not precise_timing:
start_time = round(start_time, 2)
end_time = round(end_time, 2)
return (frames_per_segment, skip_first_frames, frame_rate, start_time, end_time)
class ModelSimilarityNode:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"base_model": ("MODEL",),
"target_model": ("MODEL",)}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("similarity_report",)
FUNCTION = "compare"
CATEGORY = "Uber Comfy"
# ---------- helpers ----------
@staticmethod
def _cross(to_q, to_k, to_v, rnd):
h, e = to_q.shape
lq = nn.Linear(h, e, bias=False); lq.weight.copy_(to_q)
lk = nn.Linear(h, e, bias=False); lk.weight.copy_(to_k)
lv = nn.Linear(h, e, bias=False); lv.weight.copy_(to_v)
return torch.einsum(
"ik,jk->ik",
torch.softmax(torch.einsum("ij,kj->ik", lq(rnd), lk(rnd)), dim=-1),
lv(rnd)
)
@classmethod
def _unwrap(cls, obj):
if isinstance(obj, dict): return obj
if hasattr(obj, "get_weights"): return obj.get_weights()
if hasattr(obj, "state_dict"): return obj.state_dict()
for a in ("model", "original_model"):
if hasattr(obj, a): return cls._unwrap(getattr(obj, a))
raise TypeError("Cannot unwrap MODEL object")
# ---------- main ----------
def compare(self, base_model, target_model):
b_sd = self._unwrap(base_model)
t_sd = self._unwrap(target_model)
torch.manual_seed(114514)
rnd, b_attn, sims, common = {}, {}, [], []
# prefixes to scan: input, middle, output
scan = [
("diffusion_model.input_blocks", 5), # indices 0-4
("diffusion_model.middle_block", 1), # index 0
("diffusion_model.output_blocks",11), # indices 0-10
]
# discover layers present in both models
for prefix, max_idx in scan:
for i in range(max_idx):
key = f"{prefix}.{i}.1.transformer_blocks.0.attn1.to_q.weight"
if key in b_sd and key in t_sd:
common.append((prefix, i))
if not common:
return ("No matching attention layers found in both models.",)
# compute attention outputs for base model
for p, i in common:
q = b_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_q.weight"]
k = b_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_k.weight"]
v = b_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_v.weight"]
h, e = q.shape
key = f"{p}.{i}"
rnd[key] = torch.randn(e, h)
b_attn[key] = self._cross(q, k, v, rnd[key])
# compare with target model
for p, i in common:
key = f"{p}.{i}"
t_attn = self._cross(
t_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_q.weight"],
t_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_k.weight"],
t_sd[f"{p}.{i}.1.transformer_blocks.0.attn1.to_v.weight"],
rnd[key]
)
sims.append(torch.mean(torch.cosine_similarity(b_attn[key], t_attn)))
score = torch.mean(torch.stack(sims)) * 100
return (f"Similarity: {score:.2f}% (compared {len(common)} blocks)",)
class ModelWeightDumperNode:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"model": ("MODEL",),
"show_shapes": ("BOOLEAN", {"default": True}),
"filter_prefix": ("STRING", {"default": ""}),
}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("weight_info",)
FUNCTION = "dump_weights"
CATEGORY = "Uber Comfy"
@classmethod
def _unwrap(cls, obj):
if isinstance(obj, dict): return obj
if hasattr(obj, "get_weights"): return obj.get_weights()
if hasattr(obj, "state_dict"): return obj.state_dict()
for a in ("model", "original_model"):
if hasattr(obj, a): return cls._unwrap(getattr(obj, a))
raise TypeError("Cannot unwrap MODEL object")
def dump_weights(self, model, show_shapes=True, filter_prefix=""):
sd = self._unwrap(model)
output_lines = []
output_lines.append(f"Total weights: {len(sd)}\n")
output_lines.append("="*80 + "\n")
# Filter keys if prefix is provided
keys = sorted(sd.keys())
if filter_prefix:
keys = [k for k in keys if k.startswith(filter_prefix)]
output_lines.append(f"Filtered by prefix: '{filter_prefix}'\n")
output_lines.append(f"Matching weights: {len(keys)}\n")
output_lines.append("="*80 + "\n")
for key in keys:
weight = sd[key]
if show_shapes:
shape_str = f" → {tuple(weight.shape)}" if hasattr(weight, 'shape') else ""
dtype_str = f" ({weight.dtype})" if hasattr(weight, 'dtype') else ""
output_lines.append(f"{key}{shape_str}{dtype_str}\n")
else:
output_lines.append(f"{key}\n")
return ("".join(output_lines),)
class RunwareResolutionCalculator:
"""ComfyUI node for intelligent resolution calculation with model-specific presets."""
PRESETS = {
"Nano Banana 2": [
# Square formats (1:1)
(1024, 1024), (2048, 2048), (4096, 4096),
# Landscape 3:2 ratio
(1200, 896), (2400, 1792), (4800, 3584),
# Portrait 2:3 ratio
(896, 1200), (1792, 2400), (3584, 4800),
# Landscape 5:4 ratio
(1152, 928), (2304, 1856), (4608, 3712),
# Portrait 4:5 ratio
(928, 1152), (1856, 2304), (3712, 4608),
# Landscape 3:2 wider
(1264, 848), (2528, 1696), (5056, 3392), (5096, 3392),
# Portrait 2:3 taller
(848, 1264), (1696, 2528), (3392, 5056), (3392, 5096),
# Ultra-wide landscape
(1376, 768), (2752, 1536), (5504, 3072),
# Ultra-tall portrait
(768, 1376), (1536, 2752), (3072, 5504),
# Cinematic ultra-wide
(1548, 672), (1584, 672), (3168, 1344), (6336, 2688),
],
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"full_image": ("IMAGE",),
"model_preset": (list(cls.PRESETS.keys()), {
"default": "Nano Banana 2"
}),
},
"optional": {
"cropped_image": ("IMAGE",),
"mask": ("MASK",),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "calculate"
CATEGORY = "Uber Comfy"
def calculate(self, full_image, model_preset, cropped_image=None, mask=None):
"""Main calculation entry point."""
_, full_h, full_w, _ = full_image.shape
resolutions = self.PRESETS.get(model_preset, self.PRESETS["Nano Banana 2"])
if cropped_image is None or mask is None:
return self._direct_resolution_selection(full_w, full_h, resolutions, model_preset)
return self._mask_constrained_selection(full_image, mask, resolutions, model_preset)
def _direct_resolution_selection(self, img_w, img_h, resolutions, preset_name):
"""Select resolution using multi-tier filtering and ranking."""
print(f"🎯 {preset_name} Optimizer (Direct Resolution Mode)")
print(f" Source dimensions: {img_w}×{img_h}")
source_ratio = img_w / img_h
source_area = img_w * img_h
# Tier 1: Spatial filtering
spatial_candidates = [
(w, h) for w, h in resolutions
if w <= img_w and h <= img_h
]
if not spatial_candidates:
print(f" ⚠️ No candidates within bounds, using 1024x1024")
return (1024, 1024)
# Tier 2: Aspect ratio bucketing
def get_aspect_bucket(ratio):
if 0.95 <= ratio <= 1.05:
return "square"
elif ratio > 1.5:
return "wide"
elif ratio < 0.67:
return "tall"
else:
return "standard"
source_bucket = get_aspect_bucket(source_ratio)
# Tier 3: Multi-criteria scoring
scored_candidates = []
for res_w, res_h in spatial_candidates:
res_ratio = res_w / res_h
res_area = res_w * res_h
res_bucket = get_aspect_bucket(res_ratio)
log_ratio_distance = abs(np.log2(source_ratio) - np.log2(res_ratio))
aspect_score = 1.0 / (1.0 + log_ratio_distance)
utilization = res_area / source_area
utilization_score = utilization
bucket_bonus = 1.2 if res_bucket == source_bucket else 1.0
quality_tier = min(1.0, np.log2(res_area / 1048576) / 2)
# Weighted product
composite = (
(aspect_score ** 2.8) *
(utilization_score ** 1.5) *
bucket_bonus *
(1.0 + quality_tier * 0.3)
)
scored_candidates.append({
'resolution': (res_w, res_h),
'score': composite,
'aspect_score': aspect_score,
'utilization': utilization
})
# Maximize score
best = max(scored_candidates, key=lambda x: x['score'])
print(f" ✓ Selected: {best['resolution'][0]}×{best['resolution'][1]} "
f"(aspect: {best['aspect_score']:.3f}, util: {best['utilization']:.3f})")
return best['resolution']
def _mask_constrained_selection(self, full_image, mask_tensor, resolutions, preset_name):
"""Select resolution using bi-directional fitting analysis."""
print(f"🎯 {preset_name} Optimizer (Mask-Based Mode)")
_, full_h, full_w, _ = full_image.shape
mask_data = mask_tensor.cpu().numpy()[0]
active_pixels = mask_data > 0.5
y_coords, x_coords = np.where(active_pixels)
if len(y_coords) == 0 or len(x_coords) == 0:
return (1024, 1024)
roi_x1, roi_x2 = x_coords.min(), x_coords.max()
roi_y1, roi_y2 = y_coords.min(), y_coords.max()
roi_width = roi_x2 - roi_x1 + 1
roi_height = roi_y2 - roi_y1 + 1
print(f" ROI bounds: {roi_width}×{roi_height}")
roi_ratio = roi_width / roi_height
roi_area = roi_width * roi_height
scored_candidates = []
for res_w, res_h in resolutions:
target_ratio = res_w / res_h
# Three fitting strategies
fit_a_w = roi_width
fit_a_h = int(roi_width / target_ratio)
fit_b_h = roi_height
fit_b_w = int(roi_height * target_ratio)
scale_to_ratio = (target_ratio / roi_ratio) ** 0.5
fit_c_w = int(roi_width * scale_to_ratio)
fit_c_h = int(roi_height / scale_to_ratio)
viable_fits = []
for fit_w, fit_h, strategy in [
(fit_a_w, fit_a_h, 'width_anchor'),
(fit_b_w, fit_b_h, 'height_anchor'),
(fit_c_w, fit_c_h, 'proportional')
]:
if fit_w <= full_w and fit_h <= full_h:
expansion_w = fit_w - roi_width
expansion_h = fit_h - roi_height
total_expansion = expansion_w + expansion_h
viable_fits.append({
'width': fit_w,
'height': fit_h,
'expansion': total_expansion,
'strategy': strategy
})
if not viable_fits:
continue
best_fit = min(viable_fits, key=lambda x: x['expansion'])
fitted_area = best_fit['width'] * best_fit['height']
target_area = res_w * res_h
expansion_ratio = fitted_area / roi_area
expansion_efficiency = 1.0 / expansion_ratio
scale_match = min(fitted_area, target_area) / max(fitted_area, target_area)
strategy_weight = 1.1 if best_fit['strategy'] == 'proportional' else 1.0
# Harmonic mean
harmonic_mean = 3.0 / (
(1.0 / expansion_efficiency) +
(1.0 / scale_match) +
(1.0 / strategy_weight)
)
scored_candidates.append({
'resolution': (res_w, res_h),
'score': harmonic_mean,
'fitted': (best_fit['width'], best_fit['height']),
'expansion': expansion_ratio,
'strategy': best_fit['strategy']
})
if not scored_candidates:
return (1024, 1024)
best = max(scored_candidates, key=lambda x: x['score'])
print(f" Fitted area: {best['fitted'][0]}×{best['fitted'][1]} ({best['strategy']})")
print(f" ✓ Selected: {best['resolution'][0]}×{best['resolution'][1]} "
f"(expansion: {best['expansion']:.2f}x)")
return best['resolution']
class AdaptiveImageScaler:
"""Intelligent image scaling with optional ML-based upscaling and dimension constraints."""
INTERPOLATION_MODES = [
"lanczos", "bicubic", "bilinear", "area", "nearest-exact"
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"target_width": ("INT",),
"target_height": ("INT",),
"interpolation": (cls.INTERPOLATION_MODES,),
"dimension_alignment": ("INT", {
"default": 0, "min": 0, "max": 256, "step": 1
}),
},
"optional": {
"upscale_model": ("UPSCALE_MODEL",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
CATEGORY = "Uber Comfy"
MODEL_ENGAGEMENT_THRESHOLD = 1.08
TILE_REDUCTION_FACTOR = 0.75
MIN_SAFE_TILE_SIZE = 128
def process(self, image, target_width, target_height, interpolation,
dimension_alignment, upscale_model=None):
"""Main processing pipeline."""
import comfy.utils
from comfy import model_management
batch_size, source_h, source_w, channels = image.shape
if dimension_alignment > 0:
target_width = (target_width // dimension_alignment) * dimension_alignment
target_height = (target_height // dimension_alignment) * dimension_alignment
scale_w = target_width / source_w
scale_h = target_height / source_h
geometric_mean_scale = (scale_w * scale_h) ** 0.5
print(f"🖼️ Adaptive Scaler: {source_w}×{source_h}×{channels} → {target_width}×{target_height}")
print(f" Scale: {geometric_mean_scale:.3f}x (w:{scale_w:.2f}, h:{scale_h:.2f}) | {interpolation}")
use_ml_model = (
geometric_mean_scale > self.MODEL_ENGAGEMENT_THRESHOLD and
upscale_model is not None
)
if use_ml_model:
return self._ml_assisted_pipeline(
image, target_width, target_height, interpolation, channels,
upscale_model, comfy, model_management
)
else:
return self._standard_interpolation_pipeline(
image, target_width, target_height, interpolation,
geometric_mean_scale, comfy
)
def _ml_assisted_pipeline(self, image, target_w, target_h, interpolation,
channels, model, comfy, model_mgmt):
"""ML-based upscaling with synchronized alpha processing."""
print(f" ⚡ ML upscaling engaged")
has_alpha = (channels == 4)
if has_alpha:
print(f" 💎 Alpha channel: synchronized processing")
rgb_channels = image[:, :, :, :3]
alpha_channel = image[:, :, :, 3:4]
else:
rgb_channels = image
alpha_channel = None
device = model_mgmt.get_torch_device()
model.to(device)
rgb_gpu = rgb_channels.movedim(-1, -3).to(device)
upscaled_rgb = self._progressive_tiled_upscale(
rgb_gpu, model, comfy, model_mgmt
)
model.cpu()
upscaled_rgb = torch.clamp(upscaled_rgb.movedim(-3, -1), 0.0, 1.0)
upscaled_rgb = upscaled_rgb.movedim(-1, 1)
upscaled_rgb = comfy.utils.common_upscale(
upscaled_rgb, target_w, target_h, interpolation, "disabled"
)
upscaled_rgb = upscaled_rgb.movedim(1, -1)
if has_alpha:
# Bicubic for quality
alpha_scaled = self._scale_alpha_channel(
alpha_channel, target_w, target_h, use_bicubic=True
)
result = torch.cat([upscaled_rgb, alpha_scaled], dim=-1)
print(f" 💎 Alpha reconstructed with bicubic interpolation")
else:
result = upscaled_rgb
return (result,)
def _progressive_tiled_upscale(self, tensor, model, comfy, model_mgmt):
"""Progressive tile reduction (0.75x vs 0.5x)."""
current_tile = 512
overlap = 32
while True:
try:
steps = tensor.shape[0] * comfy.utils.get_tiled_scale_steps(
tensor.shape[3], tensor.shape[2],
tile_x=current_tile, tile_y=current_tile, overlap=overlap
)
pbar = comfy.utils.ProgressBar(steps)
result = comfy.utils.tiled_scale(
tensor,
lambda t: model(t),
tile_x=current_tile, tile_y=current_tile,
overlap=overlap,
upscale_amount=model.scale,
pbar=pbar
)
return result
except model_mgmt.OOM_EXCEPTION as e:
new_tile = int(current_tile * self.TILE_REDUCTION_FACTOR)
if new_tile < self.MIN_SAFE_TILE_SIZE:
print(f" ❌ Tile size below safe threshold ({self.MIN_SAFE_TILE_SIZE}px)")
raise e
print(f" ⚠️ Memory limit reached, reducing tile: {current_tile}→{new_tile}px")
current_tile = new_tile
def _scale_alpha_channel(self, alpha, target_w, target_h, use_bicubic=True):
"""Scale alpha using squeeze/unsqueeze (different from permute)."""
alpha_reshaped = alpha.squeeze(-1).unsqueeze(1)
mode = 'bicubic' if use_bicubic else 'bilinear'
alpha_scaled = F.interpolate(
alpha_reshaped,
size=(target_h, target_w),
mode=mode,
align_corners=False if mode == 'bilinear' else None
)
alpha_result = alpha_scaled.squeeze(1).unsqueeze(-1)
return alpha_result
def _standard_interpolation_pipeline(self, image, target_w, target_h,
method, scale, comfy):
"""Direct interpolation with integrated channels."""
direction = "⬇️ downscale" if scale < 1.0 else "⬆️ upscale"
print(f" {direction} via {method} interpolation")
tensor_bchw = image.movedim(-1, 1)
scaled_bchw = comfy.utils.common_upscale(
tensor_bchw, target_w, target_h, method, "disabled"
)
result = scaled_bchw.movedim(1, -1)
return (result,)
NODE_CLASS_MAPPINGS = {
"ControlNet Selector": ControlNetSelector,
"ControlNetOptionalLoader": ControlNetOptionalLoader,
"DiffusersSelector": DiffusersSelector,
"SaveImageJPGNoMeta": SaveImageJPGNoMeta,
"MultiInputVariableRewrite": MultiInputVariableRewrite,
"TextRegexOperations": TextRegexOperations,
"VideoSegmentCalculator": VideoSegmentCalculator,
"ModelSimilarityNode": ModelSimilarityNode,
"ModelWeightDumperNode": ModelWeightDumperNode,
"RunwareResolutionCalculator": RunwareResolutionCalculator,
"AdaptiveImageScaler": AdaptiveImageScaler,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ControlNet Selector": "ControlNet Selector",
"ControlNetOptionalLoader": "Load Optional ControlNet Model",
"DiffusersSelector": "Diffusers Selector",
"SaveImageJPGNoMeta": "Save Image JPG No Meta",
"MultiInputVariableRewrite": "Multi Input Variable Rewrite",
"TextRegexOperations": "Text Regex Operations",
"VideoSegmentCalculator": "Video Segment Calculator",
"ModelSimilarityNode": "Model Similarity Node",
"ModelWeightDumperNode": "Model Weight Dumper",
"RunwareResolutionCalculator": "Runware Resolution Calculator",
"AdaptiveImageScaler": "Adaptive Image Scaler",
}