Make comfy_api optional & guard node registration

Wrap direct comfy_api imports in try/except and introduce HAVE_COMFY_API flags so the package can load without comfy_api present. Update __init__.py to dynamically and safely import node modules and merge NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS. Conditionally define and register nodes that depend on comfy_api (Qwen image-edit nodes, WAN 2.2 video nodes, Combine/Load/Save/Preview video nodes) so they are only available when comfy_api is present. Add helper to auto-add " MXD" aliases for node names and to merge display mappings. Bump package version to 1.6.9 and add diagnostic prints when comfy_api imports fail.
This commit is contained in:
Maxed-Out-99
2026-02-07 19:55:19 -08:00
parent dc3130bd21
commit 181e818f66
4 changed files with 586 additions and 513 deletions
+21 -20
View File
@@ -1,28 +1,29 @@
from .maxedoutnodes import (
NODE_CLASS_MAPPINGS as MXD_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as MXD_NODE_DISPLAY_NAME_MAPPINGS,
)
from .mediacomparers import (
NODE_CLASS_MAPPINGS as MEDIA_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as MEDIA_NODE_DISPLAY_NAME_MAPPINGS,
)
from .wan22nodes import (
NODE_CLASS_MAPPINGS as WAN22_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as WAN22_NODE_DISPLAY_NAME_MAPPINGS,
)
import importlib
WEB_DIRECTORY = "web"
# Merge both sets into one
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(MXD_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(MEDIA_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(WAN22_NODE_CLASS_MAPPINGS)
def _safe_import(module_name: str):
try:
return importlib.import_module(f".{module_name}", __name__)
except Exception as e:
print(f"[ComfyUI-MaxedOut] Failed to import '{module_name}': {e}")
return None
def _get_mappings(mod):
if mod is None:
return {}, {}
class_map = getattr(mod, "NODE_CLASS_MAPPINGS", {}) or {}
display_map = getattr(mod, "NODE_DISPLAY_NAME_MAPPINGS", {}) or {}
return class_map, display_map
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(MXD_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(MEDIA_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(WAN22_NODE_DISPLAY_NAME_MAPPINGS)
for _name in ("maxedoutnodes", "mediacomparers", "wan22nodes"):
_mod = _safe_import(_name)
_class_map, _display_map = _get_mappings(_mod)
NODE_CLASS_MAPPINGS.update(_class_map)
NODE_DISPLAY_NAME_MAPPINGS.update(_display_map)
__all__ = [
"NODE_CLASS_MAPPINGS",
+178 -148
View File
@@ -4,7 +4,13 @@ import torch.nn.functional as F
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
import numpy as np
from PIL import Image, ImageOps, ImageSequence, ImageFilter
from comfy_api.latest import io
try:
from comfy_api.latest import io
HAVE_COMFY_API = True
except Exception as _e:
io = None
HAVE_COMFY_API = False
print(f"[ComfyUI-MaxedOut] comfy_api not available in maxedoutnodes: {_e}")
########################################################################################################################
# Flux Empty Latent Image (SD3-compatible)
@@ -443,164 +449,163 @@ class PromptWithGuidance(ComfyNodeABC):
return (conditioning,)
########################################################################################################################
class QwenImageEditSingleMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="QwenImageEditSingleMXD",
display_name="Qwen Image Edit + Latent MXD",
category="MXD/conditioning",
description="Encode prompt/image and output a matching empty latent.",
inputs=[
io.Clip.Input("clip"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Vae.Input("vae", optional=True),
io.Image.Input("image", optional=True),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Conditioning.Output(),
io.Latent.Output(), # New Output
],
)
@classmethod
def execute(cls, clip, prompt, vae=None, image=None, batch_size=1) -> io.NodeOutput:
ref_latents = []
images_vl = []
llama_template = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
image_prompt = ""
# Default fallback size if no image is provided (1024x1024)
final_width, final_height = 1024, 1024
if image is not None:
samples = image.movedim(-1, 1)
# --- VISION SCALING (384px area) ---
total_vl = int(384 * 384)
scale_vl = math.sqrt(total_vl / (samples.shape[3] * samples.shape[2]))
width_vl = round(samples.shape[3] * scale_vl)
height_vl = round(samples.shape[2] * scale_vl)
s_vl = comfy.utils.common_upscale(samples, width_vl, height_vl, "area", "disabled")
images_vl.append(s_vl.movedim(1, -1))
# --- LATENT/VAE SCALING (1024px area) ---
total_lat = int(1024 * 1024)
scale_lat = math.sqrt(total_lat / (samples.shape[3] * samples.shape[2]))
# Calculate final dimensions to be multiples of 8
final_width = round(samples.shape[3] * scale_lat / 8.0) * 8
final_height = round(samples.shape[2] * scale_lat / 8.0) * 8
if vae is not None:
s_lat = comfy.utils.common_upscale(samples, final_width, final_height, "area", "disabled")
ref_latents.append(vae.encode(s_lat.movedim(1, -1)[:, :, :, :3]))
image_prompt += "Picture 1: <|vision_start|><|image_pad|><|vision_end|>"
# 1. Generate the Empty Latent (SD3 Style: 16 channels, 1/8th resolution)
# This replaces the need for the separate EmptySD3LatentImage node
latent_tensor = torch.zeros(
[batch_size, 16, final_height // 8, final_width // 8],
device=comfy.model_management.intermediate_device()
)
latent_output = {"samples": latent_tensor}
# 2. Process Conditioning
tokens = clip.tokenize(image_prompt + prompt, images=images_vl, llama_template=llama_template)
conditioning = clip.encode_from_tokens_scheduled(tokens)
if len(ref_latents) > 0:
conditioning = node_helpers.conditioning_set_values(
conditioning,
{"reference_latents": ref_latents},
append=True,
if HAVE_COMFY_API:
class QwenImageEditSingleMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="QwenImageEditSingleMXD",
display_name="Qwen Image Edit + Latent MXD",
category="MXD/conditioning",
description="Encode prompt/image and output a matching empty latent.",
inputs=[
io.Clip.Input("clip"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Vae.Input("vae", optional=True),
io.Image.Input("image", optional=True),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Conditioning.Output(),
io.Latent.Output(), # New Output
],
)
return io.NodeOutput(conditioning, latent_output)
@classmethod
def execute(cls, clip, prompt, vae=None, image=None, batch_size=1) -> io.NodeOutput:
ref_latents = []
images_vl = []
llama_template = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
image_prompt = ""
########################################################################################################################
class QwenImageEditTripleMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="QwenImageEditTripleMXD",
display_name="Qwen Image Edit Prompt MXD (Triple)",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Vae.Input("vae", optional=True),
io.Image.Input("image1", optional=True),
io.Image.Input("image2", optional=True),
io.Image.Input("image3", optional=True),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Conditioning.Output(),
io.Latent.Output(),
],
)
# Default fallback size if no image is provided (1024x1024)
final_width, final_height = 1024, 1024
@classmethod
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None, batch_size=1) -> io.NodeOutput:
ref_latents = []
images = [image1, image2, image3]
images_vl = []
llama_template = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
image_prompt = ""
# Default fallback
latent_width = 1024
latent_height = 1024
for i, image in enumerate(images):
if image is not None:
samples = image.movedim(-1, 1)
# 1. VL Model Scaling (LLM Vision)
# --- VISION SCALING (384px area) ---
total_vl = int(384 * 384)
scale_by_vl = math.sqrt(total_vl / (samples.shape[3] * samples.shape[2]))
width_vl = round(samples.shape[3] * scale_by_vl)
height_vl = round(samples.shape[2] * scale_by_vl)
scale_vl = math.sqrt(total_vl / (samples.shape[3] * samples.shape[2]))
width_vl = round(samples.shape[3] * scale_vl)
height_vl = round(samples.shape[2] * scale_vl)
s_vl = comfy.utils.common_upscale(samples, width_vl, height_vl, "area", "disabled")
images_vl.append(s_vl.movedim(1, -1))
# 2. VAE Scaling (Synchronized to 16-step for SD3 compatibility)
# --- LATENT/VAE SCALING (1024px area) ---
total_lat = int(1024 * 1024)
scale_lat = math.sqrt(total_lat / (samples.shape[3] * samples.shape[2]))
# Calculate final dimensions to be multiples of 8
final_width = round(samples.shape[3] * scale_lat / 8.0) * 8
final_height = round(samples.shape[2] * scale_lat / 8.0) * 8
if vae is not None:
total_ref = int(1024 * 1024)
scale_by_ref = math.sqrt(total_ref / (samples.shape[3] * samples.shape[2]))
# Pixels as multiple of 16 ensures Latent (Pixels/8) is always even
width_ref = round(samples.shape[3] * scale_by_ref / 16.0) * 16
height_ref = round(samples.shape[2] * scale_by_ref / 16.0) * 16
s_lat = comfy.utils.common_upscale(samples, final_width, final_height, "area", "disabled")
ref_latents.append(vae.encode(s_lat.movedim(1, -1)[:, :, :, :3]))
if i == 0:
latent_width = width_ref
latent_height = height_ref
image_prompt += "Picture 1: <|vision_start|><|image_pad|><|vision_end|>"
s_ref = comfy.utils.common_upscale(samples, width_ref, height_ref, "area", "disabled")
ref_latents.append(vae.encode(s_ref.movedim(1, -1)[:, :, :, :3]))
# 1. Generate the Empty Latent (SD3 Style: 16 channels, 1/8th resolution)
# This replaces the need for the separate EmptySD3LatentImage node
latent_tensor = torch.zeros(
[batch_size, 16, final_height // 8, final_width // 8],
device=comfy.model_management.intermediate_device()
)
latent_output = {"samples": latent_tensor}
image_prompt += "Picture {}: <|vision_start|><|image_pad|><|vision_end|>".format(i + 1)
# 2. Process Conditioning
tokens = clip.tokenize(image_prompt + prompt, images=images_vl, llama_template=llama_template)
conditioning = clip.encode_from_tokens_scheduled(tokens)
if len(ref_latents) > 0:
conditioning = node_helpers.conditioning_set_values(
conditioning,
{"reference_latents": ref_latents},
append=True,
)
return io.NodeOutput(conditioning, latent_output)
########################################################################################################################
class QwenImageEditTripleMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="QwenImageEditTripleMXD",
display_name="Qwen Image Edit Prompt MXD (Triple)",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Vae.Input("vae", optional=True),
io.Image.Input("image1", optional=True),
io.Image.Input("image2", optional=True),
io.Image.Input("image3", optional=True),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Conditioning.Output(),
io.Latent.Output(),
],
)
@classmethod
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None, batch_size=1) -> io.NodeOutput:
ref_latents = []
images = [image1, image2, image3]
images_vl = []
llama_template = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
image_prompt = ""
# Default fallback
latent_width = 1024
latent_height = 1024
for i, image in enumerate(images):
if image is not None:
samples = image.movedim(-1, 1)
# 1. VL Model Scaling (LLM Vision)
total_vl = int(384 * 384)
scale_by_vl = math.sqrt(total_vl / (samples.shape[3] * samples.shape[2]))
width_vl = round(samples.shape[3] * scale_by_vl)
height_vl = round(samples.shape[2] * scale_by_vl)
s_vl = comfy.utils.common_upscale(samples, width_vl, height_vl, "area", "disabled")
images_vl.append(s_vl.movedim(1, -1))
# 2. VAE Scaling (Synchronized to 16-step for SD3 compatibility)
if vae is not None:
total_ref = int(1024 * 1024)
scale_by_ref = math.sqrt(total_ref / (samples.shape[3] * samples.shape[2]))
# Pixels as multiple of 16 ensures Latent (Pixels/8) is always even
width_ref = round(samples.shape[3] * scale_by_ref / 16.0) * 16
height_ref = round(samples.shape[2] * scale_by_ref / 16.0) * 16
if i == 0:
latent_width = width_ref
latent_height = height_ref
s_ref = comfy.utils.common_upscale(samples, width_ref, height_ref, "area", "disabled")
ref_latents.append(vae.encode(s_ref.movedim(1, -1)[:, :, :, :3]))
image_prompt += "Picture {}: <|vision_start|><|image_pad|><|vision_end|>".format(i + 1)
# Process tokens and conditioning
tokens = clip.tokenize(image_prompt + prompt, images=images_vl, llama_template=llama_template)
conditioning = clip.encode_from_tokens_scheduled(tokens)
if len(ref_latents) > 0:
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": ref_latents}, append=True)
# Create Output Latent
latent = torch.zeros([batch_size, 16, latent_height // 8, latent_width // 8], device=comfy.model_management.intermediate_device())
# FIXED: Return outputs positionally to match the schema defined above
# Output 1: Conditioning, Output 2: Latent Dictionary
return io.NodeOutput(conditioning, {"samples": latent})
# Process tokens and conditioning
tokens = clip.tokenize(image_prompt + prompt, images=images_vl, llama_template=llama_template)
conditioning = clip.encode_from_tokens_scheduled(tokens)
if len(ref_latents) > 0:
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": ref_latents}, append=True)
# Create Output Latent
latent = torch.zeros([batch_size, 16, latent_height // 8, latent_width // 8], device=comfy.model_management.intermediate_device())
# FIXED: Return outputs positionally to match the schema defined above
# Output 1: Conditioning, Output 2: Latent Dictionary
return io.NodeOutput(conditioning, {"samples": latent})
########################################################################################################################
class FluxResolutionMatcher:
DESCRIPTION = """Match the closest Flux resolution and orientation for the input image."""
@@ -1408,8 +1413,6 @@ NODE_CLASS_MAPPINGS = {
"Image Scale To Total Pixels (SDXL Safe)": SDXLImageScaleToTotalPixelsSafe,
"Flux Image Scale To Total Pixels (Flux Safe)": FluxImageScaleToTotalPixelsSafe,
"Prompt With Guidance (Flux)": PromptWithGuidance,
"QwenImageEditSingleMXD": QwenImageEditSingleMXD,
"QwenImageEditTripleMXD": QwenImageEditTripleMXD,
"FluxResolutionMatcher": FluxResolutionMatcher,
"SDXLResolutionMatcher": SDXLResolutionMatcher,
"LatentHalfMasks": LatentHalfMasks,
@@ -1424,6 +1427,12 @@ NODE_CLASS_MAPPINGS = {
"Dummy Node MXD": DummyNodeMXD,
}
if HAVE_COMFY_API:
NODE_CLASS_MAPPINGS.update({
"QwenImageEditSingleMXD": QwenImageEditSingleMXD,
"QwenImageEditTripleMXD": QwenImageEditTripleMXD,
})
NODE_DISPLAY_NAME_MAPPINGS = {
"Flux Empty Latent Image": "Flux Empty Latent Image MXD",
"Flux 2 Empty Latent Image": "Flux 2 Empty Latent Image MXD",
@@ -1432,8 +1441,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Image Scale To Total Pixels (SDXL Safe)": "Scale SDXL Image MXD",
"Flux Image Scale To Total Pixels (Flux Safe)": "Scale Flux Image MXD",
"Prompt With Guidance (Flux)": "Prompt with Flux Guidance MXD",
"QwenImageEditSingleMXD": "Qwen Image Edit + Latent MXD",
"QwenImageEditTripleMXD": "Qwen Image Edit Prompt MXD (Triple)",
"FluxResolutionMatcher": "Flux Resolution Matcher MXD",
"SDXLResolutionMatcher": "SDXL Resolution Matcher MXD",
"LatentHalfMasks": "Latent to L/R Masks MXD",
@@ -1447,3 +1454,26 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Save Image MXD": "Save Image MXD",
"Dummy Node MXD": "Dummy Node MXD",
}
if HAVE_COMFY_API:
NODE_DISPLAY_NAME_MAPPINGS.update({
"QwenImageEditSingleMXD": "Qwen Image Edit + Latent MXD",
"QwenImageEditTripleMXD": "Qwen Image Edit Prompt MXD (Triple)",
})
def _add_mxd_aliases(class_map, display_map):
alias_sources = {}
for key in list(class_map.keys()):
if "MXD" in key.upper():
continue
alias = f"{key} MXD"
if alias in class_map:
continue
class_map[alias] = class_map[key]
alias_sources[alias] = key
for alias, source in alias_sources.items():
if alias not in display_map:
display_map[alias] = display_map.get(source, alias)
return alias_sources
_add_mxd_aliases(NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS)
+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.6.8"
version = "1.6.9"
license = {file = "LICENSE"}
# classifiers = [
# # For OS-independent nodes (works on all operating systems)
+386 -344
View File
@@ -13,10 +13,23 @@ from nodes import KSamplerAdvanced
import node_helpers, nodes
# Comfy API
from comfy_api.latest import io, ui
from comfy_api.input import VideoInput
from comfy_api.input_impl import VideoFromFile, VideoFromComponents
from comfy_api.util import VideoComponents, VideoContainer, VideoCodec
try:
from comfy_api.latest import io, ui
from comfy_api.input import VideoInput
from comfy_api.input_impl import VideoFromFile, VideoFromComponents
from comfy_api.util import VideoComponents, VideoContainer, VideoCodec
HAVE_COMFY_API = True
except Exception as _e:
io = None
ui = None
VideoInput = None
VideoFromFile = None
VideoFromComponents = None
VideoComponents = None
VideoContainer = None
VideoCodec = None
HAVE_COMFY_API = False
print(f"[ComfyUI-MaxedOut] comfy_api not available in wan22nodes: {_e}")
from server import PromptServer
from aiohttp import web
@@ -1104,72 +1117,72 @@ class wan22EmptyHunyuanLatentVideoMXD:
)
return ({"samples": latent},)
# ---------- WAN 2.2 Image to Video (no scaling; expects pre-sized input) ----------
if HAVE_COMFY_API:
class Wan22ImageToVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Wan22ImageToVideoMXD",
display_name="WAN 2.2 Image to Video MXD",
category="conditioning/video_models",
description="WAN 2.2 image to video without scaling or CLIP vision.",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("length", default=81, min=1, max=16384, step=4),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Image.Input("start_image", optional=False),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
class Wan22ImageToVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Wan22ImageToVideoMXD",
display_name="WAN 2.2 Image to Video MXD",
category="conditioning/video_models",
description="WAN 2.2 image to video without scaling or CLIP vision.",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("length", default=81, min=1, max=16384, step=4),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Image.Input("start_image", optional=False),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
@classmethod
def execute(cls, positive, negative, vae, length, batch_size, start_image) -> io.NodeOutput:
if start_image is None:
raise ValueError("start_image must be provided (already pre-sized).")
@classmethod
def execute(cls, positive, negative, vae, length, batch_size, start_image) -> io.NodeOutput:
if start_image is None:
raise ValueError("start_image must be provided (already pre-sized).")
frames_in, ih, iw, ch = start_image.shape
frames_used = min(frames_in, length)
t = ((length - 1) // 4) + 1
frames_in, ih, iw, ch = start_image.shape
frames_used = min(frames_in, length)
t = ((length - 1) // 4) + 1
latent = torch.zeros(
[batch_size, 16, t, ih // 8, iw // 8],
device=comfy.model_management.intermediate_device()
)
latent = torch.zeros(
[batch_size, 16, t, ih // 8, iw // 8],
device=comfy.model_management.intermediate_device()
)
# create placeholder image tensor
image = torch.ones(
(length, ih, iw, ch),
device=start_image.device,
dtype=start_image.dtype
) * 0.5
image[:frames_used] = start_image[:frames_used]
# create placeholder image tensor
image = torch.ones(
(length, ih, iw, ch),
device=start_image.device,
dtype=start_image.dtype
) * 0.5
image[:frames_used] = start_image[:frames_used]
# encode using VAE
concat_latent_image = vae.encode(image[:, :, :, :3])
# encode using VAE
concat_latent_image = vae.encode(image[:, :, :, :3])
# mask zeros out the frames used
mask = torch.ones(
(1, 1, t, concat_latent_image.shape[-2], concat_latent_image.shape[-1]),
device=image.device,
dtype=image.dtype
)
mask[:, :, :((frames_used - 1) // 4) + 1] = 0.0
# mask zeros out the frames used
mask = torch.ones(
(1, 1, t, concat_latent_image.shape[-2], concat_latent_image.shape[-1]),
device=image.device,
dtype=image.dtype
)
mask[:, :, :((frames_used - 1) // 4) + 1] = 0.0
positive = node_helpers.conditioning_set_values(
positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
)
negative = node_helpers.conditioning_set_values(
negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
)
positive = node_helpers.conditioning_set_values(
positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
)
negative = node_helpers.conditioning_set_values(
negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
)
out_latent = {"samples": latent}
return io.NodeOutput(positive, negative, out_latent)
out_latent = {"samples": latent}
return io.NodeOutput(positive, negative, out_latent)
# ---- Canonical WAN 2.2 buckets ----
BUCKETS_480 = [(832,480), (480,832), (624,624)] # 16:9, 9:16, 1:1
@@ -1449,260 +1462,262 @@ class Frames_Remove_From_Start_MXD:
return (frames_after,)
class CombineVideos_MXD:
"""
Combine two VIDEO inputs end-to-end (sequentially).
"""
if HAVE_COMFY_API:
class CombineVideos_MXD:
"""
Combine two VIDEO inputs end-to-end (sequentially).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"front_video": ("VIDEO", {"tooltip": "The first video (plays first)"}),
"back_video": ("VIDEO", {"tooltip": "The second video (plays after the first)"}),
},
}
RETURN_TYPES = ("VIDEO",)
RETURN_NAMES = ("video",)
FUNCTION = "combine"
CATEGORY = "MXD/video"
def combine(self, front_video, back_video):
comp_a = front_video.get_components()
comp_b = back_video.get_components()
# Check frame rate consistency
if comp_a.frame_rate != comp_b.frame_rate:
raise ValueError(f"FPS mismatch: {comp_a.frame_rate} vs {comp_b.frame_rate}")
# ✅ Correct way: concatenate frame tensors along batch/time dimension (dim=0)
frames_a = torch.stack(comp_a.images) if isinstance(comp_a.images, list) else comp_a.images
frames_b = torch.stack(comp_b.images) if isinstance(comp_b.images, list) else comp_b.images
combined_images = torch.cat([frames_a, frames_b], dim=0)
# ✅ Combine audio sequentially
combined_audio = None
if comp_a.audio is not None or comp_b.audio is not None:
audio_a = comp_a.audio if comp_a.audio is not None else torch.zeros((1, 0))
audio_b = comp_b.audio if comp_b.audio is not None else torch.zeros((1, 0))
combined_audio = torch.cat([audio_a, audio_b], dim=1)
combined_video = VideoFromComponents(
VideoComponents(
images=combined_images,
audio=combined_audio,
frame_rate=comp_a.frame_rate,
)
)
return (combined_video,)
# ---------- Load Video MXD (video-only picker with refresh) ----------
class LoadVideoMXD:
"""Load a video from /input with a refresh button (videos only)."""
CATEGORY = "image/video"
FUNCTION = "load"
RETURN_TYPES = ("VIDEO", "STRING")
RETURN_NAMES = ("video", "video_path")
TITLE = "Load Video MXD"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"file": ("COMBO", {
# Only allow video uploads in the picker
"video_upload": True,
# Custom route that returns ONLY videos in /input
"remote": {
"route": "/mxd/videos/input",
"refresh_button": True,
"control_after_refresh": "first",
},
}),
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"front_video": ("VIDEO", {"tooltip": "The first video (plays first)"}),
"back_video": ("VIDEO", {"tooltip": "The second video (plays after the first)"}),
},
}
}
# --- helpers --------------------------------------------------------------
RETURN_TYPES = ("VIDEO",)
RETURN_NAMES = ("video",)
FUNCTION = "combine"
CATEGORY = "MXD/video"
@staticmethod
def _resolve_video_path(file: str) -> str:
"""
Try to resolve `file` in a backwards-compatible way:
1. If it's an annotated path, let folder_paths handle it.
2. Otherwise treat it as relative to the input directory.
"""
# 1) Try annotated style (old workflows / uploads)
try:
return folder_paths.get_annotated_filepath(file)
except Exception:
pass
def combine(self, front_video, back_video):
comp_a = front_video.get_components()
comp_b = back_video.get_components()
# 2) Fall back to /input relative
base = folder_paths.get_input_directory()
candidate = os.path.join(base, file)
if os.path.isfile(candidate):
# Check frame rate consistency
if comp_a.frame_rate != comp_b.frame_rate:
raise ValueError(f"FPS mismatch: {comp_a.frame_rate} vs {comp_b.frame_rate}")
# ✅ Correct way: concatenate frame tensors along batch/time dimension (dim=0)
frames_a = torch.stack(comp_a.images) if isinstance(comp_a.images, list) else comp_a.images
frames_b = torch.stack(comp_b.images) if isinstance(comp_b.images, list) else comp_b.images
combined_images = torch.cat([frames_a, frames_b], dim=0)
# ✅ Combine audio sequentially
combined_audio = None
if comp_a.audio is not None or comp_b.audio is not None:
audio_a = comp_a.audio if comp_a.audio is not None else torch.zeros((1, 0))
audio_b = comp_b.audio if comp_b.audio is not None else torch.zeros((1, 0))
combined_audio = torch.cat([audio_a, audio_b], dim=1)
combined_video = VideoFromComponents(
VideoComponents(
images=combined_images,
audio=combined_audio,
frame_rate=comp_a.frame_rate,
)
)
return (combined_video,)
# ---------- Load Video MXD (video-only picker with refresh) ----------
class LoadVideoMXD:
"""Load a video from /input with a refresh button (videos only)."""
CATEGORY = "image/video"
FUNCTION = "load"
RETURN_TYPES = ("VIDEO", "STRING")
RETURN_NAMES = ("video", "video_path")
TITLE = "Load Video MXD"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"file": ("COMBO", {
# Only allow video uploads in the picker
"video_upload": True,
# Custom route that returns ONLY videos in /input
"remote": {
"route": "/mxd/videos/input",
"refresh_button": True,
"control_after_refresh": "first",
},
}),
}
}
# --- helpers --------------------------------------------------------------
@staticmethod
def _resolve_video_path(file: str) -> str:
"""
Try to resolve `file` in a backwards-compatible way:
1. If it's an annotated path, let folder_paths handle it.
2. Otherwise treat it as relative to the input directory.
"""
# 1) Try annotated style (old workflows / uploads)
try:
return folder_paths.get_annotated_filepath(file)
except Exception:
pass
# 2) Fall back to /input relative
base = folder_paths.get_input_directory()
candidate = os.path.join(base, file)
if os.path.isfile(candidate):
return candidate
# If all else fails, just return what we got (will error later)
return candidate
# If all else fails, just return what we got (will error later)
return candidate
@staticmethod
def _is_video_file(path: str) -> bool:
_, ext = os.path.splitext(path)
return ext.lower() in VIDEO_EXTS
@staticmethod
def _is_video_file(path: str) -> bool:
_, ext = os.path.splitext(path)
return ext.lower() in VIDEO_EXTS
# --- main function --------------------------------------------------------
# --- main function --------------------------------------------------------
def load(self, file: str):
video_path = self._resolve_video_path(file)
def load(self, file: str):
video_path = self._resolve_video_path(file)
if not os.path.isfile(video_path):
raise FileNotFoundError(f"[LoadVideoMXD] File not found: {video_path}")
if not os.path.isfile(video_path):
raise FileNotFoundError(f"[LoadVideoMXD] File not found: {video_path}")
if not self._is_video_file(video_path):
raise ValueError(f"[LoadVideoMXD] Not a video file: {video_path}")
if not self._is_video_file(video_path):
raise ValueError(f"[LoadVideoMXD] Not a video file: {video_path}")
print(f"[LoadVideoMXD] Loaded exactly: {video_path}")
return (VideoFromFile(video_path), video_path)
print(f"[LoadVideoMXD] Loaded exactly: {video_path}")
return (VideoFromFile(video_path), video_path)
# --- nice-to-haves --------------------------------------------------------
# --- nice-to-haves --------------------------------------------------------
@classmethod
def IS_CHANGED(cls, file: str):
try:
p = cls._resolve_video_path(file)
return os.path.getmtime(p)
except Exception:
return 0
@classmethod
def IS_CHANGED(cls, file: str):
try:
p = cls._resolve_video_path(file)
return os.path.getmtime(p)
except Exception:
return 0
@classmethod
def VALIDATE_INPUTS(cls, file: str):
# First, try the annotated path (for backwards compat)
if folder_paths.exists_annotated_filepath(file):
resolved = folder_paths.get_annotated_filepath(file)
if not cls._is_video_file(resolved):
return f"This node only accepts video files ({', '.join(sorted(VIDEO_EXTS))})."
return True
@classmethod
def VALIDATE_INPUTS(cls, file: str):
# First, try the annotated path (for backwards compat)
if folder_paths.exists_annotated_filepath(file):
resolved = folder_paths.get_annotated_filepath(file)
if not cls._is_video_file(resolved):
return f"This node only accepts video files ({', '.join(sorted(VIDEO_EXTS))})."
return True
# Then, try treating it as /input-relative
base = folder_paths.get_input_directory()
candidate = os.path.join(base, file)
if os.path.isfile(candidate):
if not cls._is_video_file(candidate):
return f"This node only accepts video files ({', '.join(sorted(VIDEO_EXTS))})."
return True
# Then, try treating it as /input-relative
base = folder_paths.get_input_directory()
candidate = os.path.join(base, file)
if os.path.isfile(candidate):
if not cls._is_video_file(candidate):
return f"This node only accepts video files ({', '.join(sorted(VIDEO_EXTS))})."
return True
return f"Invalid video file: {file}"
return f"Invalid video file: {file}"
# ---------- Save Video MXD (auto-increment clean filenames) ----------
class SaveVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveVideoMXD",
display_name="Save Video MXD",
category="image/video",
description="Save a new version next to the original with clean counters.",
inputs=[
io.Video.Input("video"),
io.String.Input("video_path"),
io.Combo.Input("save_to_outputs", options=[False, True], default=False),
io.Combo.Input("format", options=VideoContainer.as_input(), default="auto"),
io.Combo.Input("codec", options=VideoCodec.as_input(), default="auto"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
# ---------- Save Video MXD (auto-increment clean filenames) ----------
class SaveVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveVideoMXD",
display_name="Save Video MXD",
category="image/video",
description="Save a new version next to the original with clean counters.",
inputs=[
io.Video.Input("video"),
io.String.Input("video_path"),
io.Combo.Input("save_to_outputs", options=[False, True], default=False),
io.Combo.Input("format", options=VideoContainer.as_input(), default="auto"),
io.Combo.Input("codec", options=VideoCodec.as_input(), default="auto"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, video: VideoInput, video_path: str, save_to_outputs: bool, format: str, codec: str):
base_dir, base_filename = os.path.split(video_path)
base_name, ext = os.path.splitext(base_filename)
@classmethod
def execute(cls, video: VideoInput, video_path: str, save_to_outputs: bool, format: str, codec: str):
base_dir, base_filename = os.path.split(video_path)
base_name, ext = os.path.splitext(base_filename)
# 🧹 Clean trailing counters like "__001__002" → remove them all
base_clean = re.sub(r'(__\d+)+$', '', base_name)
# 🧹 Clean trailing counters like "__001__002" → remove them all
base_clean = re.sub(r'(__\d+)+$', '', base_name)
# 🧮 Find the next available counter
pattern = re.compile(rf"^{re.escape(base_clean)}__(\d+){re.escape(ext)}$")
existing = [
int(m.group(1))
for f in os.listdir(base_dir)
if (m := pattern.match(f))
]
next_counter = max(existing, default=0) + 1
# 🧮 Find the next available counter
pattern = re.compile(rf"^{re.escape(base_clean)}__(\d+){re.escape(ext)}$")
existing = [
int(m.group(1))
for f in os.listdir(base_dir)
if (m := pattern.match(f))
]
next_counter = max(existing, default=0) + 1
new_filename = f"{base_clean}__{next_counter:03d}{ext}"
save_path = os.path.join(base_dir, new_filename)
new_filename = f"{base_clean}__{next_counter:03d}{ext}"
save_path = os.path.join(base_dir, new_filename)
# 💾 Metadata
saved_metadata = None
if not args.disable_metadata:
metadata = {}
if cls.hidden.extra_pnginfo is not None:
metadata.update(cls.hidden.extra_pnginfo)
if cls.hidden.prompt is not None:
metadata["prompt"] = cls.hidden.prompt
if metadata:
saved_metadata = metadata
# 💾 Metadata
saved_metadata = None
if not args.disable_metadata:
metadata = {}
if cls.hidden.extra_pnginfo is not None:
metadata.update(cls.hidden.extra_pnginfo)
if cls.hidden.prompt is not None:
metadata["prompt"] = cls.hidden.prompt
if metadata:
saved_metadata = metadata
# 🚀 Save main copy
video.save_to(save_path, format=format, codec=codec, metadata=saved_metadata)
# 🚀 Save main copy
video.save_to(save_path, format=format, codec=codec, metadata=saved_metadata)
# 🪣 Optional copy to outputs folder
if save_to_outputs:
out_dir = folder_paths.get_output_directory()
# 🪣 Optional copy to outputs folder
if save_to_outputs:
out_dir = folder_paths.get_output_directory()
os.makedirs(out_dir, exist_ok=True)
alt_path = os.path.join(out_dir, new_filename)
video.save_to(alt_path, format=format, codec=codec, metadata=saved_metadata)
print(f"[SaveVideoMXD] Also saved copy to outputs: {alt_path}")
print(f"[SaveVideoMXD] Saved clean new version: {new_filename}")
rel_folder = os.path.relpath(base_dir, folder_paths.get_output_directory())
return io.NodeOutput(
ui=ui.PreviewVideo([
ui.SavedResult(new_filename, rel_folder, io.FolderType.output)
])
)
class PreviewVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PreviewVideoMXD",
display_name="Preview Video MXD",
category="image/video",
description="Preview a video without saving output.",
inputs=[
io.Video.Input("input_video", tooltip="Video to preview."),
],
outputs=[
io.Video.Output("output_video", tooltip="Passes the same video forward."),
],
)
@classmethod
def execute(cls, input_video: VideoInput):
# Save a temporary H264 file so ComfyUI has something to preview
out_dir = os.path.join(folder_paths.get_output_directory(), "previews")
os.makedirs(out_dir, exist_ok=True)
alt_path = os.path.join(out_dir, new_filename)
video.save_to(alt_path, format=format, codec=codec, metadata=saved_metadata)
print(f"[SaveVideoMXD] Also saved copy to outputs: {alt_path}")
print(f"[SaveVideoMXD] Saved clean new version: {new_filename}")
preview_path = os.path.join(out_dir, "preview_temp.mp4")
input_video.save_to(preview_path, format="mp4", codec="h264")
rel_folder = os.path.relpath(base_dir, folder_paths.get_output_directory())
return io.NodeOutput(
ui=ui.PreviewVideo([
ui.SavedResult(new_filename, rel_folder, io.FolderType.output)
])
)
# ✅ Return the raw video object (not a tuple)
return io.NodeOutput(
input_video,
ui=ui.PreviewVideo([
ui.SavedResult("preview_temp.mp4", "previews", io.FolderType.output)
])
)
class PreviewVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PreviewVideoMXD",
display_name="Preview Video MXD",
category="image/video",
description="Preview a video without saving output.",
inputs=[
io.Video.Input("input_video", tooltip="Video to preview."),
],
outputs=[
io.Video.Output("output_video", tooltip="Passes the same video forward."),
],
)
@classmethod
def execute(cls, input_video: VideoInput):
# Save a temporary H264 file so ComfyUI has something to preview
out_dir = os.path.join(folder_paths.get_output_directory(), "previews")
os.makedirs(out_dir, exist_ok=True)
preview_path = os.path.join(out_dir, "preview_temp.mp4")
input_video.save_to(preview_path, format="mp4", codec="h264")
# ✅ Return the raw video object (not a tuple)
return io.NodeOutput(
input_video,
ui=ui.PreviewVideo([
ui.SavedResult("preview_temp.mp4", "previews", io.FolderType.output)
])
)
class GroupVideoFramesMXD:
CATEGORY = "MXD/Video"
@@ -1754,60 +1769,62 @@ class GroupVideoFramesMXD:
print(f"[GroupVideoFramesMXD] Split {total} frames into {len(grouped_tensors)} groups of up to {group_size}.")
return (grouped_tensors,)
class Wan22FirstLastImageToVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Wan22FirstLastImageToVideoMXD",
display_name="WAN 2.2 First&Last Image To Video MXD",
category="conditioning/video_models",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("length", default=81, min=1, max=nodes.MAX_RESOLUTION, step=4),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Image.Input("start_image", optional=True),
io.Image.Input("end_image", optional=True),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
if HAVE_COMFY_API:
class Wan22FirstLastImageToVideoMXD(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Wan22FirstLastImageToVideoMXD",
display_name="WAN 2.2 First&Last Image To Video MXD",
category="conditioning/video_models",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("length", default=81, min=1, max=nodes.MAX_RESOLUTION, step=4),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Image.Input("start_image", optional=True),
io.Image.Input("end_image", optional=True),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
@classmethod
def execute(cls, positive, negative, vae, length, batch_size, start_image=None, end_image=None) -> io.NodeOutput:
spacial_scale = vae.spacial_compression_encode()
@classmethod
def execute(cls, positive, negative, vae, length, batch_size, start_image=None, end_image=None) -> io.NodeOutput:
spacial_scale = vae.spacial_compression_encode()
# Assume incoming images are already pre-sized by upstream nodes.
height, width = start_image.shape[1], start_image.shape[2] if start_image is not None else (vae.latent_channels * spacial_scale, vae.latent_channels * spacial_scale)
# Assume incoming images are already pre-sized by upstream nodes.
height, width = start_image.shape[1], start_image.shape[2] if start_image is not None else (vae.latent_channels * spacial_scale, vae.latent_channels * spacial_scale)
latent = torch.zeros(
[batch_size, vae.latent_channels, ((length - 1) // 4) + 1, height // spacial_scale, width // spacial_scale],
device=comfy.model_management.intermediate_device()
)
latent = torch.zeros(
[batch_size, vae.latent_channels, ((length - 1) // 4) + 1, height // spacial_scale, width // spacial_scale],
device=comfy.model_management.intermediate_device()
)
image = torch.ones((length, height, width, 3)) * 0.5
mask = torch.ones((1, 1, latent.shape[2] * 4, latent.shape[-2], latent.shape[-1]))
image = torch.ones((length, height, width, 3)) * 0.5
mask = torch.ones((1, 1, latent.shape[2] * 4, latent.shape[-2], latent.shape[-1]))
if start_image is not None:
image[:start_image.shape[0]] = start_image
mask[:, :, :start_image.shape[0] + 3] = 0.0
if start_image is not None:
image[:start_image.shape[0]] = start_image
mask[:, :, :start_image.shape[0] + 3] = 0.0
if end_image is not None:
image[-end_image.shape[0]:] = end_image
mask[:, :, -end_image.shape[0]:] = 0.0
if end_image is not None:
image[-end_image.shape[0]:] = end_image
mask[:, :, -end_image.shape[0]:] = 0.0
concat_latent_image = vae.encode(image[:, :, :, :3])
mask = mask.view(1, mask.shape[2] // 4, 4, mask.shape[3], mask.shape[4]).transpose(1, 2)
concat_latent_image = vae.encode(image[:, :, :, :3])
mask = mask.view(1, mask.shape[2] // 4, 4, mask.shape[3], mask.shape[4]).transpose(1, 2)
positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask})
out_latent = {"samples": latent}
return io.NodeOutput(positive, negative, out_latent)
out_latent = {"samples": latent}
return io.NodeOutput(positive, negative, out_latent)
# ---------- Node registration ----------
NODE_CLASS_MAPPINGS = {
@@ -1819,18 +1836,22 @@ NODE_CLASS_MAPPINGS = {
"SaveLatent_I2V_MXD": SaveLatent_I2V_MXD,
"LoadLatent_I2V_MXD": LoadLatent_I2V_MXD,
"LoadLatents_FromFolder_I2V_MXD": LoadLatents_FromFolder_I2V_MXD,
"Wan22ImageToVideoMXD": Wan22ImageToVideoMXD,
"WAN22_I2V_Image_Scaler_MXD": WAN22_I2V_Image_Scaler_MXD,
"Frames_Remove_From_Start_MXD": Frames_Remove_From_Start_MXD,
"CombineVideos_MXD": CombineVideos_MXD,
"LoadVideoMXD": LoadVideoMXD,
"SaveVideoMXD": SaveVideoMXD,
"PreviewVideoMXD": PreviewVideoMXD,
"GroupVideoFramesMXD": GroupVideoFramesMXD,
"Wan22FirstLastImageToVideoMXD": Wan22FirstLastImageToVideoMXD,
"Frames_Select_StartEnd_MXD": Frames_Select_StartEnd_MXD,
}
if HAVE_COMFY_API:
NODE_CLASS_MAPPINGS.update({
"Wan22ImageToVideoMXD": Wan22ImageToVideoMXD,
"CombineVideos_MXD": CombineVideos_MXD,
"LoadVideoMXD": LoadVideoMXD,
"SaveVideoMXD": SaveVideoMXD,
"PreviewVideoMXD": PreviewVideoMXD,
"Wan22FirstLastImageToVideoMXD": Wan22FirstLastImageToVideoMXD,
})
NODE_DISPLAY_NAME_MAPPINGS = {
"SaveLatentMXD": "Save Latent MXD",
"LoadLatent_WithParams": "Load Latent MXD",
@@ -1840,14 +1861,35 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SaveLatent_I2V_MXD": "Save Latent I2V MXD",
"LoadLatent_I2V_MXD": "Load Latent I2V MXD",
"LoadLatents_FromFolder_I2V_MXD": "Load Latent Batch I2V MXD",
"Wan22ImageToVideoMXD": "Wan 2.2 Image to Video MXD",
"WAN22_I2V_Image_Scaler_MXD": "Image Scaler Wan 2.2 I2V MXD",
"Frames_Remove_From_Start_MXD": "Remove Frames From Start MXD",
"CombineVideos_MXD": "Combine Videos MXD",
"LoadVideoMXD": "Load Video MXD",
"SaveVideoMXD": "Save Video MXD",
"PreviewVideoMXD": "Preview Video MXD",
"GroupVideoFramesMXD": "Group Video Frames MXD",
"Wan22FirstLastImageToVideoMXD": "Wan 2.2 I2V First & Last Frame MXD",
"Frames_Select_StartEnd_MXD": "Select Frames MXD",
}
if HAVE_COMFY_API:
NODE_DISPLAY_NAME_MAPPINGS.update({
"Wan22ImageToVideoMXD": "Wan 2.2 Image to Video MXD",
"CombineVideos_MXD": "Combine Videos MXD",
"LoadVideoMXD": "Load Video MXD",
"SaveVideoMXD": "Save Video MXD",
"PreviewVideoMXD": "Preview Video MXD",
"Wan22FirstLastImageToVideoMXD": "Wan 2.2 I2V First & Last Frame MXD",
})
def _add_mxd_aliases(class_map, display_map):
alias_sources = {}
for key in list(class_map.keys()):
if "MXD" in key.upper():
continue
alias = f"{key} MXD"
if alias in class_map:
continue
class_map[alias] = class_map[key]
alias_sources[alias] = key
for alias, source in alias_sources.items():
if alias not in display_map:
display_map[alias] = display_map.get(source, alias)
return alias_sources
_add_mxd_aliases(NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS)