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:
+21
-20
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user