Add Z-Image Turbo latent node and update WAN 2.2 scaler

Introduces the ZImageTurboEmptyLatentImage node for SD3-compatible latent generation with curated safe resolutions. Refactors SaveLatentMXD and SaveLatent_I2V_MXD to remove preview image logic and streamline latent/conditioning saving. Updates WAN22_I2V_Image_Scaler_MXD to add a 'Safe Auto' mode for robust video-extend workflows, improves bucket selection, and enhances error handling for unsupported resolutions. Also updates display names and increments the package version to 1.3.0.
This commit is contained in:
Maxed-Out-99
2025-12-13 16:15:55 -08:00
parent 0758ae70e6
commit f02f7a8c9a
3 changed files with 219 additions and 236 deletions
+89
View File
@@ -171,6 +171,93 @@ class SdxlEmptyLatentImage:
# Typically, the latent space has 4 channels and each spatial dimension is 1/8th of the image.
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
return ({"samples": latent},)
########################################################################################################################
# Z-Image Turbo Empty Latent Image (SD3-compatible)
class ZImageTurboEmptyLatentImage:
DESCRIPTION = """
- Provides a curated set of SAFE and OPTIMAL resolutions for Z-Image Turbo.
- All presets stay within Z-Image Turbo's native comfort zone
to avoid composition breakage and latent corruption.
- Designed to save time and remove guesswork.
"""
TITLE = "Z-Image Turbo Empty Latent Image"
CATEGORY = "MXD/Latent"
RESOLUTIONS = {
"— Recommended (Best Overall) —": None,
"Square (1:1) 1024x1024": (1024, 1024),
"Landscape (16:9) 1920x1088": (1920, 1088),
"Portrait (2:3) 1024x1536": (1024, 1536),
"— High Detail —": None,
"Square (1:1) 1280x1280": (1280, 1280),
"Square (1:1) 1536x1536": (1536, 1536),
"Landscape (16:9) 2048x1152": (2048, 1152),
"Portrait (9:16) 1152x2048": (1152, 2048),
"— Cinematic / Wide —": None,
"Landscape (21:9) 2016x864": (2016, 864),
"Landscape (21:9) 1680x720": (1680, 720),
"— Fast / Draft —": None,
"Square (1:1) 768x768": (768, 768),
"Landscape (16:9) 1280x720": (1280, 720),
"Portrait (3:4) 832x1216": (832, 1216),
}
def __init__(self):
self.device = comfy.model_management.intermediate_device()
@classmethod
def INPUT_TYPES(cls) -> dict:
return {
"required": {
"resolution": (
list(cls.RESOLUTIONS.keys()),
{"default": "Square (1:1) 1024x1024"}
),
"vertical": (
"BOOLEAN",
{
"default": False,
"tooltip": "Swap width and height."
}
),
"batch_size": (
"INT",
{
"default": 1,
"min": 1,
"max": 4096,
"tooltip": "Number of latent images in the batch."
}
)
}
}
RETURN_TYPES = ("LATENT",)
OUTPUT_TOOLTIPS = ("The empty Z-Image Turbo latent batch.",)
FUNCTION = "generate"
def generate(self, resolution, vertical, batch_size=1) -> tuple:
size = self.RESOLUTIONS.get(resolution)
if size is None:
raise ValueError(f"'{resolution}' is a header or invalid option.")
width, height = size
if vertical:
width, height = height, width
latent = torch.zeros(
[batch_size, 16, height // 8, width // 8],
device=self.device
)
return ({"samples": latent},)
########################################################################################################################
# SDXL Resolution Selector
class SdxlResolutionSelector:
@@ -973,6 +1060,7 @@ NODE_CLASS_MAPPINGS = {
"Crop Image By Mask": CropImageByMask,
"Load Image Batch MXD": LoadImageBatchMXD,
"LoadImageWithPromptsMXD": LoadImageWithPromptsMXD,
"ZImageTurboEmptyLatentImage": ZImageTurboEmptyLatentImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -989,4 +1077,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Crop Image By Mask": "Crop Image by Mask MXD",
"Load Image Batch MXD": "Load Image Batch MXD",
"LoadImageWithPromptsMXD": "Load Image MXD",
"ZImageTurboEmptyLatentImage": "ZImageTurbo Empty Latent Image MXD",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "maxedout"
description = "Custom ComfyUI nodes used in Maxed Out workflows (SDXL, Flux, Wan 2.2, etc.)"
version = "1.2.1"
version = "1.3.0"
license = {file = "LICENSE"}
# classifiers = [
# # For OS-independent nodes (works on all operating systems)
+129 -235
View File
@@ -3,8 +3,6 @@ import os, re, glob, json, hashlib
from typing import Any, Dict, Tuple, Optional, List, Union
import torch
import numpy as np
from PIL import Image
from safetensors import safe_open
import folder_paths
@@ -60,34 +58,25 @@ async def mxd_list_input_videos(request):
class SaveLatentMXD:
DESCRIPTION = """
- Saves latents to `.latent` files under `input/latents/`.
- Also decodes & saves preview images for quick inspection in the UI.
- Preserves prompt & extra Comfy metadata inside the file.
"""
TITLE = "Save Latent (with Preview)"
TITLE = "Save Latent"
CATEGORY = "MXD/Latents"
RETURN_TYPES = () # only UI
FUNCTION = "save_and_preview"
FUNCTION = "save_only"
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"samples": ("LATENT", {"tooltip": "Latent tensor to save & preview."}),
"vae": ("VAE", {"tooltip": "VAE used to decode preview images."}),
"samples": ("LATENT", {"tooltip": "Latent tensor to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "Prefix for saved latent filename."}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = () # only UI
FUNCTION = "save_and_preview"
OUTPUT_NODE = True
CATEGORY = "MXD/Latents"
def save_and_preview(self, samples, vae, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
def save_only(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
# ---------- Save Latent ----------
latents_dir = os.path.join(folder_paths.get_input_directory(), "latents")
@@ -97,62 +86,91 @@ class SaveLatentMXD:
filename_prefix, latents_dir
)
prompt_info = ""
if prompt is not None:
try:
prompt_info = json.dumps(prompt)
except Exception:
pass
metadata = None
# Metadata
meta = None
if not args.disable_metadata:
metadata = {"prompt": prompt_info}
meta = {}
if prompt is not None:
try: meta["prompt"] = json.dumps(prompt)
except: pass
if extra_pnginfo is not None:
for k, v in extra_pnginfo.items():
try:
metadata[k] = json.dumps(v)
except Exception:
pass
try: meta[k] = json.dumps(v)
except: pass
file = f"{filename}_{counter:05}_.latent"
file = os.path.join(full_output_folder, file)
file = os.path.join(full_output_folder, f"{filename}_{counter:05}_.latent")
output = {"latent_tensor": samples["samples"].contiguous(),
"latent_format_version_0": torch.tensor([])}
payload = {
"latent_tensor": samples["samples"].contiguous(),
"latent_format_version_0": torch.tensor([]),
}
comfy.utils.save_torch_file(output, file, metadata=metadata)
comfy.utils.save_torch_file(payload, file, metadata=meta)
# ---------- Decode + Save preview images ----------
images = vae.decode(samples["samples"])
if len(images.shape) == 5: # merge video/batched latents
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
return {} # no previews, no UI
# Save previews to TEMP so the UI can locate them with type="temp"
temp_dir = folder_paths.get_temp_directory()
# build a preview name using the same helper so subfolder/counter are valid
w, h = images[0].shape[1], images[0].shape[0]
preview_prefix = filename_prefix + "_preview"
full_temp_folder, preview_name, temp_counter, temp_subfolder, _ = folder_paths.get_save_image_path(
preview_prefix, temp_dir, w, h
# ---------- SaveLatent I2V (saves latent + conditioning) ----------
class SaveLatent_I2V_MXD:
"""
I2V-only saver that persists:
• latent tensor -> .latent
• pos/neg CONDITIONING -> .cond.pt
"""
TITLE = "Save Latent I2V (with Conditioning)"
CATEGORY = "MXD/Latents (I2V)"
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "save_only"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"samples": ("LATENT", {"tooltip": "High-noise latent to save for later low-noise finishing."}),
"positive": ("CONDITIONING", {"tooltip": "Positive CONDITIONING after WAN image→video."}),
"negative": ("CONDITIONING", {"tooltip": "Negative CONDITIONING after WAN image→video."}),
"filename_prefix": ("STRING", {"default": "I2V", "tooltip": "Prefix for saved files"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
def save_only(self, samples, positive, negative, filename_prefix="I2V",
prompt=None, extra_pnginfo=None):
# ---- save latent (.latent) ----
latents_dir = os.path.join(folder_paths.get_input_directory(), "latents")
os.makedirs(latents_dir, exist_ok=True)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, latents_dir
)
results = []
for batch_number, image in enumerate(images):
np_img = (255.0 * image.cpu().numpy())
img = Image.fromarray(np.clip(np_img, 0, 255).astype(np.uint8))
# Metadata
meta = None
if not args.disable_metadata:
meta = {}
if prompt is not None:
try: meta["prompt"] = json.dumps(prompt)
except: pass
if extra_pnginfo is not None:
for k, v in extra_pnginfo.items():
try: meta[k] = json.dumps(v)
except: pass
fn_with_batch = preview_name.replace("%batch_num%", str(batch_number))
preview_file = f"{fn_with_batch}_{temp_counter:05}_.png"
img.save(os.path.join(full_temp_folder, preview_file), compress_level=1)
latent_path = os.path.join(full_output_folder, f"{filename}_{counter:05}_.latent")
results.append({
"filename": preview_file,
"subfolder": temp_subfolder,
"type": "temp" # 👈 matches temp_dir so UI can render
})
temp_counter += 1
payload = {
"latent_tensor": samples["samples"].contiguous(),
"latent_format_version_0": torch.tensor([]),
}
comfy.utils.save_torch_file(payload, latent_path, metadata=meta)
return {"ui": {"images": results}}
# ---- save conditioning sidecar (.cond.pt) ----
cond_path = latent_path.replace(".latent", ".cond.pt")
torch.save({"positive": positive, "negative": negative}, cond_path)
# No preview logic at all
return {}
# ---------- Helpers ----------
def _load_latent_file(latent_path: str) -> Tuple[Dict[str, torch.Tensor], Dict[str, Any], List[str]]:
@@ -679,7 +697,12 @@ class LoadLatents_FromFolder_WithParams:
sample_dict, meta, _ = _load_latent_file(path)
t = sample_dict["samples"]
slices = [t[i:i+1].contiguous() for i in range(t.size(0))] if (t.dim() >= 4 and t.size(0) > 1) else [t.unsqueeze(0)]
if isinstance(t, torch.Tensor) and t.dim() >= 4 and t.size(0) > 1:
slices = [t[i:i+1].contiguous() for i in range(t.size(0))]
elif isinstance(t, torch.Tensor) and t.dim() >= 4 and t.size(0) == 1:
slices = [t]
else:
slices = [t.unsqueeze(0)]
prompt_json = _safe_json_loads(meta.get("prompt"))
pos, neg, n_steps, cfg, sampler_name, scheduler, end_at_step = _extract_params_from_prompt_json(prompt_json or {})
@@ -1098,100 +1121,6 @@ class wan22EmptyHunyuanLatentVideoMXD:
device=comfy.model_management.intermediate_device()
)
return ({"samples": latent},)
# ---------- I2V-specific latent save/load (sidecar conditioning; subclassed loader) ----------
class SaveLatent_I2V_MXD:
"""
I2V-only saver that persists:
• latent tensor -> .latent (safetensors via comfy.utils.save_torch_file)
• pos/neg CONDITIONING -> .cond.pt (torch.save; robust for nested tensors)
• optional preview images to TEMP for UI
"""
TITLE = "Save Latent I2V (with Conditioning)"
CATEGORY = "MXD/Latents (I2V)"
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "save_and_preview"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"samples": ("LATENT", {"tooltip": "High-noise latent to save for later low-noise finishing."}),
"positive": ("CONDITIONING", {"tooltip": "Positive CONDITIONING after WAN image→video."}),
"negative": ("CONDITIONING", {"tooltip": "Negative CONDITIONING after WAN image→video."}),
"vae": ("VAE", {"tooltip": "Used to decode preview images for UI convenience."}),
"filename_prefix": ("STRING", {"default": "I2V", "tooltip": "Prefix for saved files"}),
"show_preview": ("BOOLEAN", {"default": False, "tooltip": "Show decoded preview images (slower)"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
def save_and_preview(self, samples, positive, negative, vae, filename_prefix="I2V",
show_preview=False, prompt=None, extra_pnginfo=None):
# ---- save latent (.latent) ----
latents_dir = os.path.join(folder_paths.get_input_directory(), "latents")
os.makedirs(latents_dir, exist_ok=True)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, latents_dir
)
meta = None
if not args.disable_metadata:
meta = {}
if prompt is not None:
try:
meta["prompt"] = json.dumps(prompt)
except Exception:
pass
if extra_pnginfo is not None:
for k, v in extra_pnginfo.items():
try:
meta[k] = json.dumps(v)
except Exception:
pass
latent_path = os.path.join(full_output_folder, f"{filename}_{counter:05}_.latent")
payload = {
"latent_tensor": samples["samples"].contiguous(),
"latent_format_version_0": torch.tensor([]),
}
comfy.utils.save_torch_file(payload, latent_path, metadata=meta)
# ---- save conditioning sidecar (.cond.pt) ----
cond_path = latent_path.replace(".latent", ".cond.pt")
torch.save({"positive": positive, "negative": negative}, cond_path)
# ---- optional preview images ----
if not show_preview:
return {} # skip VAE decode and preview generation
images = vae.decode(samples["samples"])
if len(images.shape) == 5:
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
temp_dir = folder_paths.get_temp_directory()
w, h = images[0].shape[1], images[0].shape[0]
preview_prefix = filename_prefix + "_preview"
full_temp_folder, preview_name, temp_counter, temp_subfolder, _ = folder_paths.get_save_image_path(
preview_prefix, temp_dir, w, h
)
results = []
for b, image in enumerate(images):
np_img = (255.0 * image.cpu().numpy())
img = Image.fromarray(np.clip(np_img, 0, 255).astype(np.uint8))
fn_with_batch = preview_name.replace("%batch_num%", str(b))
preview_file = f"{fn_with_batch}_{temp_counter:05}_.png"
img.save(os.path.join(full_temp_folder, preview_file), compress_level=1)
results.append({"filename": preview_file, "subfolder": temp_subfolder, "type": "temp"})
temp_counter += 1
return {"ui": {"images": results}}
# ---------- WAN 2.2 Image to Video (no scaling; expects pre-sized input) ----------
class Wan22ImageToVideoMXD(io.ComfyNode):
@@ -1199,7 +1128,7 @@ class Wan22ImageToVideoMXD(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="Wan22ImageToVideoMXD",
display_name="WAN 2.2 Image to Video",
display_name="WAN 2.2 Image to Video MXD",
category="conditioning/video_models",
description="WAN 2.2 Image to Video (no scaling, no clip vision)",
inputs=[
@@ -1327,46 +1256,35 @@ def _resize_fit_inside(img, out_w, out_h):
resized = comfy.utils.common_upscale(img.movedim(-1, 1), tw, th, "bilinear", "center").movedim(1, -1)
return resized, tw, th
# ---------- WAN 2.2 Image Scaler (no padding; fit or crop modes; square-aware) ----------
# Known WAN 2.2 "safe" video buckets (no-pad, multiples of 16)
# ---------- WAN22_I2V_Image_Scaler_MXD ----------
# Adds a new “Safe Auto” mode for video extend workflows.
# Normal modes (Auto / 480p / 720p) behave exactly as before.
# “Safe Auto” adds passthrough + strict checks to prevent failures on WAN 2.2 extend.
_WAN22_VALID_RES = {
(832, 480), (480, 832), # 480p landscape/portrait
(1280, 720), (720, 1280), # 720p landscape/portrait
(624, 624), # square ~480-tier
(720, 720), # square 720-tier
(832, 480), (480, 832),
(1280, 720), (720, 1280),
(624, 624), (720, 720),
}
def _wan22_is_valid_dim(w, h):
"""Return True if (w, h) is an exact WAN 2.2 bucket we consider safe."""
return (w, h) in _WAN22_VALID_RES
class WAN22_I2V_Image_Scaler_MXD:
"""
MXD Image Scaler for WAN 2.2 (NO PADDING)
- Modes: Auto / 480p / 720p
- Modes: Auto / 480p / 720p / Safe Auto
- Fit (no pad): proportional resize ≤ target; returns resized dims.
- Crop (no pad): resize-to-cover then center-crop to exact target.
- Square handling:
* Auto & 480p: ~square → 624×624
* 720p: ~square → 720×720
- Auto logic:
* Chooses 480p or 720p bucket based on minimal scaling.
* Prefers 480p for small inputs so we don't jump to 720p unnecessarily.
- Video-safe guardrails:
* If input is already an exact WAN 2.2 bucket:
832×480, 480×832,
1280×720, 720×1280,
624×624, 720×720
→ the image is returned **unchanged** (perfect for video-extend “last frame” use).
* If input area is far outside 480p/720p range, raises a clear error
explaining that WAN 2.2 does not support arbitrary resolutions and
they should use something near 480p or 720p.
- “Safe Auto”:
* If input is already a valid WAN 2.2 bucket, passthrough.
* If input is far outside 480p–720p range, error early.
* Otherwise, same logic as Auto.
* Perfect for video-extend workflows.
"""
TITLE = "Image Bucket Scaler MXD (No Pad)"
@@ -1379,7 +1297,7 @@ class WAN22_I2V_Image_Scaler_MXD:
return {
"required": {
"image": ("IMAGE",),
"tier": (["Auto", "480p", "720p"], {"default": "Auto"}),
"tier": (["Auto", "480p", "720p", "Safe Auto"], {"default": "Auto"}),
"crop_to_fit": ("BOOLEAN", {
"default": True,
"label_on": "Perfect Fit (Crops Edges)",
@@ -1392,7 +1310,6 @@ class WAN22_I2V_Image_Scaler_MXD:
# Internal helpers
# -----------------------------
def _pick_bucket(self, iw, ih, tier, crop_to_fit):
in_ar = _ar(iw, ih)
is_squareish = _is_squareish(iw, ih)
is_landscape = iw >= ih
@@ -1401,91 +1318,68 @@ class WAN22_I2V_Image_Scaler_MXD:
if tier == "720p":
return (720, 720)
else:
# Auto & 480p share same square bucket
return (624, 624)
# --- Explicit 480p tier ---
# --- Explicit tiers ---
if tier == "480p":
buckets = [(832, 480)] if is_landscape else [(480, 832)]
return _closest_bucket(iw, ih, buckets, cover=crop_to_fit)
# --- Explicit 720p tier ---
return _closest_bucket(iw, ih, [(832, 480)] if is_landscape else [(480, 832)], cover=crop_to_fit)
if tier == "720p":
buckets = [(1280, 720)] if is_landscape else [(720, 1280)]
return _closest_bucket(iw, ih, buckets, cover=crop_to_fit)
return _closest_bucket(iw, ih, [(1280, 720)] if is_landscape else [(720, 1280)], cover=crop_to_fit)
# --- Auto tier (smart minimal-scaling logic) ---
# --- Auto tier logic ---
buckets_480 = [(832, 480)] if is_landscape else [(480, 832)]
buckets_720 = [(1280, 720)] if is_landscape else [(720, 1280)]
iw_ih = iw * ih
area_480 = 832 * 480
area_720 = 1280 * 720
iw_ih = iw * ih
area_480, area_720 = 832 * 480, 1280 * 720
scale_to_480 = abs(iw_ih - area_480) / area_480
scale_to_720 = abs(iw_ih - area_720) / area_720
# --- Rule 1: small images → stay in 480p bucket (avoid jumping to 720p) ---
# (We still allow upscaling 640x360 → 832x480, etc.)
# prefer minimal scaling
if iw <= 832 and ih <= 480:
return _closest_bucket(iw, ih, buckets_480, cover=crop_to_fit)
# --- Rule 2: otherwise pick bucket with smaller scale delta ---
if scale_to_480 <= scale_to_720:
return _closest_bucket(iw, ih, buckets_480, cover=crop_to_fit)
else:
return _closest_bucket(iw, ih, buckets_720, cover=crop_to_fit)
return _closest_bucket(iw, ih, buckets_480 if scale_to_480 <= scale_to_720 else buckets_720, cover=crop_to_fit)
# -----------------------------
# Main function
# -----------------------------
def scale(self, image, tier="Auto", crop_to_fit=False):
# image: [B, H, W, C]
_, ih, iw, _ = image.shape
# 1) VIDEO-EXTEND SAFETY: If the frame is already a known WAN 2.2 bucket,
# just passthrough. This makes "use last frame from WAN video" bulletproof:
# no chance of re-scaling to the wrong tier.
if _wan22_is_valid_dim(iw, ih):
# Already WAN-safe (832x480, 1280x720, 624x624, 720x720, + portrait variants)
return (image,)
# --- Safe Auto logic ---
if tier == "Safe Auto":
# passthrough if already WAN-safe
if _wan22_is_valid_dim(iw, ih):
return (image,)
# 2) HARD BOUNDS: if the resolution is way off from WAN 2.2's expected range,
# error early with a clear message instead of failing after a long video run.
area = iw * ih
area_480 = 832 * 480 # ~399k
area_720 = 1280 * 720 # ~921k
min_area = int(area_480 * 0.5) # ~half of 480p bucket
max_area = int(area_720 * 1.8) # somewhat above 720p bucket
area = iw * ih
area_480, area_720 = 832 * 480, 1280 * 720
min_area, max_area = int(area_480 * 0.5), int(area_720 * 1.8)
if area < min_area or area > max_area:
size_label = "small" if area < min_area else "large"
raise ValueError(
"[WAN22_I2V_Image_Scaler_MXD] Input resolution "
f"{iw}x{ih} is too {size_label} for WAN 2.2 video buckets.\n"
"WAN 2.2 works best around these ranges:\n"
" • 480p tier ≈ 832×480 (or 480×832)\n"
" • 720p tier ≈ 1280×720 (or 720×1280)\n"
" • Squares: 624×624 or 720×720\n\n"
"Please use a source closer to 480p/720p, or first run it through "
"your WAN 2.2 workflow at one of those tiers. This check exists so "
"the workflow fails early here instead of deep inside WAN 2.2."
)
if area < min_area or area > max_area:
size_label = "small" if area < min_area else "large"
raise ValueError(
f"[WAN22_I2V_Image_Scaler_MXD] Input resolution {iw}x{ih} is too {size_label} for WAN 2.2 video buckets.\n"
"WAN 2.2 works best around:\n"
" • 480p tier ≈ 832×480 (or 480×832)\n"
" • 720p tier ≈ 1280×720 (or 720×1280)\n"
" • Squares: 624×624 or 720×720\n\n"
"Please use a source closer to 480p/720p, or first process it "
"through your WAN 2.2 workflow. This ensures extend runs without mismatch."
)
# fallback to Auto scaling
tier = "Auto"
# 3) Normal bucket selection + scaling
# --- Normal path (Auto / 480p / 720p) ---
bw, bh = self._pick_bucket(iw, ih, tier, crop_to_fit)
is_squareish = _is_squareish(iw, ih)
# Don't crop squares; just fit
if is_squareish:
crop_to_fit = False
if crop_to_fit:
# crop case: bucket dims rounded up to multiples of 16
bw, bh = _safe_hw(_ceil16(bw), _ceil16(bh))
out = _resize_then_center_crop(image, bw, bh)
else:
# fit case: bucket dims rounded down to multiples of 16
bw, bh = _safe_hw(_floor16(bw), _floor16(bh))
out, _, _ = _resize_fit_inside(image, bw, bh)
@@ -1883,7 +1777,7 @@ class Wan22FirstLastImageToVideoMXD(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="Wan22FirstLastImageToVideoMXD",
display_name="WAN 2.2 First+Last Image → Video MXD",
display_name="WAN 2.2 First&Last Image To Video MXD",
category="conditioning/video_models",
inputs=[
io.Conditioning.Input("positive"),