Merge branch 'v3-definition' into v3-definition-wip

This commit is contained in:
Jedrzej Kosinski 2025-07-25 20:53:33 -07:00
commit 320f4be792
63 changed files with 4660 additions and 1473 deletions

View File

@ -52,6 +52,6 @@ class EmptyAceStepLatentAudio(io.ComfyNode):
NODES_LIST: list[type[io.ComfyNode]] = [
TextEncodeAceStepAudio,
EmptyAceStepLatentAudio,
TextEncodeAceStepAudio,
]

View File

@ -122,7 +122,7 @@ class SamplerEulerCFGpp(io.ComfyNode):
return io.NodeOutput(sampler)
NODES_LIST = [
SamplerLCMUpscale,
NODES_LIST: list[type[io.ComfyNode]] = [
SamplerEulerCFGpp,
SamplerLCMUpscale,
]

View File

@ -5,6 +5,18 @@ import torch
from comfy_api.latest import io
def loglinear_interp(t_steps, num_steps):
"""Performs log-linear interpolation of a given array of decreasing numbers."""
xs = np.linspace(0, 1, len(t_steps))
ys = np.log(t_steps[::-1])
new_xs = np.linspace(0, 1, num_steps)
new_ys = np.interp(new_xs, xs, ys)
return np.exp(new_ys)[::-1].copy()
NOISE_LEVELS = {
"SD1": [
14.6146412293,
@ -36,17 +48,6 @@ NOISE_LEVELS = {
}
def loglinear_interp(t_steps, num_steps):
"""Performs log-linear interpolation of a given array of decreasing numbers."""
xs = np.linspace(0, 1, len(t_steps))
ys = np.log(t_steps[::-1])
new_xs = np.linspace(0, 1, num_steps)
new_ys = np.interp(new_xs, xs, ys)
return np.exp(new_ys)[::-1].copy()
class AlignYourStepsScheduler(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
@ -78,6 +79,6 @@ class AlignYourStepsScheduler(io.ComfyNode):
return io.NodeOutput(torch.FloatTensor(sigmas))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
AlignYourStepsScheduler,
]

View File

@ -93,6 +93,6 @@ class APG(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
APG,
]

View File

@ -131,9 +131,9 @@ class UNetTemporalAttentionMultiply(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
UNetSelfAttentionMultiply,
UNetCrossAttentionMultiply,
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPAttentionMultiply,
UNetCrossAttentionMultiply,
UNetSelfAttentionMultiply,
UNetTemporalAttentionMultiply,
]

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import hashlib
import os
import av
import torch
import torchaudio
@ -12,6 +13,28 @@ import node_helpers
from comfy_api.latest import io, ui
class EmptyLatentAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptyLatentAudio_V3",
category="latent/audio",
inputs=[
io.Float.Input("seconds", default=47.6, min=1.0, max=1000.0, step=0.1),
io.Int.Input(
"batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."
),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, seconds, batch_size) -> io.NodeOutput:
length = round((seconds * 44100 / 2048) / 2) * 2
latent = torch.zeros([batch_size, 64, length], device=comfy.model_management.intermediate_device())
return io.NodeOutput({"samples": latent, "type": "audio"})
class ConditioningStableAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -42,83 +65,71 @@ class ConditioningStableAudio(io.ComfyNode):
)
class EmptyLatentAudio(io.ComfyNode):
class VAEEncodeAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptyLatentAudio_V3",
node_id="VAEEncodeAudio_V3",
category="latent/audio",
inputs=[
io.Float.Input("seconds", default=47.6, min=1.0, max=1000.0, step=0.1),
io.Int.Input(
id="batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch."
),
io.Audio.Input("audio"),
io.Vae.Input("vae"),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, seconds, batch_size) -> io.NodeOutput:
length = round((seconds * 44100 / 2048) / 2) * 2
latent = torch.zeros([batch_size, 64, length], device=comfy.model_management.intermediate_device())
return io.NodeOutput({"samples": latent, "type": "audio"})
def execute(cls, vae, audio) -> io.NodeOutput:
sample_rate = audio["sample_rate"]
if 44100 != sample_rate:
waveform = torchaudio.functional.resample(audio["waveform"], sample_rate, 44100)
else:
waveform = audio["waveform"]
return io.NodeOutput({"samples": vae.encode(waveform.movedim(1, -1))})
class LoadAudio(io.ComfyNode):
class VAEDecodeAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LoadAudio_V3", # frontend expects "LoadAudio" to work
display_name="Load Audio _V3", # frontend ignores "display_name" for this node
category="audio",
node_id="VAEDecodeAudio_V3",
category="latent/audio",
inputs=[
io.Combo.Input("audio", upload=io.UploadType.audio, options=cls.get_files_options()),
io.Latent.Input("samples"),
io.Vae.Input("vae"),
],
outputs=[io.Audio.Output()],
)
@classmethod
def get_files_options(cls) -> list[str]:
input_dir = folder_paths.get_input_directory()
return sorted(folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"]))
@classmethod
def execute(cls, audio) -> io.NodeOutput:
waveform, sample_rate = torchaudio.load(folder_paths.get_annotated_filepath(audio))
return io.NodeOutput({"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate})
@classmethod
def fingerprint_inputs(s, audio):
image_path = folder_paths.get_annotated_filepath(audio)
m = hashlib.sha256()
with open(image_path, "rb") as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def validate_inputs(s, audio):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
def execute(cls, vae, samples) -> io.NodeOutput:
audio = vae.decode(samples["samples"]).movedim(-1, 1)
std = torch.std(audio, dim=[1, 2], keepdim=True) * 5.0
std[std < 1.0] = 1.0
audio /= std
return io.NodeOutput({"waveform": audio, "sample_rate": 44100})
class PreviewAudio(io.ComfyNode):
class SaveAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PreviewAudio_V3", # frontend expects "PreviewAudio" to work
display_name="Preview Audio _V3", # frontend ignores "display_name" for this node
node_id="SaveAudio_V3", # frontend expects "SaveAudio" to work
display_name="Save Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
io.String.Input("filename_prefix", default="audio/ComfyUI"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, audio) -> io.NodeOutput:
return io.NodeOutput(ui=ui.PreviewAudio(audio, cls=cls))
def execute(cls, audio, filename_prefix="ComfyUI", format="flac") -> io.NodeOutput:
return io.NodeOutput(
ui=ui.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=format)
)
class SaveAudioMP3(io.ComfyNode):
@ -171,71 +182,99 @@ class SaveAudioOpus(io.ComfyNode):
)
class SaveAudio(io.ComfyNode):
class PreviewAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveAudio_V3", # frontend expects "SaveAudio" to work
display_name="Save Audio _V3", # frontend ignores "display_name" for this node
node_id="PreviewAudio_V3", # frontend expects "PreviewAudio" to work
display_name="Preview Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Audio.Input("audio"),
io.String.Input("filename_prefix", default="audio/ComfyUI"),
],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, audio, filename_prefix="ComfyUI", format="flac") -> io.NodeOutput:
return io.NodeOutput(
ui=ui.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=format)
)
def execute(cls, audio) -> io.NodeOutput:
return io.NodeOutput(ui=ui.PreviewAudio(audio, cls=cls))
class VAEDecodeAudio(io.ComfyNode):
class LoadAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VAEDecodeAudio_V3",
category="latent/audio",
node_id="LoadAudio_V3", # frontend expects "LoadAudio" to work
display_name="Load Audio _V3", # frontend ignores "display_name" for this node
category="audio",
inputs=[
io.Latent.Input("samples"),
io.Vae.Input("vae"),
io.Combo.Input("audio", upload=io.UploadType.audio, options=cls.get_files_options()),
],
outputs=[io.Audio.Output()],
)
@classmethod
def execute(cls, vae, samples) -> io.NodeOutput:
audio = vae.decode(samples["samples"]).movedim(-1, 1)
std = torch.std(audio, dim=[1, 2], keepdim=True) * 5.0
std[std < 1.0] = 1.0
audio /= std
return io.NodeOutput({"waveform": audio, "sample_rate": 44100})
class VAEEncodeAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VAEEncodeAudio_V3",
category="latent/audio",
inputs=[
io.Audio.Input("audio"),
io.Vae.Input("vae"),
],
outputs=[io.Latent.Output()],
)
def get_files_options(cls) -> list[str]:
input_dir = folder_paths.get_input_directory()
return sorted(folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"]))
@classmethod
def execute(cls, vae, audio) -> io.NodeOutput:
sample_rate = audio["sample_rate"]
if 44100 != sample_rate:
waveform = torchaudio.functional.resample(audio["waveform"], sample_rate, 44100)
else:
waveform = audio["waveform"]
return io.NodeOutput({"samples": vae.encode(waveform.movedim(1, -1))})
def load(cls, filepath: str) -> tuple[torch.Tensor, int]:
with av.open(filepath) as af:
if not af.streams.audio:
raise ValueError("No audio stream found in the file.")
stream = af.streams.audio[0]
sr = stream.codec_context.sample_rate
n_channels = stream.channels
frames = []
length = 0
for frame in af.decode(streams=stream.index):
buf = torch.from_numpy(frame.to_ndarray())
if buf.shape[0] != n_channels:
buf = buf.view(-1, n_channels).t()
frames.append(buf)
length += buf.shape[1]
if not frames:
raise ValueError("No audio frames decoded.")
wav = torch.cat(frames, dim=1)
wav = cls.f32_pcm(wav)
return wav, sr
@classmethod
def f32_pcm(cls, wav: torch.Tensor) -> torch.Tensor:
"""Convert audio to float 32 bits PCM format."""
if wav.dtype.is_floating_point:
return wav
elif wav.dtype == torch.int16:
return wav.float() / (2 ** 15)
elif wav.dtype == torch.int32:
return wav.float() / (2 ** 31)
raise ValueError(f"Unsupported wav dtype: {wav.dtype}")
@classmethod
def execute(cls, audio) -> io.NodeOutput:
waveform, sample_rate = cls.load(folder_paths.get_annotated_filepath(audio))
return io.NodeOutput({"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate})
@classmethod
def fingerprint_inputs(s, audio):
image_path = folder_paths.get_annotated_filepath(audio)
m = hashlib.sha256()
with open(image_path, "rb") as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def validate_inputs(s, audio):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
NODES_LIST: list[type[io.ComfyNode]] = [
@ -243,9 +282,9 @@ NODES_LIST: list[type[io.ComfyNode]] = [
EmptyLatentAudio,
LoadAudio,
PreviewAudio,
SaveAudio,
SaveAudioMP3,
SaveAudioOpus,
SaveAudio,
VAEDecodeAudio,
VAEEncodeAudio,
]

View File

@ -212,6 +212,6 @@ class WanCameraEmbedding(io.ComfyNode):
return io.NodeOutput(control_camera_video, width, height, length)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
WanCameraEmbedding,
]

View File

@ -27,6 +27,6 @@ class Canny(io.ComfyNode):
return io.NodeOutput(img_out)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
Canny,
]

View File

@ -5,6 +5,7 @@ import torch
from comfy_api.latest import io
# https://github.com/WeichenFan/CFG-Zero-star
def optimized_scale(positive, negative):
positive_flat = positive.reshape(positive.shape[0], -1)
negative_flat = negative.reshape(negative.shape[0], -1)
@ -21,6 +22,36 @@ def optimized_scale(positive, negative):
return st_star.reshape([positive.shape[0]] + [1] * (positive.ndim - 1))
class CFGZeroStar(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="CFGZeroStar_V3",
category="advanced/guidance",
inputs=[
io.Model.Input("model"),
],
outputs=[io.Model.Output(display_name="patched_model")],
)
@classmethod
def execute(cls, model) -> io.NodeOutput:
m = model.clone()
def cfg_zero_star(args):
guidance_scale = args['cond_scale']
x = args['input']
cond_p = args['cond_denoised']
uncond_p = args['uncond_denoised']
out = args["denoised"]
alpha = optimized_scale(x - cond_p, x - uncond_p)
return out + uncond_p * (alpha - 1.0) + guidance_scale * uncond_p * (1.0 - alpha)
m.set_model_sampler_post_cfg_function(cfg_zero_star)
return io.NodeOutput(m)
class CFGNorm(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
@ -52,37 +83,7 @@ class CFGNorm(io.ComfyNode):
return io.NodeOutput(m)
class CFGZeroStar(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="CFGZeroStar_V3",
category="advanced/guidance",
inputs=[
io.Model.Input("model"),
],
outputs=[io.Model.Output(display_name="patched_model")],
)
@classmethod
def execute(cls, model) -> io.NodeOutput:
m = model.clone()
def cfg_zero_star(args):
guidance_scale = args['cond_scale']
x = args['input']
cond_p = args['cond_denoised']
uncond_p = args['uncond_denoised']
out = args["denoised"]
alpha = optimized_scale(x - cond_p, x - uncond_p)
return out + uncond_p * (alpha - 1.0) + guidance_scale * uncond_p * (1.0 - alpha)
m.set_model_sampler_post_cfg_function(cfg_zero_star)
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CFGNorm,
CFGZeroStar,
]

View File

@ -4,6 +4,31 @@ import nodes
from comfy_api.latest import io
class CLIPTextEncodeSDXLRefiner(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeSDXLRefiner_V3",
category="advanced/conditioning",
inputs=[
io.Float.Input("ascore", default=6.0, min=0.0, max=1000.0, step=0.01),
io.Int.Input("width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
io.String.Input("text", multiline=True, dynamic_prompts=True),
io.Clip.Input("clip"),
],
outputs=[io.Conditioning.Output()],
)
@classmethod
def execute(cls, ascore, width, height, text, clip) -> io.NodeOutput:
tokens = clip.tokenize(text)
conditioning = clip.encode_from_tokens_scheduled(
tokens, add_dict={"aesthetic_score": ascore, "width": width, "height": height}
)
return io.NodeOutput(conditioning)
class CLIPTextEncodeSDXL(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -48,32 +73,7 @@ class CLIPTextEncodeSDXL(io.ComfyNode):
return io.NodeOutput(conditioning)
class CLIPTextEncodeSDXLRefiner(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeSDXLRefiner_V3",
category="advanced/conditioning",
inputs=[
io.Float.Input("ascore", default=6.0, min=0.0, max=1000.0, step=0.01),
io.Int.Input("width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
io.String.Input("text", multiline=True, dynamic_prompts=True),
io.Clip.Input("clip"),
],
outputs=[io.Conditioning.Output()],
)
@classmethod
def execute(cls, ascore, width, height, text, clip) -> io.NodeOutput:
tokens = clip.tokenize(text)
conditioning = clip.encode_from_tokens_scheduled(
tokens, add_dict={"aesthetic_score": ascore, "width": width, "height": height}
)
return io.NodeOutput(conditioning)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeSDXL,
CLIPTextEncodeSDXLRefiner,
]

View File

@ -112,32 +112,6 @@ def porter_duff_composite(
return out_image, out_alpha
class JoinImageWithAlpha(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="JoinImageWithAlpha_V3",
display_name="Join Image with Alpha _V3",
category="mask/compositing",
inputs=[
io.Image.Input("image"),
io.Mask.Input("alpha"),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, image: torch.Tensor, alpha: torch.Tensor) -> io.NodeOutput:
batch_size = min(len(image), len(alpha))
out_images = []
alpha = 1.0 - resize_mask(alpha, image.shape[1:])
for i in range(batch_size):
out_images.append(torch.cat((image[i][:, :, :3], alpha[i].unsqueeze(2)), dim=2))
return io.NodeOutput(torch.stack(out_images))
class PorterDuffImageComposite(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -219,7 +193,33 @@ class SplitImageWithAlpha(io.ComfyNode):
return io.NodeOutput(torch.stack(out_images), 1.0 - torch.stack(out_alphas))
NODES_LIST = [
class JoinImageWithAlpha(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="JoinImageWithAlpha_V3",
display_name="Join Image with Alpha _V3",
category="mask/compositing",
inputs=[
io.Image.Input("image"),
io.Mask.Input("alpha"),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, image: torch.Tensor, alpha: torch.Tensor) -> io.NodeOutput:
batch_size = min(len(image), len(alpha))
out_images = []
alpha = 1.0 - resize_mask(alpha, image.shape[1:])
for i in range(batch_size):
out_images.append(torch.cat((image[i][:, :, :3], alpha[i].unsqueeze(2)), dim=2))
return io.NodeOutput(torch.stack(out_images))
NODES_LIST: list[type[io.ComfyNode]] = [
JoinImageWithAlpha,
PorterDuffImageComposite,
SplitImageWithAlpha,

View File

@ -54,7 +54,7 @@ class T5TokenizerOptions(io.ComfyNode):
return io.NodeOutput(clip)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeControlnet,
T5TokenizerOptions,
]

View File

@ -3,6 +3,33 @@ from comfy.cldm.control_types import UNION_CONTROLNET_TYPES
from comfy_api.latest import io
class SetUnionControlNetType(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SetUnionControlNetType_V3",
category="conditioning/controlnet",
inputs=[
io.ControlNet.Input("control_net"),
io.Combo.Input("type", options=["auto"] + list(UNION_CONTROLNET_TYPES.keys())),
],
outputs=[
io.ControlNet.Output(),
],
)
@classmethod
def execute(cls, control_net, type) -> io.NodeOutput:
control_net = control_net.copy()
type_number = UNION_CONTROLNET_TYPES.get(type, -1)
if type_number >= 0:
control_net.set_extra_arg("control_type", [type_number])
else:
control_net.set_extra_arg("control_type", [])
return io.NodeOutput(control_net)
class ControlNetApplyAdvanced(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -60,33 +87,6 @@ class ControlNetApplyAdvanced(io.ComfyNode):
return io.NodeOutput(out[0], out[1])
class SetUnionControlNetType(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SetUnionControlNetType_V3",
category="conditioning/controlnet",
inputs=[
io.ControlNet.Input("control_net"),
io.Combo.Input("type", options=["auto"] + list(UNION_CONTROLNET_TYPES.keys())),
],
outputs=[
io.ControlNet.Output(),
],
)
@classmethod
def execute(cls, control_net, type) -> io.NodeOutput:
control_net = control_net.copy()
type_number = UNION_CONTROLNET_TYPES.get(type, -1)
if type_number >= 0:
control_net.set_extra_arg("control_type", [type_number])
else:
control_net.set_extra_arg("control_type", [])
return io.NodeOutput(control_net)
class ControlNetInpaintingAliMamaApply(ControlNetApplyAdvanced):
@classmethod
def define_schema(cls):

View File

@ -9,6 +9,29 @@ import nodes
from comfy_api.latest import io
class EmptyCosmosLatentVideo(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="EmptyCosmosLatentVideo_V3",
category="latent/video",
inputs=[
io.Int.Input("width", default=1280, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("height", default=704, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("length", default=121, min=1, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, width, height, length, batch_size) -> io.NodeOutput:
latent = torch.zeros(
[batch_size, 16, ((length - 1) // 8) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()
)
return io.NodeOutput({"samples": latent})
def vae_encode_with_padding(vae, image, width, height, length, padding=0):
pixels = comfy.utils.common_upscale(image[..., :3].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1)
pixel_len = min(pixels.shape[0], length)
@ -116,30 +139,7 @@ class CosmosPredict2ImageToVideoLatent(io.ComfyNode):
return io.NodeOutput(out_latent)
class EmptyCosmosLatentVideo(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="EmptyCosmosLatentVideo_V3",
category="latent/video",
inputs=[
io.Int.Input("width", default=1280, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("height", default=704, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("length", default=121, min=1, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, width, height, length, batch_size) -> io.NodeOutput:
latent = torch.zeros(
[batch_size, 16, ((length - 1) // 8) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()
)
return io.NodeOutput({"samples": latent})
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CosmosImageToVideoLatent,
CosmosPredict2ImageToVideoLatent,
EmptyCosmosLatentVideo,

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,6 @@ class DifferentialDiffusion(io.ComfyNode):
return (denoise_mask >= threshold).to(denoise_mask.dtype)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
DifferentialDiffusion,
]

View File

@ -29,6 +29,6 @@ class ReferenceLatent(io.ComfyNode):
return io.NodeOutput(conditioning)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
ReferenceLatent,
]

View File

@ -49,28 +49,6 @@ class CLIPTextEncodeFlux(io.ComfyNode):
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens, add_dict={"guidance": guidance}))
class FluxDisableGuidance(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="FluxDisableGuidance_V3",
category="advanced/conditioning/flux",
description="This node completely disables the guidance embed on Flux and Flux like models",
inputs=[
io.Conditioning.Input("conditioning"),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, conditioning):
c = node_helpers.conditioning_set_values(conditioning, {"guidance": None})
return io.NodeOutput(c)
class FluxGuidance(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -91,6 +69,25 @@ class FluxGuidance(io.ComfyNode):
c = node_helpers.conditioning_set_values(conditioning, {"guidance": guidance})
return io.NodeOutput(c)
class FluxDisableGuidance(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="FluxDisableGuidance_V3",
category="advanced/conditioning/flux",
description="This node completely disables the guidance embed on Flux and Flux like models",
inputs=[
io.Conditioning.Input("conditioning"),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, conditioning):
c = node_helpers.conditioning_set_values(conditioning, {"guidance": None})
return io.NodeOutput(c)
class FluxKontextImageScale(io.ComfyNode):
@classmethod
@ -117,7 +114,7 @@ class FluxKontextImageScale(io.ComfyNode):
return io.NodeOutput(image)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeFlux,
FluxDisableGuidance,
FluxGuidance,

View File

@ -125,7 +125,7 @@ class FreeU_V2(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
FreeU,
FreeU_V2,
]

View File

@ -105,6 +105,6 @@ class FreSca(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
FreSca,
]

View File

@ -371,6 +371,6 @@ class GITSScheduler(io.ComfyNode):
return io.NodeOutput(torch.FloatTensor(sigmas))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
GITSScheduler,
]

View File

@ -6,33 +6,6 @@ import folder_paths
from comfy_api.latest import io
class CLIPTextEncodeHiDream(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeHiDream_V3",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.String.Input("clip_l", multiline=True, dynamic_prompts=True),
io.String.Input("clip_g", multiline=True, dynamic_prompts=True),
io.String.Input("t5xxl", multiline=True, dynamic_prompts=True),
io.String.Input("llama", multiline=True, dynamic_prompts=True),
],
outputs=[
io.Conditioning.Output(),
]
)
@classmethod
def execute(cls, clip, clip_l, clip_g, t5xxl, llama):
tokens = clip.tokenize(clip_g)
tokens["l"] = clip.tokenize(clip_l)["l"]
tokens["t5xxl"] = clip.tokenize(t5xxl)["t5xxl"]
tokens["llama"] = clip.tokenize(llama)["llama"]
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
class QuadrupleCLIPLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -65,7 +38,34 @@ class QuadrupleCLIPLoader(io.ComfyNode):
)
NODES_LIST = [
class CLIPTextEncodeHiDream(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeHiDream_V3",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.String.Input("clip_l", multiline=True, dynamic_prompts=True),
io.String.Input("clip_g", multiline=True, dynamic_prompts=True),
io.String.Input("t5xxl", multiline=True, dynamic_prompts=True),
io.String.Input("llama", multiline=True, dynamic_prompts=True),
],
outputs=[
io.Conditioning.Output(),
]
)
@classmethod
def execute(cls, clip, clip_l, clip_g, t5xxl, llama):
tokens = clip.tokenize(clip_g)
tokens["l"] = clip.tokenize(clip_l)["l"]
tokens["t5xxl"] = clip.tokenize(t5xxl)["t5xxl"]
tokens["llama"] = clip.tokenize(llama)["llama"]
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeHiDream,
QuadrupleCLIPLoader,
]

View File

@ -7,16 +7,6 @@ import node_helpers
import nodes
from comfy_api.latest import io
PROMPT_TEMPLATE_ENCODE_VIDEO_I2V = (
"<|start_header_id|>system<|end_header_id|>\n\n<image>\nDescribe the video by detailing the following aspects according to the reference image: "
"1. The main content and theme of the video."
"2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects."
"3. Actions, events, behaviors temporal relationships, physical movement changes of the objects."
"4. background environment, light, style and atmosphere."
"5. camera angles, movements, and transitions used in the video:<|eot_id|>\n\n"
"<|start_header_id|>user<|end_header_id|>\n\n{}<|eot_id|>"
"<|start_header_id|>assistant<|end_header_id|>\n\n"
)
class CLIPTextEncodeHunyuanDiT(io.ComfyNode):
@classmethod
@ -68,6 +58,51 @@ class EmptyHunyuanLatentVideo(io.ComfyNode):
return io.NodeOutput({"samples":latent})
PROMPT_TEMPLATE_ENCODE_VIDEO_I2V = (
"<|start_header_id|>system<|end_header_id|>\n\n<image>\nDescribe the video by detailing the following aspects according to the reference image: "
"1. The main content and theme of the video."
"2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects."
"3. Actions, events, behaviors temporal relationships, physical movement changes of the objects."
"4. background environment, light, style and atmosphere."
"5. camera angles, movements, and transitions used in the video:<|eot_id|>\n\n"
"<|start_header_id|>user<|end_header_id|>\n\n{}<|eot_id|>"
"<|start_header_id|>assistant<|end_header_id|>\n\n"
)
class TextEncodeHunyuanVideo_ImageToVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextEncodeHunyuanVideo_ImageToVideo_V3",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.ClipVisionOutput.Input("clip_vision_output"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Int.Input(
"image_interleave",
default=2,
min=1,
max=512,
tooltip="How much the image influences things vs the text prompt. Higher number means more influence from the text prompt.",
),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, clip, clip_vision_output, prompt, image_interleave):
tokens = clip.tokenize(
prompt, llama_template=PROMPT_TEMPLATE_ENCODE_VIDEO_I2V,
image_embeds=clip_vision_output.mm_projected,
image_interleave=image_interleave,
)
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
class HunyuanImageToVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -126,40 +161,7 @@ class HunyuanImageToVideo(io.ComfyNode):
return io.NodeOutput(positive, out_latent)
class TextEncodeHunyuanVideo_ImageToVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextEncodeHunyuanVideo_ImageToVideo_V3",
category="advanced/conditioning",
inputs=[
io.Clip.Input("clip"),
io.ClipVisionOutput.Input("clip_vision_output"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.Int.Input(
"image_interleave",
default=2,
min=1,
max=512,
tooltip="How much the image influences things vs the text prompt. Higher number means more influence from the text prompt.",
),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, clip, clip_vision_output, prompt, image_interleave):
tokens = clip.tokenize(
prompt, llama_template=PROMPT_TEMPLATE_ENCODE_VIDEO_I2V,
image_embeds=clip_vision_output.mm_projected,
image_interleave=image_interleave,
)
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeHunyuanDiT,
EmptyHunyuanLatentVideo,
HunyuanImageToVideo,

View File

@ -0,0 +1,672 @@
from __future__ import annotations
import json
import os
import struct
import numpy as np
import torch
import comfy.model_management
import folder_paths
from comfy.cli_args import args
from comfy.ldm.modules.diffusionmodules.mmdit import (
get_1d_sincos_pos_embed_from_grid_torch,
)
from comfy_api.latest import io
class VOXEL:
def __init__(self, data):
self.data = data
class MESH:
def __init__(self, vertices, faces):
self.vertices = vertices
self.faces = faces
def voxel_to_mesh(voxels, threshold=0.5, device=None):
if device is None:
device = torch.device("cpu")
voxels = voxels.to(device)
binary = (voxels > threshold).float()
padded = torch.nn.functional.pad(binary, (1, 1, 1, 1, 1, 1), 'constant', 0)
D, H, W = binary.shape
neighbors = torch.tensor([
[0, 0, 1],
[0, 0, -1],
[0, 1, 0],
[0, -1, 0],
[1, 0, 0],
[-1, 0, 0]
], device=device)
z, y, x = torch.meshgrid(
torch.arange(D, device=device),
torch.arange(H, device=device),
torch.arange(W, device=device),
indexing='ij'
)
voxel_indices = torch.stack([z.flatten(), y.flatten(), x.flatten()], dim=1)
solid_mask = binary.flatten() > 0
solid_indices = voxel_indices[solid_mask]
corner_offsets = [
torch.tensor([
[0, 0, 1], [0, 1, 1], [1, 1, 1], [1, 0, 1]
], device=device),
torch.tensor([
[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]
], device=device),
torch.tensor([
[0, 1, 0], [1, 1, 0], [1, 1, 1], [0, 1, 1]
], device=device),
torch.tensor([
[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 0]
], device=device),
torch.tensor([
[1, 0, 1], [1, 1, 1], [1, 1, 0], [1, 0, 0]
], device=device),
torch.tensor([
[0, 1, 0], [0, 1, 1], [0, 0, 1], [0, 0, 0]
], device=device)
]
all_vertices = []
all_indices = []
vertex_count = 0
for face_idx, offset in enumerate(neighbors):
neighbor_indices = solid_indices + offset
padded_indices = neighbor_indices + 1
is_exposed = padded[
padded_indices[:, 0],
padded_indices[:, 1],
padded_indices[:, 2]
] == 0
if not is_exposed.any():
continue
exposed_indices = solid_indices[is_exposed]
corners = corner_offsets[face_idx].unsqueeze(0)
face_vertices = exposed_indices.unsqueeze(1) + corners
all_vertices.append(face_vertices.reshape(-1, 3))
num_faces = exposed_indices.shape[0]
face_indices = torch.arange(
vertex_count,
vertex_count + 4 * num_faces,
device=device
).reshape(-1, 4)
all_indices.append(torch.stack([face_indices[:, 0], face_indices[:, 1], face_indices[:, 2]], dim=1))
all_indices.append(torch.stack([face_indices[:, 0], face_indices[:, 2], face_indices[:, 3]], dim=1))
vertex_count += 4 * num_faces
if len(all_vertices) > 0:
vertices = torch.cat(all_vertices, dim=0)
faces = torch.cat(all_indices, dim=0)
else:
vertices = torch.zeros((1, 3))
faces = torch.zeros((1, 3))
v_min = 0
v_max = max(voxels.shape)
vertices = vertices - (v_min + v_max) / 2
scale = (v_max - v_min) / 2
if scale > 0:
vertices = vertices / scale
vertices = torch.fliplr(vertices)
return vertices, faces
def voxel_to_mesh_surfnet(voxels, threshold=0.5, device=None):
if device is None:
device = torch.device("cpu")
voxels = voxels.to(device)
D, H, W = voxels.shape
padded = torch.nn.functional.pad(voxels, (1, 1, 1, 1, 1, 1), 'constant', 0)
z, y, x = torch.meshgrid(
torch.arange(D, device=device),
torch.arange(H, device=device),
torch.arange(W, device=device),
indexing='ij'
)
cell_positions = torch.stack([z.flatten(), y.flatten(), x.flatten()], dim=1)
corner_offsets = torch.tensor([
[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0],
[0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]
], device=device)
corner_values = torch.zeros((cell_positions.shape[0], 8), device=device)
for c, (dz, dy, dx) in enumerate(corner_offsets):
corner_values[:, c] = padded[
cell_positions[:, 0] + dz,
cell_positions[:, 1] + dy,
cell_positions[:, 2] + dx
]
corner_signs = corner_values > threshold
has_inside = torch.any(corner_signs, dim=1)
has_outside = torch.any(~corner_signs, dim=1)
contains_surface = has_inside & has_outside
active_cells = cell_positions[contains_surface]
active_signs = corner_signs[contains_surface]
active_values = corner_values[contains_surface]
if active_cells.shape[0] == 0:
return torch.zeros((0, 3), device=device), torch.zeros((0, 3), dtype=torch.long, device=device)
edges = torch.tensor([
[0, 1], [0, 2], [0, 4], [1, 3],
[1, 5], [2, 3], [2, 6], [3, 7],
[4, 5], [4, 6], [5, 7], [6, 7]
], device=device)
cell_vertices = {}
progress = comfy.utils.ProgressBar(100)
for edge_idx, (e1, e2) in enumerate(edges):
progress.update(1)
crossing = active_signs[:, e1] != active_signs[:, e2]
if not crossing.any():
continue
cell_indices = torch.nonzero(crossing, as_tuple=True)[0]
v1 = active_values[cell_indices, e1]
v2 = active_values[cell_indices, e2]
t = torch.zeros_like(v1, device=device)
denom = v2 - v1
valid = denom != 0
t[valid] = (threshold - v1[valid]) / denom[valid]
t[~valid] = 0.5
p1 = corner_offsets[e1].float()
p2 = corner_offsets[e2].float()
intersection = p1.unsqueeze(0) + t.unsqueeze(1) * (p2.unsqueeze(0) - p1.unsqueeze(0))
for i, point in zip(cell_indices.tolist(), intersection):
if i not in cell_vertices:
cell_vertices[i] = []
cell_vertices[i].append(point)
# Calculate the final vertices as the average of intersection points for each cell
vertices = []
vertex_lookup = {}
vert_progress_mod = round(len(cell_vertices)/50)
for i, points in cell_vertices.items():
if not i % vert_progress_mod:
progress.update(1)
if points:
vertex = torch.stack(points).mean(dim=0)
vertex = vertex + active_cells[i].float()
vertex_lookup[tuple(active_cells[i].tolist())] = len(vertices)
vertices.append(vertex)
if not vertices:
return torch.zeros((0, 3), device=device), torch.zeros((0, 3), dtype=torch.long, device=device)
final_vertices = torch.stack(vertices)
inside_corners_mask = active_signs
outside_corners_mask = ~active_signs
inside_counts = inside_corners_mask.sum(dim=1, keepdim=True).float()
outside_counts = outside_corners_mask.sum(dim=1, keepdim=True).float()
inside_pos = torch.zeros((active_cells.shape[0], 3), device=device)
outside_pos = torch.zeros((active_cells.shape[0], 3), device=device)
for i in range(8):
mask_inside = inside_corners_mask[:, i].unsqueeze(1)
mask_outside = outside_corners_mask[:, i].unsqueeze(1)
inside_pos += corner_offsets[i].float().unsqueeze(0) * mask_inside
outside_pos += corner_offsets[i].float().unsqueeze(0) * mask_outside
inside_pos /= inside_counts
outside_pos /= outside_counts
gradients = inside_pos - outside_pos
pos_dirs = torch.tensor([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
], device=device)
cross_products = [
torch.linalg.cross(pos_dirs[i].float(), pos_dirs[j].float())
for i in range(3) for j in range(i+1, 3)
]
faces = []
all_keys = set(vertex_lookup.keys())
face_progress_mod = round(len(active_cells)/38*3)
for pair_idx, (i, j) in enumerate([(0,1), (0,2), (1,2)]):
dir_i = pos_dirs[i]
dir_j = pos_dirs[j]
cross_product = cross_products[pair_idx]
ni_positions = active_cells + dir_i
nj_positions = active_cells + dir_j
diag_positions = active_cells + dir_i + dir_j
alignments = torch.matmul(gradients, cross_product)
valid_quads = []
quad_indices = []
for idx, active_cell in enumerate(active_cells):
if not idx % face_progress_mod:
progress.update(1)
cell_key = tuple(active_cell.tolist())
ni_key = tuple(ni_positions[idx].tolist())
nj_key = tuple(nj_positions[idx].tolist())
diag_key = tuple(diag_positions[idx].tolist())
if cell_key in all_keys and ni_key in all_keys and nj_key in all_keys and diag_key in all_keys:
v0 = vertex_lookup[cell_key]
v1 = vertex_lookup[ni_key]
v2 = vertex_lookup[nj_key]
v3 = vertex_lookup[diag_key]
valid_quads.append((v0, v1, v2, v3))
quad_indices.append(idx)
for q_idx, (v0, v1, v2, v3) in enumerate(valid_quads):
cell_idx = quad_indices[q_idx]
if alignments[cell_idx] > 0:
faces.append(torch.tensor([v0, v1, v3], device=device, dtype=torch.long))
faces.append(torch.tensor([v0, v3, v2], device=device, dtype=torch.long))
else:
faces.append(torch.tensor([v0, v3, v1], device=device, dtype=torch.long))
faces.append(torch.tensor([v0, v2, v3], device=device, dtype=torch.long))
if faces:
faces = torch.stack(faces)
else:
faces = torch.zeros((0, 3), dtype=torch.long, device=device)
v_min = 0
v_max = max(D, H, W)
final_vertices = final_vertices - (v_min + v_max) / 2
scale = (v_max - v_min) / 2
if scale > 0:
final_vertices = final_vertices / scale
final_vertices = torch.fliplr(final_vertices)
return final_vertices, faces
def save_glb(vertices, faces, filepath, metadata=None):
"""
Save PyTorch tensor vertices and faces as a GLB file without external dependencies.
Parameters:
vertices: torch.Tensor of shape (N, 3) - The vertex coordinates
faces: torch.Tensor of shape (M, 3) - The face indices (triangle faces)
filepath: str - Output filepath (should end with .glb)
"""
# Convert tensors to numpy arrays
vertices_np = vertices.cpu().numpy().astype(np.float32)
faces_np = faces.cpu().numpy().astype(np.uint32)
vertices_buffer = vertices_np.tobytes()
indices_buffer = faces_np.tobytes()
def pad_to_4_bytes(buffer):
padding_length = (4 - (len(buffer) % 4)) % 4
return buffer + b'\x00' * padding_length
vertices_buffer_padded = pad_to_4_bytes(vertices_buffer)
indices_buffer_padded = pad_to_4_bytes(indices_buffer)
buffer_data = vertices_buffer_padded + indices_buffer_padded
vertices_byte_length = len(vertices_buffer)
vertices_byte_offset = 0
indices_byte_length = len(indices_buffer)
indices_byte_offset = len(vertices_buffer_padded)
gltf = {
"asset": {"version": "2.0", "generator": "ComfyUI"},
"buffers": [
{
"byteLength": len(buffer_data)
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": vertices_byte_offset,
"byteLength": vertices_byte_length,
"target": 34962 # ARRAY_BUFFER
},
{
"buffer": 0,
"byteOffset": indices_byte_offset,
"byteLength": indices_byte_length,
"target": 34963 # ELEMENT_ARRAY_BUFFER
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5126, # FLOAT
"count": len(vertices_np),
"type": "VEC3",
"max": vertices_np.max(axis=0).tolist(),
"min": vertices_np.min(axis=0).tolist()
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5125, # UNSIGNED_INT
"count": faces_np.size,
"type": "SCALAR"
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0
},
"indices": 1,
"mode": 4 # TRIANGLES
}
]
}
],
"nodes": [
{
"mesh": 0
}
],
"scenes": [
{
"nodes": [0]
}
],
"scene": 0
}
if metadata is not None:
gltf["asset"]["extras"] = metadata
# Convert the JSON to bytes
gltf_json = json.dumps(gltf).encode('utf8')
def pad_json_to_4_bytes(buffer):
padding_length = (4 - (len(buffer) % 4)) % 4
return buffer + b' ' * padding_length
gltf_json_padded = pad_json_to_4_bytes(gltf_json)
# Create the GLB header
# Magic glTF
glb_header = struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(gltf_json_padded) + 8 + len(buffer_data))
# Create JSON chunk header (chunk type 0)
json_chunk_header = struct.pack('<II', len(gltf_json_padded), 0x4E4F534A) # "JSON" in little endian
# Create BIN chunk header (chunk type 1)
bin_chunk_header = struct.pack('<II', len(buffer_data), 0x004E4942) # "BIN\0" in little endian
# Write the GLB file
with open(filepath, 'wb') as f:
f.write(glb_header)
f.write(json_chunk_header)
f.write(gltf_json_padded)
f.write(bin_chunk_header)
f.write(buffer_data)
return filepath
class EmptyLatentHunyuan3Dv2(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptyLatentHunyuan3Dv2_V3",
category="latent/3d",
inputs=[
io.Int.Input("resolution", default=3072, min=1, max=8192),
io.Int.Input("batch_size", default=1, min=1, max=4096, tooltip="The number of latent images in the batch.")
],
outputs=[
io.Latent.Output()
]
)
@classmethod
def execute(cls, resolution, batch_size):
latent = torch.zeros([batch_size, 64, resolution], device=comfy.model_management.intermediate_device())
return io.NodeOutput({"samples": latent, "type": "hunyuan3dv2"})
class Hunyuan3Dv2Conditioning(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Hunyuan3Dv2Conditioning_V3",
category="conditioning/video_models",
inputs=[
io.ClipVisionOutput.Input("clip_vision_output")
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative")
]
)
@classmethod
def execute(cls, clip_vision_output):
embeds = clip_vision_output.last_hidden_state
positive = [[embeds, {}]]
negative = [[torch.zeros_like(embeds), {}]]
return io.NodeOutput(positive, negative)
class Hunyuan3Dv2ConditioningMultiView(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Hunyuan3Dv2ConditioningMultiView_V3",
category="conditioning/video_models",
inputs=[
io.ClipVisionOutput.Input("front", optional=True),
io.ClipVisionOutput.Input("left", optional=True),
io.ClipVisionOutput.Input("back", optional=True),
io.ClipVisionOutput.Input("right", optional=True)
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative")
]
)
@classmethod
def execute(cls, front=None, left=None, back=None, right=None):
all_embeds = [front, left, back, right]
out = []
pos_embeds = None
for i, e in enumerate(all_embeds):
if e is not None:
if pos_embeds is None:
pos_embeds = get_1d_sincos_pos_embed_from_grid_torch(e.last_hidden_state.shape[-1], torch.arange(4))
out.append(e.last_hidden_state + pos_embeds[i].reshape(1, 1, -1))
embeds = torch.cat(out, dim=1)
positive = [[embeds, {}]]
negative = [[torch.zeros_like(embeds), {}]]
return io.NodeOutput(positive, negative)
class SaveGLB(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveGLB_V3",
category="3d",
is_output_node=True,
inputs=[
io.Mesh.Input("mesh"),
io.String.Input("filename_prefix", default="mesh/ComfyUI")
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo]
)
@classmethod
def execute(cls, mesh, filename_prefix):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
results = []
metadata = {}
if not args.disable_metadata:
if cls.hidden.prompt is not None:
metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
for i in range(mesh.vertices.shape[0]):
f = f"{filename}_{counter:05}_.glb"
save_glb(mesh.vertices[i], mesh.faces[i], os.path.join(full_output_folder, f), metadata)
results.append({
"filename": f,
"subfolder": subfolder,
"type": "output"
})
counter += 1
return io.NodeOutput(ui={"ui": {"3d": results}}) # TODO: do we need an additional type of preview for this?
class VAEDecodeHunyuan3D(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VAEDecodeHunyuan3D_V3",
category="latent/3d",
inputs=[
io.Latent.Input("samples"),
io.Vae.Input("vae"),
io.Int.Input("num_chunks", default=8000, min=1000, max=500000),
io.Int.Input("octree_resolution", default=256, min=16, max=512)
],
outputs=[
io.Voxel.Output()
]
)
@classmethod
def execute(cls, vae, samples, num_chunks, octree_resolution):
voxels = VOXEL(vae.decode(samples["samples"], vae_options={"num_chunks": num_chunks, "octree_resolution": octree_resolution}))
return io.NodeOutput(voxels)
class VoxelToMesh(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VoxelToMesh_V3",
category="3d",
inputs=[
io.Voxel.Input("voxel"),
io.Combo.Input("algorithm", options=["surface net", "basic"]),
io.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01)
],
outputs=[
io.Mesh.Output()
]
)
@classmethod
def execute(cls, voxel, algorithm, threshold):
vertices = []
faces = []
if algorithm == "basic":
mesh_function = voxel_to_mesh
elif algorithm == "surface net":
mesh_function = voxel_to_mesh_surfnet
for x in voxel.data:
v, f = mesh_function(x, threshold=threshold, device=None)
vertices.append(v)
faces.append(f)
return io.NodeOutput(MESH(torch.stack(vertices), torch.stack(faces)))
class VoxelToMeshBasic(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VoxelToMeshBasic_V3",
category="3d",
inputs=[
io.Voxel.Input("voxel"),
io.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01)
],
outputs=[
io.Mesh.Output()
]
)
@classmethod
def execute(cls, voxel, threshold):
vertices = []
faces = []
for x in voxel.data:
v, f = voxel_to_mesh(x, threshold=threshold, device=None)
vertices.append(v)
faces.append(f)
return io.NodeOutput(MESH(torch.stack(vertices), torch.stack(faces)))
NODES_LIST: list[type[io.ComfyNode]] = [
EmptyLatentHunyuan3Dv2,
Hunyuan3Dv2Conditioning,
Hunyuan3Dv2ConditioningMultiView,
SaveGLB,
VAEDecodeHunyuan3D,
VoxelToMesh,
VoxelToMeshBasic,
]

View File

@ -131,6 +131,6 @@ class HypernetworkLoader(io.ComfyNode):
return io.NodeOutput(model_hypernetwork)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
HypernetworkLoader,
]

View File

@ -90,6 +90,6 @@ class HyperTile(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
HyperTile,
]

View File

@ -51,6 +51,6 @@ class InstructPixToPixConditioning(io.ComfyNode):
return io.NodeOutput(out[0], out[1], out_latent)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
InstructPixToPixConditioning,
]

View File

@ -44,16 +44,15 @@ class LatentAdd(io.ComfyNode):
return io.NodeOutput(samples_out)
class LatentApplyOperation(io.ComfyNode):
class LatentSubtract(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentApplyOperation_V3",
category="latent/advanced/operations",
is_experimental=True,
node_id="LatentSubtract_V3",
category="latent/advanced",
inputs=[
io.Latent.Input("samples"),
io.LatentOperation.Input("operation"),
io.Latent.Input("samples1"),
io.Latent.Input("samples2"),
],
outputs=[
io.Latent.Output(),
@ -61,44 +60,78 @@ class LatentApplyOperation(io.ComfyNode):
)
@classmethod
def execute(cls, samples, operation):
samples_out = samples.copy()
def execute(cls, samples1, samples2):
samples_out = samples1.copy()
s1 = samples["samples"]
samples_out["samples"] = operation(latent=s1)
s1 = samples1["samples"]
s2 = samples2["samples"]
s2 = reshape_latent_to(s1.shape, s2)
samples_out["samples"] = s1 - s2
return io.NodeOutput(samples_out)
class LatentApplyOperationCFG(io.ComfyNode):
class LatentMultiply(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentApplyOperationCFG_V3",
category="latent/advanced/operations",
is_experimental=True,
node_id="LatentMultiply_V3",
category="latent/advanced",
inputs=[
io.Model.Input("model"),
io.LatentOperation.Input("operation"),
io.Latent.Input("samples"),
io.Float.Input("multiplier", default=1.0, min=-10.0, max=10.0, step=0.01),
],
outputs=[
io.Model.Output(),
io.Latent.Output(),
],
)
@classmethod
def execute(cls, model, operation):
m = model.clone()
def execute(cls, samples, multiplier):
samples_out = samples.copy()
def pre_cfg_function(args):
conds_out = args["conds_out"]
if len(conds_out) == 2:
conds_out[0] = operation(latent=(conds_out[0] - conds_out[1])) + conds_out[1]
else:
conds_out[0] = operation(latent=conds_out[0])
return conds_out
s1 = samples["samples"]
samples_out["samples"] = s1 * multiplier
return io.NodeOutput(samples_out)
m.set_model_sampler_pre_cfg_function(pre_cfg_function)
return io.NodeOutput(m)
class LatentInterpolate(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentInterpolate_V3",
category="latent/advanced",
inputs=[
io.Latent.Input("samples1"),
io.Latent.Input("samples2"),
io.Float.Input("ratio", default=1.0, min=0.0, max=1.0, step=0.01),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, samples1, samples2, ratio):
samples_out = samples1.copy()
s1 = samples1["samples"]
s2 = samples2["samples"]
s2 = reshape_latent_to(s1.shape, s2)
m1 = torch.linalg.vector_norm(s1, dim=(1))
m2 = torch.linalg.vector_norm(s2, dim=(1))
s1 = torch.nan_to_num(s1 / m1)
s2 = torch.nan_to_num(s2 / m2)
t = (s1 * ratio + s2 * (1.0 - ratio))
mt = torch.linalg.vector_norm(t, dim=(1))
st = torch.nan_to_num(t / mt)
samples_out["samples"] = st * (m1 * ratio + m2 * (1.0 - ratio))
return io.NodeOutput(samples_out)
class LatentBatch(io.ComfyNode):
@ -159,54 +192,16 @@ class LatentBatchSeedBehavior(io.ComfyNode):
return io.NodeOutput(samples_out)
class LatentInterpolate(io.ComfyNode):
class LatentApplyOperation(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentInterpolate_V3",
category="latent/advanced",
inputs=[
io.Latent.Input("samples1"),
io.Latent.Input("samples2"),
io.Float.Input("ratio", default=1.0, min=0.0, max=1.0, step=0.01),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, samples1, samples2, ratio):
samples_out = samples1.copy()
s1 = samples1["samples"]
s2 = samples2["samples"]
s2 = reshape_latent_to(s1.shape, s2)
m1 = torch.linalg.vector_norm(s1, dim=(1))
m2 = torch.linalg.vector_norm(s2, dim=(1))
s1 = torch.nan_to_num(s1 / m1)
s2 = torch.nan_to_num(s2 / m2)
t = (s1 * ratio + s2 * (1.0 - ratio))
mt = torch.linalg.vector_norm(t, dim=(1))
st = torch.nan_to_num(t / mt)
samples_out["samples"] = st * (m1 * ratio + m2 * (1.0 - ratio))
return io.NodeOutput(samples_out)
class LatentMultiply(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentMultiply_V3",
category="latent/advanced",
node_id="LatentApplyOperation_V3",
category="latent/advanced/operations",
is_experimental=True,
inputs=[
io.Latent.Input("samples"),
io.Float.Input("multiplier", default=1.0, min=-10.0, max=10.0, step=0.01),
io.LatentOperation.Input("operation"),
],
outputs=[
io.Latent.Output(),
@ -214,14 +209,81 @@ class LatentMultiply(io.ComfyNode):
)
@classmethod
def execute(cls, samples, multiplier):
def execute(cls, samples, operation):
samples_out = samples.copy()
s1 = samples["samples"]
samples_out["samples"] = s1 * multiplier
samples_out["samples"] = operation(latent=s1)
return io.NodeOutput(samples_out)
class LatentApplyOperationCFG(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentApplyOperationCFG_V3",
category="latent/advanced/operations",
is_experimental=True,
inputs=[
io.Model.Input("model"),
io.LatentOperation.Input("operation"),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, operation):
m = model.clone()
def pre_cfg_function(args):
conds_out = args["conds_out"]
if len(conds_out) == 2:
conds_out[0] = operation(latent=(conds_out[0] - conds_out[1])) + conds_out[1]
else:
conds_out[0] = operation(latent=conds_out[0])
return conds_out
m.set_model_sampler_pre_cfg_function(pre_cfg_function)
return io.NodeOutput(m)
class LatentOperationTonemapReinhard(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentOperationTonemapReinhard_V3",
category="latent/advanced/operations",
is_experimental=True,
inputs=[
io.Float.Input("multiplier", default=1.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.LatentOperation.Output(),
],
)
@classmethod
def execute(cls, multiplier):
def tonemap_reinhard(latent, **kwargs):
latent_vector_magnitude = (torch.linalg.vector_norm(latent, dim=(1)) + 0.0000000001)[:,None]
normalized_latent = latent / latent_vector_magnitude
mean = torch.mean(latent_vector_magnitude, dim=(1,2,3), keepdim=True)
std = torch.std(latent_vector_magnitude, dim=(1,2,3), keepdim=True)
top = (std * 5 + mean) * multiplier
#reinhard
latent_vector_magnitude *= (1.0 / top)
new_magnitude = latent_vector_magnitude / (latent_vector_magnitude + 1.0)
new_magnitude *= top
return normalized_latent * new_magnitude
return io.NodeOutput(tonemap_reinhard)
class LatentOperationSharpen(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -264,69 +326,7 @@ class LatentOperationSharpen(io.ComfyNode):
return io.NodeOutput(sharpen)
class LatentOperationTonemapReinhard(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentOperationTonemapReinhard_V3",
category="latent/advanced/operations",
is_experimental=True,
inputs=[
io.Float.Input("multiplier", default=1.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.LatentOperation.Output(),
],
)
@classmethod
def execute(cls, multiplier):
def tonemap_reinhard(latent, **kwargs):
latent_vector_magnitude = (torch.linalg.vector_norm(latent, dim=(1)) + 0.0000000001)[:,None]
normalized_latent = latent / latent_vector_magnitude
mean = torch.mean(latent_vector_magnitude, dim=(1,2,3), keepdim=True)
std = torch.std(latent_vector_magnitude, dim=(1,2,3), keepdim=True)
top = (std * 5 + mean) * multiplier
#reinhard
latent_vector_magnitude *= (1.0 / top)
new_magnitude = latent_vector_magnitude / (latent_vector_magnitude + 1.0)
new_magnitude *= top
return normalized_latent * new_magnitude
return io.NodeOutput(tonemap_reinhard)
class LatentSubtract(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentSubtract_V3",
category="latent/advanced",
inputs=[
io.Latent.Input("samples1"),
io.Latent.Input("samples2"),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, samples1, samples2):
samples_out = samples1.copy()
s1 = samples1["samples"]
s2 = samples2["samples"]
s2 = reshape_latent_to(s1.shape, s2)
samples_out["samples"] = s1 - s2
return io.NodeOutput(samples_out)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
LatentAdd,
LatentApplyOperation,
LatentApplyOperationCFG,

View File

@ -172,7 +172,7 @@ class Preview3DAnimation(io.ComfyNode):
return io.NodeOutput(ui=ui.PreviewUI3D(model_file, camera_info, cls=cls))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
Load3D,
Load3DAnimation,
Preview3D,

View File

@ -133,6 +133,6 @@ class LoraSave(io.ComfyNode):
return io.NodeOutput()
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
LoraSave,
]

View File

@ -29,6 +29,6 @@ class LotusConditioning(io.ComfyNode):
return io.NodeOutput(cond)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
LotusConditioning,
]

View File

@ -127,12 +127,12 @@ class LTXVAddGuide(io.ComfyNode):
io.Vae.Input("vae"),
io.Latent.Input("latent"),
io.Image.Input(
id="image",
"image",
tooltip="Image or video to condition the latent video on. Must be 8*n + 1 frames. "
"If the video is not 8*n + 1 frames, it will be cropped to the nearest 8*n + 1 frames.",
),
io.Int.Input(
id="frame_idx",
"frame_idx",
default=0,
min=-9999,
max=9999,
@ -516,7 +516,7 @@ class ModelSamplingLTXV(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
EmptyLTXVLatentVideo,
LTXVAddGuide,
LTXVConditioning,

View File

@ -5,50 +5,6 @@ import torch
from comfy_api.latest import io
class CLIPTextEncodeLumina2(io.ComfyNode):
SYSTEM_PROMPT = {
"superior": "You are an assistant designed to generate superior images with the superior "
"degree of image-text alignment based on textual prompts or user prompts.",
"alignment": "You are an assistant designed to generate high-quality images with the "
"highest degree of image-text alignment based on textual prompts."
}
SYSTEM_PROMPT_TIP = "Lumina2 provide two types of system prompts:" \
"Superior: You are an assistant designed to generate superior images with the superior "\
"degree of image-text alignment based on textual prompts or user prompts. "\
"Alignment: You are an assistant designed to generate high-quality images with the highest "\
"degree of image-text alignment based on textual prompts."
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeLumina2_V3",
display_name="CLIP Text Encode for Lumina2 _V3",
category="conditioning",
description="Encodes a system prompt and a user prompt using a CLIP model into an embedding "
"that can be used to guide the diffusion model towards generating specific images.",
inputs=[
io.Combo.Input("system_prompt", options=list(cls.SYSTEM_PROMPT.keys()), tooltip=cls.SYSTEM_PROMPT_TIP),
io.String.Input("user_prompt", multiline=True, dynamic_prompts=True, tooltip="The text to be encoded."),
io.Clip.Input("clip", tooltip="The CLIP model used for encoding the text."),
],
outputs=[
io.Conditioning.Output(tooltip="A conditioning containing the embedded text used to guide the diffusion model."),
],
)
@classmethod
def execute(cls, system_prompt, user_prompt, clip):
if clip is None:
raise RuntimeError(
"ERROR: clip input is invalid: None\n\n"
"If the clip is from a checkpoint loader node your checkpoint does not contain a valid clip or text encoder model."
)
system_prompt = cls.SYSTEM_PROMPT[system_prompt]
prompt = f'{system_prompt} <Prompt Start> {user_prompt}'
tokens = clip.tokenize(prompt)
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
class RenormCFG(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -110,7 +66,51 @@ class RenormCFG(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
class CLIPTextEncodeLumina2(io.ComfyNode):
SYSTEM_PROMPT = {
"superior": "You are an assistant designed to generate superior images with the superior "
"degree of image-text alignment based on textual prompts or user prompts.",
"alignment": "You are an assistant designed to generate high-quality images with the "
"highest degree of image-text alignment based on textual prompts."
}
SYSTEM_PROMPT_TIP = "Lumina2 provide two types of system prompts:" \
"Superior: You are an assistant designed to generate superior images with the superior " \
"degree of image-text alignment based on textual prompts or user prompts. " \
"Alignment: You are an assistant designed to generate high-quality images with the highest " \
"degree of image-text alignment based on textual prompts."
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPTextEncodeLumina2_V3",
display_name="CLIP Text Encode for Lumina2 _V3",
category="conditioning",
description="Encodes a system prompt and a user prompt using a CLIP model into an embedding "
"that can be used to guide the diffusion model towards generating specific images.",
inputs=[
io.Combo.Input("system_prompt", options=list(cls.SYSTEM_PROMPT.keys()), tooltip=cls.SYSTEM_PROMPT_TIP),
io.String.Input("user_prompt", multiline=True, dynamic_prompts=True, tooltip="The text to be encoded."),
io.Clip.Input("clip", tooltip="The CLIP model used for encoding the text."),
],
outputs=[
io.Conditioning.Output(tooltip="A conditioning containing the embedded text used to guide the diffusion model."),
],
)
@classmethod
def execute(cls, system_prompt, user_prompt, clip):
if clip is None:
raise RuntimeError(
"ERROR: clip input is invalid: None\n\n"
"If the clip is from a checkpoint loader node your checkpoint does not contain a valid clip or text encoder model."
)
system_prompt = cls.SYSTEM_PROMPT[system_prompt]
prompt = f'{system_prompt} <Prompt Start> {user_prompt}'
tokens = clip.tokenize(prompt)
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeLumina2,
RenormCFG,
]

View File

@ -0,0 +1,51 @@
from __future__ import annotations
import torch
import torch.nn.functional as F
from comfy_api.latest import io
class Mahiro(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Mahiro_V3",
display_name="Mahiro is so cute that she deserves a better guidance function!! (。・ω・。) _V3",
category="_for_testing",
description="Modify the guidance to scale more on the 'direction' of the positive prompt rather than the difference between the negative prompt.",
is_experimental=True,
inputs=[
io.Model.Input("model")
],
outputs=[
io.Model.Output(display_name="patched_model")
]
)
@classmethod
def execute(cls, model):
m = model.clone()
def mahiro_normd(args):
scale: float = args['cond_scale']
cond_p: torch.Tensor = args['cond_denoised']
uncond_p: torch.Tensor = args['uncond_denoised']
#naive leap
leap = cond_p * scale
#sim with uncond leap
u_leap = uncond_p * scale
cfg = args["denoised"]
merge = (leap + cfg) / 2
normu = torch.sqrt(u_leap.abs()) * u_leap.sign()
normm = torch.sqrt(merge.abs()) * merge.sign()
sim = F.cosine_similarity(normu, normm).mean()
simsc = 2 * (sim+1)
wm = (simsc*cfg + (4-simsc)*leap) / 4
return wm
m.set_model_sampler_post_cfg_function(mahiro_normd)
return io.NodeOutput(m)
NODES_LIST: list[type[io.ComfyNode]] = [
Mahiro,
]

View File

@ -57,6 +57,161 @@ def composite(destination, source, x, y, mask=None, multiplier=8, resize_source=
return destination
class LatentCompositeMasked(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentCompositeMasked_V3",
display_name="Latent Composite Masked _V3",
category="latent",
inputs=[
io.Latent.Input("destination"),
io.Latent.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION, step=8),
io.Boolean.Input("resize_source", default=False),
io.Mask.Input("mask", optional=True),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, resize_source, mask=None) -> io.NodeOutput:
output = destination.copy()
destination_samples = destination["samples"].clone()
source_samples = source["samples"]
output["samples"] = composite(destination_samples, source_samples, x, y, mask, 8, resize_source)
return io.NodeOutput(output)
class ImageCompositeMasked(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageCompositeMasked_V3",
display_name="Image Composite Masked _V3",
category="image",
inputs=[
io.Image.Input("destination"),
io.Image.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Boolean.Input("resize_source", default=False),
io.Mask.Input("mask", optional=True),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, resize_source, mask=None) -> io.NodeOutput:
destination, source = node_helpers.image_alpha_fix(destination, source)
destination = destination.clone().movedim(-1, 1)
output = composite(destination, source.movedim(-1, 1), x, y, mask, 1, resize_source).movedim(1, -1)
return io.NodeOutput(output)
class MaskToImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MaskToImage_V3",
display_name="Convert Mask to Image _V3",
category="mask",
inputs=[
io.Mask.Input("mask"),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, mask) -> io.NodeOutput:
return io.NodeOutput(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3))
class ImageToMask(io.ComfyNode):
CHANNELS = ["red", "green", "blue", "alpha"]
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageToMask_V3",
display_name="Convert Image to Mask _V3",
category="mask",
inputs=[
io.Image.Input("image"),
io.Combo.Input("channel", options=cls.CHANNELS),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, image, channel) -> io.NodeOutput:
return io.NodeOutput(image[:, :, :, cls.CHANNELS.index(channel)])
class ImageColorToMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageColorToMask_V3",
display_name="Image Color to Mask _V3",
category="mask",
inputs=[
io.Image.Input("image"),
io.Int.Input("color", default=0, min=0, max=0xFFFFFF),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, image, color) -> io.NodeOutput:
temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int)
temp = (
torch.bitwise_left_shift(temp[:, :, :, 0], 16)
+ torch.bitwise_left_shift(temp[:, :, :, 1], 8)
+ temp[:, :, :, 2]
)
return io.NodeOutput(torch.where(temp == color, 1.0, 0).float())
class SolidMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SolidMask_V3",
display_name="Solid Mask _V3",
category="mask",
inputs=[
io.Float.Input("value", default=1.0, min=0.0, max=1.0, step=0.01),
io.Int.Input("width", default=512, min=1, max=nodes.MAX_RESOLUTION),
io.Int.Input("height", default=512, min=1, max=nodes.MAX_RESOLUTION),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, value, width, height) -> io.NodeOutput:
return io.NodeOutput(torch.full((1, height, width), value, dtype=torch.float32, device="cpu"))
class InvertMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="InvertMask_V3",
display_name="Invert Mask _V3",
category="mask",
inputs=[
io.Mask.Input("mask"),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, mask) -> io.NodeOutput:
return io.NodeOutput(1.0 - mask)
class CropMask(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -80,6 +235,66 @@ class CropMask(io.ComfyNode):
return io.NodeOutput(mask[:, y : y + height, x : x + width])
class MaskComposite(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MaskComposite_V3",
display_name="Mask Composite _V3",
category="mask",
inputs=[
io.Mask.Input("destination"),
io.Mask.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Combo.Input("operation", options=["multiply", "add", "subtract", "and", "or", "xor"]),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, operation) -> io.NodeOutput:
output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone()
source = source.reshape((-1, source.shape[-2], source.shape[-1]))
left, top = (
x,
y,
)
right, bottom = (
min(left + source.shape[-1], destination.shape[-1]),
min(top + source.shape[-2], destination.shape[-2]),
)
visible_width, visible_height = (
right - left,
bottom - top,
)
source_portion = source[:, :visible_height, :visible_width]
destination_portion = output[:, top:bottom, left:right]
if operation == "multiply":
output[:, top:bottom, left:right] = destination_portion * source_portion
elif operation == "add":
output[:, top:bottom, left:right] = destination_portion + source_portion
elif operation == "subtract":
output[:, top:bottom, left:right] = destination_portion - source_portion
elif operation == "and":
output[:, top:bottom, left:right] = torch.bitwise_and(
destination_portion.round().bool(), source_portion.round().bool()
).float()
elif operation == "or":
output[:, top:bottom, left:right] = torch.bitwise_or(
destination_portion.round().bool(), source_portion.round().bool()
).float()
elif operation == "xor":
output[:, top:bottom, left:right] = torch.bitwise_xor(
destination_portion.round().bool(), source_portion.round().bool()
).float()
return io.NodeOutput(torch.clamp(output, 0.0, 1.0))
class FeatherMask(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -158,183 +373,28 @@ class GrowMask(io.ComfyNode):
return io.NodeOutput(torch.stack(out, dim=0))
class ImageColorToMask(io.ComfyNode):
class ThresholdMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageColorToMask_V3",
display_name="Image Color to Mask _V3",
category="mask",
inputs=[
io.Image.Input("image"),
io.Int.Input("color", default=0, min=0, max=0xFFFFFF),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, image, color) -> io.NodeOutput:
temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int)
temp = (
torch.bitwise_left_shift(temp[:, :, :, 0], 16)
+ torch.bitwise_left_shift(temp[:, :, :, 1], 8)
+ temp[:, :, :, 2]
)
return io.NodeOutput(torch.where(temp == color, 1.0, 0).float())
class ImageCompositeMasked(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageCompositeMasked_V3",
display_name="Image Composite Masked _V3",
category="image",
inputs=[
io.Image.Input("destination"),
io.Image.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Boolean.Input("resize_source", default=False),
io.Mask.Input("mask", optional=True),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, resize_source, mask=None) -> io.NodeOutput:
destination, source = node_helpers.image_alpha_fix(destination, source)
destination = destination.clone().movedim(-1, 1)
output = composite(destination, source.movedim(-1, 1), x, y, mask, 1, resize_source).movedim(1, -1)
return io.NodeOutput(output)
class ImageToMask(io.ComfyNode):
CHANNELS = ["red", "green", "blue", "alpha"]
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageToMask_V3",
display_name="Convert Image to Mask _V3",
category="mask",
inputs=[
io.Image.Input("image"),
io.Combo.Input("channel", options=cls.CHANNELS),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, image, channel) -> io.NodeOutput:
return io.NodeOutput(image[:, :, :, cls.CHANNELS.index(channel)])
class InvertMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="InvertMask_V3",
display_name="Invert Mask _V3",
node_id="ThresholdMask_V3",
display_name="Threshold Mask _V3",
category="mask",
inputs=[
io.Mask.Input("mask"),
io.Float.Input("value", default=0.5, min=0.0, max=1.0, step=0.01),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, mask) -> io.NodeOutput:
return io.NodeOutput(1.0 - mask)
class LatentCompositeMasked(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LatentCompositeMasked_V3",
display_name="Latent Composite Masked _V3",
category="latent",
inputs=[
io.Latent.Input("destination"),
io.Latent.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION, step=8),
io.Boolean.Input("resize_source", default=False),
io.Mask.Input("mask", optional=True),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, resize_source, mask=None) -> io.NodeOutput:
output = destination.copy()
destination_samples = destination["samples"].clone()
source_samples = source["samples"]
output["samples"] = composite(destination_samples, source_samples, x, y, mask, 8, resize_source)
return io.NodeOutput(output)
class MaskComposite(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MaskComposite_V3",
display_name="Mask Composite _V3",
category="mask",
inputs=[
io.Mask.Input("destination"),
io.Mask.Input("source"),
io.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Int.Input("y", default=0, min=0, max=nodes.MAX_RESOLUTION),
io.Combo.Input("operation", options=["multiply", "add", "subtract", "and", "or", "xor"]),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, destination, source, x, y, operation) -> io.NodeOutput:
output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone()
source = source.reshape((-1, source.shape[-2], source.shape[-1]))
left, top = (
x,
y,
)
right, bottom = (
min(left + source.shape[-1], destination.shape[-1]),
min(top + source.shape[-2], destination.shape[-2]),
)
visible_width, visible_height = (
right - left,
bottom - top,
)
source_portion = source[:, :visible_height, :visible_width]
destination_portion = output[:, top:bottom, left:right]
if operation == "multiply":
output[:, top:bottom, left:right] = destination_portion * source_portion
elif operation == "add":
output[:, top:bottom, left:right] = destination_portion + source_portion
elif operation == "subtract":
output[:, top:bottom, left:right] = destination_portion - source_portion
elif operation == "and":
output[:, top:bottom, left:right] = torch.bitwise_and(
destination_portion.round().bool(), source_portion.round().bool()
).float()
elif operation == "or":
output[:, top:bottom, left:right] = torch.bitwise_or(
destination_portion.round().bool(), source_portion.round().bool()
).float()
elif operation == "xor":
output[:, top:bottom, left:right] = torch.bitwise_xor(
destination_portion.round().bool(), source_portion.round().bool()
).float()
return io.NodeOutput(torch.clamp(output, 0.0, 1.0))
def execute(cls, mask, value) -> io.NodeOutput:
return io.NodeOutput((mask > value).float())
# Mask Preview - original implement from
# https://github.com/cubiq/ComfyUI_essentials/blob/9d9f4bedfc9f0321c19faf71855e228c93bd0dc9/mask.py#L81
# upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
class MaskPreview(io.ComfyNode):
"""Mask Preview - original implement in ComfyUI_essentials.
@ -360,63 +420,6 @@ class MaskPreview(io.ComfyNode):
return io.NodeOutput(ui=ui.PreviewMask(masks))
class MaskToImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MaskToImage_V3",
display_name="Convert Mask to Image _V3",
category="mask",
inputs=[
io.Mask.Input("mask"),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, mask) -> io.NodeOutput:
return io.NodeOutput(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3))
class SolidMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SolidMask_V3",
display_name="Solid Mask _V3",
category="mask",
inputs=[
io.Float.Input("value", default=1.0, min=0.0, max=1.0, step=0.01),
io.Int.Input("width", default=512, min=1, max=nodes.MAX_RESOLUTION),
io.Int.Input("height", default=512, min=1, max=nodes.MAX_RESOLUTION),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, value, width, height) -> io.NodeOutput:
return io.NodeOutput(torch.full((1, height, width), value, dtype=torch.float32, device="cpu"))
class ThresholdMask(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ThresholdMask_V3",
display_name="Threshold Mask _V3",
category="mask",
inputs=[
io.Mask.Input("mask"),
io.Float.Input("value", default=0.5, min=0.0, max=1.0, step=0.01),
],
outputs=[io.Mask.Output()],
)
@classmethod
def execute(cls, mask, value) -> io.NodeOutput:
return io.NodeOutput((mask > value).float())
NODES_LIST: list[type[io.ComfyNode]] = [
CropMask,
FeatherMask,

View File

@ -33,6 +33,6 @@ class EmptyMochiLatentVideo(io.ComfyNode):
return io.NodeOutput({"samples": latent})
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
EmptyMochiLatentVideo,
]

View File

@ -57,15 +57,16 @@ class ModelSamplingDiscreteDistilled(comfy.model_sampling.ModelSamplingDiscrete)
return log_sigma.exp().to(timestep.device)
class ModelComputeDtype(io.ComfyNode):
class ModelSamplingDiscrete(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelComputeDtype_V3",
category="advanced/debug/model",
node_id="ModelSamplingDiscrete_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Combo.Input("dtype", options=["default", "fp32", "fp16", "bf16"]),
io.Combo.Input("sampling", options=["eps", "v_prediction", "lcm", "x0", "img_to_img"]),
io.Boolean.Input("zsnr", default=False),
],
outputs=[
io.Model.Output(),
@ -73,9 +74,150 @@ class ModelComputeDtype(io.ComfyNode):
)
@classmethod
def execute(cls, model, dtype):
def execute(cls, model, sampling, zsnr):
m = model.clone()
m.set_model_compute_dtype(node_helpers.string_to_torch_dtype(dtype))
sampling_base = comfy.model_sampling.ModelSamplingDiscrete
if sampling == "eps":
sampling_type = comfy.model_sampling.EPS
elif sampling == "v_prediction":
sampling_type = comfy.model_sampling.V_PREDICTION
elif sampling == "lcm":
sampling_type = LCM
sampling_base = ModelSamplingDiscreteDistilled
elif sampling == "x0":
sampling_type = comfy.model_sampling.X0
elif sampling == "img_to_img":
sampling_type = comfy.model_sampling.IMG_TO_IMG
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config, zsnr=zsnr)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingStableCascade(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingStableCascade_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=2.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift):
m = model.clone()
sampling_base = comfy.model_sampling.StableCascadeSampling
sampling_type = comfy.model_sampling.EPS
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingSD3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingSD3_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=3.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift, multiplier: int | float = 1000):
m = model.clone()
sampling_base = comfy.model_sampling.ModelSamplingDiscreteFlow
sampling_type = comfy.model_sampling.CONST
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift, multiplier=multiplier)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingAuraFlow(ModelSamplingSD3):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingAuraFlow_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=1.73, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift, multiplier: int | float = 1.0):
return super().execute(model, shift, multiplier)
class ModelSamplingFlux(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingFlux_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("max_shift", default=1.15, min=0.0, max=100.0, step=0.01),
io.Float.Input("base_shift", default=0.5, min=0.0, max=100.0, step=0.01),
io.Int.Input("width", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=8),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, max_shift, base_shift, width, height):
m = model.clone()
x1 = 256
x2 = 4096
mm = (max_shift - base_shift) / (x2 - x1)
b = base_shift - mm * x1
shift = (width * height / (8 * 8 * 2 * 2)) * mm + b
sampling_base = comfy.model_sampling.ModelSamplingFlux
sampling_type = comfy.model_sampling.CONST
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
@ -165,170 +307,6 @@ class ModelSamplingContinuousV(io.ComfyNode):
return io.NodeOutput(m)
class ModelSamplingDiscrete(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingDiscrete_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Combo.Input("sampling", options=["eps", "v_prediction", "lcm", "x0", "img_to_img"]),
io.Boolean.Input("zsnr", default=False),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, sampling, zsnr):
m = model.clone()
sampling_base = comfy.model_sampling.ModelSamplingDiscrete
if sampling == "eps":
sampling_type = comfy.model_sampling.EPS
elif sampling == "v_prediction":
sampling_type = comfy.model_sampling.V_PREDICTION
elif sampling == "lcm":
sampling_type = LCM
sampling_base = ModelSamplingDiscreteDistilled
elif sampling == "x0":
sampling_type = comfy.model_sampling.X0
elif sampling == "img_to_img":
sampling_type = comfy.model_sampling.IMG_TO_IMG
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config, zsnr=zsnr)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingFlux(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingFlux_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("max_shift", default=1.15, min=0.0, max=100.0, step=0.01),
io.Float.Input("base_shift", default=0.5, min=0.0, max=100.0, step=0.01),
io.Int.Input("width", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=8),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, max_shift, base_shift, width, height):
m = model.clone()
x1 = 256
x2 = 4096
mm = (max_shift - base_shift) / (x2 - x1)
b = base_shift - mm * x1
shift = (width * height / (8 * 8 * 2 * 2)) * mm + b
sampling_base = comfy.model_sampling.ModelSamplingFlux
sampling_type = comfy.model_sampling.CONST
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingSD3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingSD3_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=3.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift, multiplier: int | float = 1000):
m = model.clone()
sampling_base = comfy.model_sampling.ModelSamplingDiscreteFlow
sampling_type = comfy.model_sampling.CONST
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift, multiplier=multiplier)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class ModelSamplingAuraFlow(ModelSamplingSD3):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingAuraFlow_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=1.73, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift, multiplier: int | float = 1.0):
return super().execute(model, shift, multiplier)
class ModelSamplingStableCascade(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSamplingStableCascade_V3",
category="advanced/model",
inputs=[
io.Model.Input("model"),
io.Float.Input("shift", default=2.0, min=0.0, max=100.0, step=0.01),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, shift):
m = model.clone()
sampling_base = comfy.model_sampling.StableCascadeSampling
sampling_type = comfy.model_sampling.EPS
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift)
m.add_object_patch("model_sampling", model_sampling)
return io.NodeOutput(m)
class RescaleCFG(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -374,7 +352,29 @@ class RescaleCFG(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
class ModelComputeDtype(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelComputeDtype_V3",
category="advanced/debug/model",
inputs=[
io.Model.Input("model"),
io.Combo.Input("dtype", options=["default", "fp32", "fp16", "bf16"]),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, dtype):
m = model.clone()
m.set_model_compute_dtype(node_helpers.string_to_torch_dtype(dtype))
return io.NodeOutput(m)
NODES_LIST: list[type[io.ComfyNode]] = [
ModelSamplingAuraFlow,
ModelComputeDtype,
ModelSamplingContinuousEDM,

View File

@ -63,6 +63,6 @@ class PatchModelAddDownscale(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
PatchModelAddDownscale,
]

View File

@ -0,0 +1,422 @@
from __future__ import annotations
import json
import os
import torch
import comfy.model_base
import comfy.model_management
import comfy.model_sampling
import comfy.sd
import comfy.utils
import folder_paths
from comfy.cli_args import args
from comfy_api.latest import io
def save_checkpoint(model, clip=None, vae=None, clip_vision=None, filename_prefix=None, output_dir=None, prompt=None, extra_pnginfo=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, output_dir)
prompt_info = ""
if prompt is not None:
prompt_info = json.dumps(prompt)
metadata = {}
enable_modelspec = True
if isinstance(model.model, comfy.model_base.SDXL):
if isinstance(model.model, comfy.model_base.SDXL_instructpix2pix):
metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-edit"
else:
metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-base"
elif isinstance(model.model, comfy.model_base.SDXLRefiner):
metadata["modelspec.architecture"] = "stable-diffusion-xl-v1-refiner"
elif isinstance(model.model, comfy.model_base.SVD_img2vid):
metadata["modelspec.architecture"] = "stable-video-diffusion-img2vid-v1"
elif isinstance(model.model, comfy.model_base.SD3):
metadata["modelspec.architecture"] = "stable-diffusion-v3-medium" #TODO: other SD3 variants
else:
enable_modelspec = False
if enable_modelspec:
metadata["modelspec.sai_model_spec"] = "1.0.0"
metadata["modelspec.implementation"] = "sgm"
metadata["modelspec.title"] = "{} {}".format(filename, counter)
#TODO:
# "stable-diffusion-v1", "stable-diffusion-v1-inpainting", "stable-diffusion-v2-512",
# "stable-diffusion-v2-768-v", "stable-diffusion-v2-unclip-l", "stable-diffusion-v2-unclip-h",
# "v2-inpainting"
extra_keys = {}
model_sampling = model.get_model_object("model_sampling")
if isinstance(model_sampling, comfy.model_sampling.ModelSamplingContinuousEDM):
if isinstance(model_sampling, comfy.model_sampling.V_PREDICTION):
extra_keys["edm_vpred.sigma_max"] = torch.tensor(model_sampling.sigma_max).float()
extra_keys["edm_vpred.sigma_min"] = torch.tensor(model_sampling.sigma_min).float()
if model.model.model_type == comfy.model_base.ModelType.EPS:
metadata["modelspec.predict_key"] = "epsilon"
elif model.model.model_type == comfy.model_base.ModelType.V_PREDICTION:
metadata["modelspec.predict_key"] = "v"
extra_keys["v_pred"] = torch.tensor([])
if getattr(model_sampling, "zsnr", False):
extra_keys["ztsnr"] = torch.tensor([])
if not args.disable_metadata:
metadata["prompt"] = prompt_info
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
comfy.sd.save_checkpoint(output_checkpoint, model, clip, vae, clip_vision, metadata=metadata, extra_keys=extra_keys)
class ModelMergeSimple(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelMergeSimple_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("ratio", default=1.0, min=0.0, max=1.0, step=0.01)
],
outputs=[
io.Model.Output()
]
)
@classmethod
def execute(cls, model1, model2, ratio):
m = model1.clone()
kp = model2.get_key_patches("diffusion_model.")
for k in kp:
m.add_patches({k: kp[k]}, 1.0 - ratio, ratio)
return io.NodeOutput(m)
class ModelSubtract(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelMergeSubtract_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("multiplier", default=1.0, min=-10.0, max=10.0, step=0.01)
],
outputs=[
io.Model.Output()
]
)
@classmethod
def execute(cls, model1, model2, multiplier):
m = model1.clone()
kp = model2.get_key_patches("diffusion_model.")
for k in kp:
m.add_patches({k: kp[k]}, - multiplier, multiplier)
return io.NodeOutput(m)
class ModelAdd(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelMergeAdd_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model1"),
io.Model.Input("model2")
],
outputs=[
io.Model.Output()
]
)
@classmethod
def execute(cls, model1, model2):
m = model1.clone()
kp = model2.get_key_patches("diffusion_model.")
for k in kp:
m.add_patches({k: kp[k]}, 1.0, 1.0)
return io.NodeOutput(m)
class CLIPMergeSimple(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPMergeSimple_V3",
category="advanced/model_merging",
inputs=[
io.Clip.Input("clip1"),
io.Clip.Input("clip2"),
io.Float.Input("ratio", default=1.0, min=0.0, max=1.0, step=0.01)
],
outputs=[
io.Clip.Output()
]
)
@classmethod
def execute(cls, clip1, clip2, ratio):
m = clip1.clone()
kp = clip2.get_key_patches()
for k in kp:
if k.endswith(".position_ids") or k.endswith(".logit_scale"):
continue
m.add_patches({k: kp[k]}, 1.0 - ratio, ratio)
return io.NodeOutput(m)
class CLIPSubtract(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPMergeSubtract_V3",
category="advanced/model_merging",
inputs=[
io.Clip.Input("clip1"),
io.Clip.Input("clip2"),
io.Float.Input("multiplier", default=1.0, min=-10.0, max=10.0, step=0.01)
],
outputs=[
io.Clip.Output()
]
)
@classmethod
def execute(cls, clip1, clip2, multiplier):
m = clip1.clone()
kp = clip2.get_key_patches()
for k in kp:
if k.endswith(".position_ids") or k.endswith(".logit_scale"):
continue
m.add_patches({k: kp[k]}, - multiplier, multiplier)
return io.NodeOutput(m)
class CLIPAdd(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPMergeAdd_V3",
category="advanced/model_merging",
inputs=[
io.Clip.Input("clip1"),
io.Clip.Input("clip2")
],
outputs=[
io.Clip.Output()
]
)
@classmethod
def execute(cls, clip1, clip2):
m = clip1.clone()
kp = clip2.get_key_patches()
for k in kp:
if k.endswith(".position_ids") or k.endswith(".logit_scale"):
continue
m.add_patches({k: kp[k]}, 1.0, 1.0)
return io.NodeOutput(m)
class ModelMergeBlocks(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelMergeBlocks_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("input", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("middle", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("out", default=1.0, min=0.0, max=1.0, step=0.01)
],
outputs=[
io.Model.Output()
]
)
@classmethod
def execute(cls, model1, model2, **kwargs):
m = model1.clone()
kp = model2.get_key_patches("diffusion_model.")
default_ratio = next(iter(kwargs.values()))
for k in kp:
ratio = default_ratio
k_unet = k[len("diffusion_model."):]
last_arg_size = 0
for arg in kwargs:
if k_unet.startswith(arg) and last_arg_size < len(arg):
ratio = kwargs[arg]
last_arg_size = len(arg)
m.add_patches({k: kp[k]}, 1.0 - ratio, ratio)
return io.NodeOutput(m)
class CheckpointSave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CheckpointSave_V3",
display_name="Save Checkpoint _V3",
category="advanced/model_merging",
is_output_node=True,
inputs=[
io.Model.Input("model"),
io.Clip.Input("clip"),
io.Vae.Input("vae"),
io.String.Input("filename_prefix", default="checkpoints/ComfyUI")
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo]
)
@classmethod
def execute(cls, model, clip, vae, filename_prefix):
save_checkpoint(model, clip=clip, vae=vae, filename_prefix=filename_prefix, output_dir=folder_paths.get_output_directory(), prompt=cls.hidden.prompt, extra_pnginfo=cls.hidden.extra_pnginfo)
return io.NodeOutput()
class CLIPSave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CLIPSave_V3",
category="advanced/model_merging",
is_output_node=True,
inputs=[
io.Clip.Input("clip"),
io.String.Input("filename_prefix", default="clip/ComfyUI")
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo]
)
@classmethod
def execute(cls, clip, filename_prefix):
prompt_info = ""
if cls.hidden.prompt is not None:
prompt_info = json.dumps(cls.hidden.prompt)
metadata = {}
if not args.disable_metadata:
metadata["format"] = "pt"
metadata["prompt"] = prompt_info
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
comfy.model_management.load_models_gpu([clip.load_model()], force_patch_weights=True)
clip_sd = clip.get_sd()
for prefix in ["clip_l.", "clip_g.", "clip_h.", "t5xxl.", "pile_t5xl.", "mt5xl.", "umt5xxl.", "t5base.", "gemma2_2b.", "llama.", "hydit_clip.", ""]:
k = list(filter(lambda a: a.startswith(prefix), clip_sd.keys()))
current_clip_sd = {}
for x in k:
current_clip_sd[x] = clip_sd.pop(x)
if len(current_clip_sd) == 0:
continue
p = prefix[:-1]
replace_prefix = {}
filename_prefix_ = filename_prefix
if len(p) > 0:
filename_prefix_ = "{}_{}".format(filename_prefix_, p)
replace_prefix[prefix] = ""
replace_prefix["transformer."] = ""
full_output_folder, filename, counter, subfolder, filename_prefix_ = folder_paths.get_save_image_path(filename_prefix_, folder_paths.get_output_directory())
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
current_clip_sd = comfy.utils.state_dict_prefix_replace(current_clip_sd, replace_prefix)
comfy.utils.save_torch_file(current_clip_sd, output_checkpoint, metadata=metadata)
return io.NodeOutput()
class VAESave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="VAESave_V3",
category="advanced/model_merging",
is_output_node=True,
inputs=[
io.Vae.Input("vae"),
io.String.Input("filename_prefix", default="vae/ComfyUI_vae")
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo]
)
@classmethod
def execute(cls, vae, filename_prefix):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
prompt_info = ""
if cls.hidden.prompt is not None:
prompt_info = json.dumps(cls.hidden.prompt)
metadata = {}
if not args.disable_metadata:
metadata["prompt"] = prompt_info
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
comfy.utils.save_torch_file(vae.get_sd(), output_checkpoint, metadata=metadata)
return io.NodeOutput()
class ModelSave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ModelSave_V3",
category="advanced/model_merging",
is_output_node=True,
inputs=[
io.Model.Input("model"),
io.String.Input("filename_prefix", default="diffusion_models/ComfyUI")
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo]
)
@classmethod
def execute(cls, model, filename_prefix):
save_checkpoint(model, filename_prefix=filename_prefix, output_dir=folder_paths.get_output_directory(), prompt=cls.hidden.prompt, extra_pnginfo=cls.hidden.extra_pnginfo)
return io.NodeOutput()
NODES_LIST: list[type[io.ComfyNode]] = [
CheckpointSave,
CLIPAdd,
CLIPMergeSimple,
CLIPSave,
CLIPSubtract,
ModelAdd,
ModelMergeBlocks,
ModelMergeSimple,
ModelSave,
ModelSubtract,
VAESave,
]

View File

@ -0,0 +1,399 @@
from __future__ import annotations
from comfy_api.latest import io
from comfy_extras.v3.nodes_model_merging import ModelMergeBlocks
class ModelMergeSD1(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("time_embed.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("label_emb.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(12):
inputs.append(io.Float.Input(f"input_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(3):
inputs.append(io.Float.Input(f"middle_block.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(12):
inputs.append(io.Float.Input(f"output_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("out.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeSD1_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeSDXL(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("time_embed.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("label_emb.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(9):
inputs.append(io.Float.Input(f"input_blocks.{i}", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(3):
inputs.append(io.Float.Input(f"middle_block.{i}", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(9):
inputs.append(io.Float.Input(f"output_blocks.{i}", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("out.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeSDXL_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeSD3_2B(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embed.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("context_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("y_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(24):
inputs.append(io.Float.Input(f"joint_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeSD3_2B_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeAuraflow(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("init_x_linear.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("positional_encoding", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("cond_seq_linear.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("register_tokens", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(4):
inputs.append(io.Float.Input(f"double_layers.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(32):
inputs.append(io.Float.Input(f"single_layers.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.extend([
io.Float.Input("modF.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("final_linear.", default=1.0, min=0.0, max=1.0, step=0.01)
])
return io.Schema(
node_id="ModelMergeAuraflow_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeFlux1(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("img_in.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("time_in.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("guidance_in", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("vector_in.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("txt_in.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(19):
inputs.append(io.Float.Input(f"double_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
for i in range(38):
inputs.append(io.Float.Input(f"single_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeFlux1_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeSD35_Large(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embed.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("context_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("y_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(38):
inputs.append(io.Float.Input(f"joint_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeSD35_Large_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeMochiPreview(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_frequencies.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t5_y_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t5_yproj.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(48):
inputs.append(io.Float.Input(f"blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeMochiPreview_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeLTXV(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("patchify_proj.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("adaln_single.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("caption_projection.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(28):
inputs.append(io.Float.Input(f"transformer_blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.extend([
io.Float.Input("scale_shift_table", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("proj_out.", default=1.0, min=0.0, max=1.0, step=0.01)
])
return io.Schema(
node_id="ModelMergeLTXV_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeCosmos7B(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("extra_pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("affline_norm.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(28):
inputs.append(io.Float.Input(f"blocks.block{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeCosmos7B_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeCosmos14B(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("extra_pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("affline_norm.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(36):
inputs.append(io.Float.Input(f"blocks.block{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeCosmos14B_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeWAN2_1(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("patch_embedding.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("time_embedding.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("time_projection.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("text_embedding.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("img_emb.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(40):
inputs.append(io.Float.Input(f"blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("head.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeWAN2_1_V3",
category="advanced/model_merging/model_specific",
description="1.3B model has 30 blocks, 14B model has 40 blocks. Image to video model has the extra img_emb.",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeCosmosPredict2_2B(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedding_norm.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(28):
inputs.append(io.Float.Input(f"blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeCosmosPredict2_2B_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
class ModelMergeCosmosPredict2_14B(ModelMergeBlocks):
@classmethod
def define_schema(cls):
inputs = [
io.Model.Input("model1"),
io.Model.Input("model2"),
io.Float.Input("pos_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("x_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedder.", default=1.0, min=0.0, max=1.0, step=0.01),
io.Float.Input("t_embedding_norm.", default=1.0, min=0.0, max=1.0, step=0.01)
]
for i in range(36):
inputs.append(io.Float.Input(f"blocks.{i}.", default=1.0, min=0.0, max=1.0, step=0.01))
inputs.append(io.Float.Input("final_layer.", default=1.0, min=0.0, max=1.0, step=0.01))
return io.Schema(
node_id="ModelMergeCosmosPredict2_14B_V3",
category="advanced/model_merging/model_specific",
inputs=inputs,
outputs=[
io.Model.Output(),
]
)
NODES_LIST: list[type[io.ComfyNode]] = [
ModelMergeAuraflow,
ModelMergeCosmos14B,
ModelMergeCosmos7B,
ModelMergeCosmosPredict2_14B,
ModelMergeCosmosPredict2_2B,
ModelMergeFlux1,
ModelMergeLTXV,
ModelMergeMochiPreview,
ModelMergeSD1,
ModelMergeSD3_2B,
ModelMergeSD35_Large,
ModelMergeSDXL,
ModelMergeWAN2_1,
]

View File

@ -16,6 +16,47 @@ import comfy.model_management
from comfy_api.latest import io
class Morphology(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Morphology_V3",
display_name="ImageMorphology _V3",
category="image/postprocessing",
inputs=[
io.Image.Input("image"),
io.Combo.Input("operation", options=["erode", "dilate", "open", "close", "gradient", "bottom_hat", "top_hat"]),
io.Int.Input("kernel_size", default=3, min=3, max=999, step=1),
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def execute(cls, image, operation, kernel_size):
device = comfy.model_management.get_torch_device()
kernel = torch.ones(kernel_size, kernel_size, device=device)
image_k = image.to(device).movedim(-1, 1)
if operation == "erode":
output = erosion(image_k, kernel)
elif operation == "dilate":
output = dilation(image_k, kernel)
elif operation == "open":
output = opening(image_k, kernel)
elif operation == "close":
output = closing(image_k, kernel)
elif operation == "gradient":
output = gradient(image_k, kernel)
elif operation == "top_hat":
output = top_hat(image_k, kernel)
elif operation == "bottom_hat":
output = bottom_hat(image_k, kernel)
else:
raise ValueError(f"Invalid operation {operation} for morphology. Must be one of 'erode', 'dilate', 'open', 'close', 'gradient', 'tophat', 'bottomhat'")
return io.NodeOutput(output.to(comfy.model_management.intermediate_device()).movedim(1, -1))
class ImageRGBToYUV(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -60,48 +101,7 @@ class ImageYUVToRGB(io.ComfyNode):
return io.NodeOutput(kornia.color.ycbcr_to_rgb(image.movedim(-1, 1)).movedim(1, -1))
class Morphology(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Morphology_V3",
display_name="ImageMorphology _V3",
category="image/postprocessing",
inputs=[
io.Image.Input("image"),
io.Combo.Input("operation", options=["erode", "dilate", "open", "close", "gradient", "bottom_hat", "top_hat"]),
io.Int.Input("kernel_size", default=3, min=3, max=999, step=1),
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def execute(cls, image, operation, kernel_size):
device = comfy.model_management.get_torch_device()
kernel = torch.ones(kernel_size, kernel_size, device=device)
image_k = image.to(device).movedim(-1, 1)
if operation == "erode":
output = erosion(image_k, kernel)
elif operation == "dilate":
output = dilation(image_k, kernel)
elif operation == "open":
output = opening(image_k, kernel)
elif operation == "close":
output = closing(image_k, kernel)
elif operation == "gradient":
output = gradient(image_k, kernel)
elif operation == "top_hat":
output = top_hat(image_k, kernel)
elif operation == "bottom_hat":
output = bottom_hat(image_k, kernel)
else:
raise ValueError(f"Invalid operation {operation} for morphology. Must be one of 'erode', 'dilate', 'open', 'close', 'gradient', 'tophat', 'bottomhat'")
return io.NodeOutput(output.to(comfy.model_management.intermediate_device()).movedim(1, -1))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
ImageRGBToYUV,
ImageYUVToRGB,
Morphology,

View File

@ -59,4 +59,6 @@ class OptimalStepsScheduler(io.ComfyNode):
return io.NodeOutput(torch.FloatTensor(sigmas))
NODES_LIST = [OptimalStepsScheduler]
NODES_LIST: list[type[io.ComfyNode]] = [
OptimalStepsScheduler,
]

View File

@ -57,4 +57,6 @@ class PerturbedAttentionGuidance(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [PerturbedAttentionGuidance]
NODES_LIST: list[type[io.ComfyNode]] = [
PerturbedAttentionGuidance,
]

View File

@ -109,4 +109,6 @@ class PerpNegGuider(io.ComfyNode):
return io.NodeOutput(guider)
NODES_LIST = [PerpNegGuider]
NODES_LIST: list[type[io.ComfyNode]] = [
PerpNegGuider,
]

View File

@ -121,6 +121,32 @@ class PhotoMakerIDEncoder(comfy.clip_model.CLIPVisionModelProjection):
return self.fuse_module(prompt_embeds, id_embeds, class_tokens_mask)
class PhotoMakerLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PhotoMakerLoader_V3",
category="_for_testing/photomaker",
inputs=[
io.Combo.Input("photomaker_model_name", options=folder_paths.get_filename_list("photomaker")),
],
outputs=[
io.Photomaker.Output(),
],
is_experimental=True,
)
@classmethod
def execute(cls, photomaker_model_name):
photomaker_model_path = folder_paths.get_full_path_or_raise("photomaker", photomaker_model_name)
photomaker_model = PhotoMakerIDEncoder()
data = comfy.utils.load_torch_file(photomaker_model_path, safe_load=True)
if "id_encoder" in data:
data = data["id_encoder"]
photomaker_model.load_state_dict(data)
return io.NodeOutput(photomaker_model)
class PhotoMakerEncode(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -173,33 +199,7 @@ class PhotoMakerEncode(io.ComfyNode):
return io.NodeOutput([[out, {"pooled_output": pooled}]])
class PhotoMakerLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PhotoMakerLoader_V3",
category="_for_testing/photomaker",
inputs=[
io.Combo.Input("photomaker_model_name", options=folder_paths.get_filename_list("photomaker")),
],
outputs=[
io.Photomaker.Output(),
],
is_experimental=True,
)
@classmethod
def execute(cls, photomaker_model_name):
photomaker_model_path = folder_paths.get_full_path_or_raise("photomaker", photomaker_model_name)
photomaker_model = PhotoMakerIDEncoder()
data = comfy.utils.load_torch_file(photomaker_model_path, safe_load=True)
if "id_encoder" in data:
data = data["id_encoder"]
photomaker_model.load_state_dict(data)
return io.NodeOutput(photomaker_model)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
PhotoMakerEncode,
PhotoMakerLoader,
]

View File

@ -28,6 +28,6 @@ class CLIPTextEncodePixArtAlpha(io.ComfyNode):
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens, add_dict={"width": width, "height": height}))
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodePixArtAlpha,
]

View File

@ -13,13 +13,6 @@ import node_helpers
from comfy_api.latest import io
def gaussian_kernel(kernel_size: int, sigma: float, device=None):
x, y = torch.meshgrid(torch.linspace(-1, 1, kernel_size, device=device), torch.linspace(-1, 1, kernel_size, device=device), indexing="ij")
d = torch.sqrt(x * x + y * y)
g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
return g / g.sum()
class Blend(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -109,36 +102,11 @@ class Blur(io.ComfyNode):
return io.NodeOutput(blurred.to(comfy.model_management.intermediate_device()))
class ImageScaleToTotalPixels(io.ComfyNode):
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
crop_methods = ["disabled", "center"]
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageScaleToTotalPixels_V3",
category="image/upscaling",
inputs=[
io.Image.Input("image"),
io.Combo.Input("upscale_method", options=cls.upscale_methods),
io.Float.Input("megapixels", default=1.0, min=0.01, max=16.0, step=0.01),
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def execute(cls, image, upscale_method, megapixels):
samples = image.movedim(-1,1)
total = int(megapixels * 1024 * 1024)
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
width = round(samples.shape[3] * scale_by)
height = round(samples.shape[2] * scale_by)
s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
return io.NodeOutput(s.movedim(1,-1))
def gaussian_kernel(kernel_size: int, sigma: float, device=None):
x, y = torch.meshgrid(torch.linspace(-1, 1, kernel_size, device=device), torch.linspace(-1, 1, kernel_size, device=device), indexing="ij")
d = torch.sqrt(x * x + y * y)
g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
return g / g.sum()
class Quantize(io.ComfyNode):
@ -246,7 +214,39 @@ class Sharpen(io.ComfyNode):
return io.NodeOutput(result.to(comfy.model_management.intermediate_device()))
NODES_LIST = [
class ImageScaleToTotalPixels(io.ComfyNode):
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
crop_methods = ["disabled", "center"]
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageScaleToTotalPixels_V3",
category="image/upscaling",
inputs=[
io.Image.Input("image"),
io.Combo.Input("upscale_method", options=cls.upscale_methods),
io.Float.Input("megapixels", default=1.0, min=0.01, max=16.0, step=0.01),
],
outputs=[
io.Image.Output(),
],
)
@classmethod
def execute(cls, image, upscale_method, megapixels):
samples = image.movedim(-1,1)
total = int(megapixels * 1024 * 1024)
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
width = round(samples.shape[3] * scale_by)
height = round(samples.shape[2] * scale_by)
s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
return io.NodeOutput(s.movedim(1,-1))
NODES_LIST: list[type[io.ComfyNode]] = [
Blend,
Blur,
ImageScaleToTotalPixels,

View File

@ -142,7 +142,7 @@ class LatentRebatch(io.ComfyNode):
return io.NodeOutput(output_list)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
ImageRebatch,
LatentRebatch,
]

View File

@ -186,4 +186,6 @@ class SelfAttentionGuidance(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [SelfAttentionGuidance]
NODES_LIST: list[type[io.ComfyNode]] = [
SelfAttentionGuidance,
]

View File

@ -10,6 +10,59 @@ from comfy_api.latest import io
from comfy_extras.v3.nodes_slg import SkipLayerGuidanceDiT
class TripleCLIPLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TripleCLIPLoader_V3",
category="advanced/loaders",
description="[Recipes]\n\nsd3: clip-l, clip-g, t5",
inputs=[
io.Combo.Input("clip_name1", options=folder_paths.get_filename_list("text_encoders")),
io.Combo.Input("clip_name2", options=folder_paths.get_filename_list("text_encoders")),
io.Combo.Input("clip_name3", options=folder_paths.get_filename_list("text_encoders")),
],
outputs=[
io.Clip.Output(),
],
)
@classmethod
def execute(cls, clip_name1: str, clip_name2: str, clip_name3: str):
clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)
clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)
clip_path3 = folder_paths.get_full_path_or_raise("text_encoders", clip_name3)
clip = comfy.sd.load_clip(
ckpt_paths=[clip_path1, clip_path2, clip_path3],
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
return io.NodeOutput(clip)
class EmptySD3LatentImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptySD3LatentImage_V3",
category="latent/sd3",
inputs=[
io.Int.Input("width", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("height", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, width: int, height: int, batch_size=1):
latent = torch.zeros(
[batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device()
)
return io.NodeOutput({"samples":latent})
class CLIPTextEncodeSD3(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -54,30 +107,6 @@ class CLIPTextEncodeSD3(io.ComfyNode):
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
class EmptySD3LatentImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptySD3LatentImage_V3",
category="latent/sd3",
inputs=[
io.Int.Input("width", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("height", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
io.Latent.Output(),
],
)
@classmethod
def execute(cls, width: int, height: int, batch_size=1):
latent = torch.zeros(
[batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device()
)
return io.NodeOutput({"samples":latent})
class SkipLayerGuidanceSD3(SkipLayerGuidanceDiT):
"""
Enhance guidance towards detailed dtructure by having another set of CFG negative with skipped layers.
@ -108,36 +137,7 @@ class SkipLayerGuidanceSD3(SkipLayerGuidanceDiT):
model=model, scale=scale, start_percent=start_percent, end_percent=end_percent, double_layers=layers
)
class TripleCLIPLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TripleCLIPLoader_V3",
category="advanced/loaders",
description="[Recipes]\n\nsd3: clip-l, clip-g, t5",
inputs=[
io.Combo.Input("clip_name1", options=folder_paths.get_filename_list("text_encoders")),
io.Combo.Input("clip_name2", options=folder_paths.get_filename_list("text_encoders")),
io.Combo.Input("clip_name3", options=folder_paths.get_filename_list("text_encoders")),
],
outputs=[
io.Clip.Output(),
],
)
@classmethod
def execute(cls, clip_name1: str, clip_name2: str, clip_name3: str):
clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)
clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)
clip_path3 = folder_paths.get_full_path_or_raise("text_encoders", clip_name3)
clip = comfy.sd.load_clip(
ckpt_paths=[clip_path1, clip_path2, clip_path3],
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
return io.NodeOutput(clip)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
CLIPTextEncodeSD3,
EmptySD3LatentImage,
SkipLayerGuidanceSD3,

View File

@ -53,4 +53,6 @@ class SD_4XUpscale_Conditioning(io.ComfyNode):
latent = torch.zeros([images.shape[0], 4, height // 4, width // 4])
return io.NodeOutput(out_cp, out_cn, {"samples":latent})
NODES_LIST = [SD_4XUpscale_Conditioning]
NODES_LIST: list[type[io.ComfyNode]] = [
SD_4XUpscale_Conditioning,
]

View File

@ -167,7 +167,7 @@ class SkipLayerGuidanceDiTSimple(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
SkipLayerGuidanceDiT,
SkipLayerGuidanceDiTSimple,
]

View File

@ -0,0 +1,165 @@
from __future__ import annotations
import torch
import comfy.utils
import nodes
from comfy_api.latest import io
def camera_embeddings(elevation, azimuth):
elevation = torch.as_tensor([elevation])
azimuth = torch.as_tensor([azimuth])
embeddings = torch.stack(
[
torch.deg2rad(
(90 - elevation) - 90
), # Zero123 polar is 90-elevation
torch.sin(torch.deg2rad(azimuth)),
torch.cos(torch.deg2rad(azimuth)),
torch.deg2rad(
90 - torch.full_like(elevation, 0)
),
], dim=-1).unsqueeze(1)
return embeddings
class StableZero123_Conditioning(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StableZero123_Conditioning_V3",
category="conditioning/3d_models",
inputs=[
io.ClipVision.Input("clip_vision"),
io.Image.Input("init_image"),
io.Vae.Input("vae"),
io.Int.Input("width", default=256, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=256, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Float.Input("elevation", default=0.0, min=-180.0, max=180.0, step=0.1, round=False),
io.Float.Input("azimuth", default=0.0, min=-180.0, max=180.0, step=0.1, round=False)
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent")
]
)
@classmethod
def execute(cls, clip_vision, init_image, vae, width, height, batch_size, elevation, azimuth):
output = clip_vision.encode_image(init_image)
pooled = output.image_embeds.unsqueeze(0)
pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1)
encode_pixels = pixels[:,:,:,:3]
t = vae.encode(encode_pixels)
cam_embeds = camera_embeddings(elevation, azimuth)
cond = torch.cat([pooled, cam_embeds.to(pooled.device).repeat((pooled.shape[0], 1, 1))], dim=-1)
positive = [[cond, {"concat_latent_image": t}]]
negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t)}]]
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
return io.NodeOutput(positive, negative, {"samples":latent})
class StableZero123_Conditioning_Batched(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StableZero123_Conditioning_Batched_V3",
category="conditioning/3d_models",
inputs=[
io.ClipVision.Input("clip_vision"),
io.Image.Input("init_image"),
io.Vae.Input("vae"),
io.Int.Input("width", default=256, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=256, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.Float.Input("elevation", default=0.0, min=-180.0, max=180.0, step=0.1, round=False),
io.Float.Input("azimuth", default=0.0, min=-180.0, max=180.0, step=0.1, round=False),
io.Float.Input("elevation_batch_increment", default=0.0, min=-180.0, max=180.0, step=0.1, round=False),
io.Float.Input("azimuth_batch_increment", default=0.0, min=-180.0, max=180.0, step=0.1, round=False)
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent")
]
)
@classmethod
def execute(cls, clip_vision, init_image, vae, width, height, batch_size, elevation, azimuth, elevation_batch_increment, azimuth_batch_increment):
output = clip_vision.encode_image(init_image)
pooled = output.image_embeds.unsqueeze(0)
pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1)
encode_pixels = pixels[:,:,:,:3]
t = vae.encode(encode_pixels)
cam_embeds = []
for i in range(batch_size):
cam_embeds.append(camera_embeddings(elevation, azimuth))
elevation += elevation_batch_increment
azimuth += azimuth_batch_increment
cam_embeds = torch.cat(cam_embeds, dim=0)
cond = torch.cat([comfy.utils.repeat_to_batch_size(pooled, batch_size), cam_embeds], dim=-1)
positive = [[cond, {"concat_latent_image": t}]]
negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t)}]]
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
return io.NodeOutput(positive, negative, {"samples":latent, "batch_index": [0] * batch_size})
class SV3D_Conditioning(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SV3D_Conditioning_V3",
category="conditioning/3d_models",
inputs=[
io.ClipVision.Input("clip_vision"),
io.Image.Input("init_image"),
io.Vae.Input("vae"),
io.Int.Input("width", default=576, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("height", default=576, min=16, max=nodes.MAX_RESOLUTION, step=8),
io.Int.Input("video_frames", default=21, min=1, max=4096),
io.Float.Input("elevation", default=0.0, min=-90.0, max=90.0, step=0.1, round=False)
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent")
]
)
@classmethod
def execute(cls, clip_vision, init_image, vae, width, height, video_frames, elevation):
output = clip_vision.encode_image(init_image)
pooled = output.image_embeds.unsqueeze(0)
pixels = comfy.utils.common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1)
encode_pixels = pixels[:,:,:,:3]
t = vae.encode(encode_pixels)
azimuth = 0
azimuth_increment = 360 / (max(video_frames, 2) - 1)
elevations = []
azimuths = []
for i in range(video_frames):
elevations.append(elevation)
azimuths.append(azimuth)
azimuth += azimuth_increment
positive = [[pooled, {"concat_latent_image": t, "elevation": elevations, "azimuth": azimuths}]]
negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t), "elevation": elevations, "azimuth": azimuths}]]
latent = torch.zeros([video_frames, 4, height // 8, width // 8])
return io.NodeOutput(positive, negative, {"samples":latent})
NODES_LIST: list[type[io.ComfyNode]] = [
StableZero123_Conditioning,
StableZero123_Conditioning_Batched,
SV3D_Conditioning,
]

View File

@ -0,0 +1,380 @@
from __future__ import annotations
import re
from comfy_api.latest import io
class StringConcatenate(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringConcatenate_V3",
display_name="Concatenate _V3",
category="utils/string",
inputs=[
io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True),
io.String.Input("delimiter", multiline=False, default="")
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string_a, string_b, delimiter):
return io.NodeOutput(delimiter.join((string_a, string_b)))
class StringSubstring(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringSubstring_V3",
display_name="Substring _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.Int.Input("start"),
io.Int.Input("end")
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, start, end):
return io.NodeOutput(string[start:end])
class StringLength(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringLength_V3",
display_name="Length _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True)
],
outputs=[
io.Int.Output(display_name="length")
]
)
@classmethod
def execute(cls, string):
return io.NodeOutput(len(string))
class CaseConverter(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CaseConverter_V3",
display_name="Case Converter _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"])
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, mode):
if mode == "UPPERCASE":
result = string.upper()
elif mode == "lowercase":
result = string.lower()
elif mode == "Capitalize":
result = string.capitalize()
elif mode == "Title Case":
result = string.title()
else:
result = string
return io.NodeOutput(result)
class StringTrim(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringTrim_V3",
display_name="Trim _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["Both", "Left", "Right"])
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, mode):
if mode == "Both":
result = string.strip()
elif mode == "Left":
result = string.lstrip()
elif mode == "Right":
result = string.rstrip()
else:
result = string
return io.NodeOutput(result)
class StringReplace(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringReplace_V3",
display_name="Replace _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("find", multiline=True),
io.String.Input("replace", multiline=True)
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, find, replace):
return io.NodeOutput(string.replace(find, replace))
class StringContains(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringContains_V3",
display_name="Contains _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("substring", multiline=True),
io.Boolean.Input("case_sensitive", default=True)
],
outputs=[
io.Boolean.Output(display_name="contains")
]
)
@classmethod
def execute(cls, string, substring, case_sensitive):
if case_sensitive:
contains = substring in string
else:
contains = substring.lower() in string.lower()
return io.NodeOutput(contains)
class StringCompare(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringCompare_V3",
display_name="Compare _V3",
category="utils/string",
inputs=[
io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True),
io.Combo.Input("mode", options=["Starts With", "Ends With", "Equal"]),
io.Boolean.Input("case_sensitive", default=True)
],
outputs=[
io.Boolean.Output()
]
)
@classmethod
def execute(cls, string_a, string_b, mode, case_sensitive):
if case_sensitive:
a = string_a
b = string_b
else:
a = string_a.lower()
b = string_b.lower()
if mode == "Equal":
return io.NodeOutput(a == b)
elif mode == "Starts With":
return io.NodeOutput(a.startswith(b))
elif mode == "Ends With":
return io.NodeOutput(a.endswith(b))
class RegexMatch(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RegexMatch_V3",
display_name="Regex Match _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True),
io.Boolean.Input("case_insensitive", default=True),
io.Boolean.Input("multiline", default=False),
io.Boolean.Input("dotall", default=False)
],
outputs=[
io.Boolean.Output(display_name="matches")
]
)
@classmethod
def execute(cls, string, regex_pattern, case_insensitive, multiline, dotall):
flags = 0
if case_insensitive:
flags |= re.IGNORECASE
if multiline:
flags |= re.MULTILINE
if dotall:
flags |= re.DOTALL
try:
match = re.search(regex_pattern, string, flags)
result = match is not None
except re.error:
result = False
return io.NodeOutput(result)
class RegexExtract(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RegexExtract_V3",
display_name="Regex Extract _V3",
category="utils/string",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True),
io.Combo.Input("mode", options=["First Match", "All Matches", "First Group", "All Groups"]),
io.Boolean.Input("case_insensitive", default=True),
io.Boolean.Input("multiline", default=False),
io.Boolean.Input("dotall", default=False),
io.Int.Input("group_index", default=1, min=0, max=100)
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, regex_pattern, mode, case_insensitive, multiline, dotall, group_index):
join_delimiter = "\n"
flags = 0
if case_insensitive:
flags |= re.IGNORECASE
if multiline:
flags |= re.MULTILINE
if dotall:
flags |= re.DOTALL
try:
if mode == "First Match":
match = re.search(regex_pattern, string, flags)
if match:
result = match.group(0)
else:
result = ""
elif mode == "All Matches":
matches = re.findall(regex_pattern, string, flags)
if matches:
if isinstance(matches[0], tuple):
result = join_delimiter.join([m[0] for m in matches])
else:
result = join_delimiter.join(matches)
else:
result = ""
elif mode == "First Group":
match = re.search(regex_pattern, string, flags)
if match and len(match.groups()) >= group_index:
result = match.group(group_index)
else:
result = ""
elif mode == "All Groups":
matches = re.finditer(regex_pattern, string, flags)
results = []
for match in matches:
if match.groups() and len(match.groups()) >= group_index:
results.append(match.group(group_index))
result = join_delimiter.join(results)
else:
result = ""
except re.error:
result = ""
return io.NodeOutput(result)
class RegexReplace(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RegexReplace_V3",
display_name="Regex Replace _V3",
category="utils/string",
description="Find and replace text using regex patterns.",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True),
io.String.Input("replace", multiline=True),
io.Boolean.Input("case_insensitive", default=True, optional=True),
io.Boolean.Input("multiline", default=False, optional=True),
io.Boolean.Input("dotall", default=False, optional=True, tooltip="When enabled, the dot (.) character will match any character including newline characters. When disabled, dots won't match newlines."),
io.Int.Input("count", default=0, min=0, max=100, optional=True, tooltip="Maximum number of replacements to make. Set to 0 to replace all occurrences (default). Set to 1 to replace only the first match, 2 for the first two matches, etc.")
],
outputs=[
io.String.Output()
]
)
@classmethod
def execute(cls, string, regex_pattern, replace, case_insensitive=True, multiline=False, dotall=False, count=0):
flags = 0
if case_insensitive:
flags |= re.IGNORECASE
if multiline:
flags |= re.MULTILINE
if dotall:
flags |= re.DOTALL
result = re.sub(regex_pattern, replace, string, count=count, flags=flags)
return io.NodeOutput(result)
NODES_LIST: list[type[io.ComfyNode]] = [
CaseConverter,
RegexExtract,
RegexMatch,
RegexReplace,
StringCompare,
StringConcatenate,
StringContains,
StringLength,
StringReplace,
StringSubstring,
StringTrim,
]

View File

@ -65,6 +65,6 @@ class TCFG(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
TCFG,
]

View File

@ -185,6 +185,6 @@ class TomePatchModel(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
TomePatchModel,
]

View File

@ -27,6 +27,6 @@ class TorchCompileModel(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
TorchCompileModel,
]

View File

@ -162,57 +162,6 @@ def load_and_process_images(image_files, input_dir, resize_method="None", w=None
return torch.cat(output_images, dim=0)
def draw_loss_graph(loss_map, steps):
width, height = 500, 300
img = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(img)
min_loss, max_loss = min(loss_map.values()), max(loss_map.values())
scaled_loss = [(l_v - min_loss) / (max_loss - min_loss) for l_v in loss_map.values()]
prev_point = (0, height - int(scaled_loss[0] * height))
for i, l_v in enumerate(scaled_loss[1:], start=1):
x = int(i / (steps - 1) * width)
y = height - int(l_v * height)
draw.line([prev_point, (x, y)], fill="blue", width=2)
prev_point = (x, y)
return img
def find_all_highest_child_module_with_forward(model: torch.nn.Module, result = None, name = None):
if result is None:
result = []
elif hasattr(model, "forward") and not isinstance(model, (torch.nn.ModuleList, torch.nn.Sequential, torch.nn.ModuleDict)):
result.append(model)
logging.debug(f"Found module with forward: {name} ({model.__class__.__name__})")
return result
name = name or "root"
for next_name, child in model.named_children():
find_all_highest_child_module_with_forward(child, result, f"{name}.{next_name}")
return result
def patch(m):
if not hasattr(m, "forward"):
return
org_forward = m.forward
def fwd(args, kwargs):
return org_forward(*args, **kwargs)
def checkpointing_fwd(*args, **kwargs):
return torch.utils.checkpoint.checkpoint(
fwd, args, kwargs, use_reentrant=False
)
m.org_forward = org_forward
m.forward = checkpointing_fwd
def unpatch(m):
if hasattr(m, "org_forward"):
m.forward = m.org_forward
del m.org_forward
class LoadImageSetFromFolderNode(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -328,126 +277,55 @@ class LoadImageTextSetFromFolderNode(io.ComfyNode):
return io.NodeOutput(output_tensor, conditions)
class LoraModelLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LoraModelLoader_V3",
display_name="Load LoRA Model _V3",
category="loaders",
description="Load Trained LoRA weights from Train LoRA node.",
is_experimental=True,
inputs=[
io.Model.Input("model", tooltip="The diffusion model the LoRA will be applied to."),
io.LoraModel.Input("lora", tooltip="The LoRA model to apply to the diffusion model."),
io.Float.Input("strength_model", default=1.0, min=-100.0, max=100.0, step=0.01, tooltip="How strongly to modify the diffusion model. This value can be negative."),
],
outputs=[
io.Model.Output(tooltip="The modified diffusion model."),
],
)
@classmethod
def execute(cls, model, lora, strength_model):
if strength_model == 0:
return io.NodeOutput(model)
model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0)
return io.NodeOutput(model_lora)
class LossGraphNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LossGraphNode_V3",
display_name="Plot Loss Graph _V3",
category="training",
description="Plots the loss graph and saves it to the output directory.",
is_experimental=True,
is_output_node=True,
inputs=[
io.LossMap.Input("loss"), # TODO: original V1 node has also `default={}` parameter
io.String.Input("filename_prefix", default="loss_graph"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, loss, filename_prefix):
loss_values = loss["loss"]
width, height = 800, 480
margin = 40
img = Image.new(
"RGB", (width + margin, height + margin), "white"
) # Extend canvas
def draw_loss_graph(loss_map, steps):
width, height = 500, 300
img = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(img)
min_loss, max_loss = min(loss_values), max(loss_values)
scaled_loss = [(l_v - min_loss) / (max_loss - min_loss) for l_v in loss_values]
min_loss, max_loss = min(loss_map.values()), max(loss_map.values())
scaled_loss = [(l_v - min_loss) / (max_loss - min_loss) for l_v in loss_map.values()]
steps = len(loss_values)
prev_point = (margin, height - int(scaled_loss[0] * height))
prev_point = (0, height - int(scaled_loss[0] * height))
for i, l_v in enumerate(scaled_loss[1:], start=1):
x = margin + int(i / steps * width) # Scale X properly
x = int(i / (steps - 1) * width)
y = height - int(l_v * height)
draw.line([prev_point, (x, y)], fill="blue", width=2)
prev_point = (x, y)
draw.line([(margin, 0), (margin, height)], fill="black", width=2) # Y-axis
draw.line(
[(margin, height), (width + margin, height)], fill="black", width=2
) # X-axis
return img
try:
font = ImageFont.truetype("arial.ttf", 12)
except IOError:
font = ImageFont.load_default()
# Add axis labels
draw.text((5, height // 2), "Loss", font=font, fill="black")
draw.text((width // 2, height + 10), "Steps", font=font, fill="black")
def find_all_highest_child_module_with_forward(model: torch.nn.Module, result = None, name = None):
if result is None:
result = []
elif hasattr(model, "forward") and not isinstance(model, (torch.nn.ModuleList, torch.nn.Sequential, torch.nn.ModuleDict)):
result.append(model)
logging.debug(f"Found module with forward: {name} ({model.__class__.__name__})")
return result
name = name or "root"
for next_name, child in model.named_children():
find_all_highest_child_module_with_forward(child, result, f"{name}.{next_name}")
return result
# Add min/max loss values
draw.text((margin - 30, 0), f"{max_loss:.2f}", font=font, fill="black")
draw.text(
(margin - 30, height - 10), f"{min_loss:.2f}", font=font, fill="black"
def patch(m):
if not hasattr(m, "forward"):
return
org_forward = m.forward
def fwd(args, kwargs):
return org_forward(*args, **kwargs)
def checkpointing_fwd(*args, **kwargs):
return torch.utils.checkpoint.checkpoint(
fwd, args, kwargs, use_reentrant=False
)
return io.NodeOutput(ui=ui.PreviewImage(img, cls=cls))
m.org_forward = org_forward
m.forward = checkpointing_fwd
class SaveLoRA(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveLoRA_V3",
display_name="Save LoRA Weights _V3",
category="loaders",
is_experimental=True,
is_output_node=True,
inputs=[
io.LoraModel.Input("lora", tooltip="The LoRA model to save. Do not use the model with LoRA layers."),
io.String.Input("prefix", default="loras/ComfyUI_trained_lora", tooltip="The prefix to use for the saved LoRA file."),
io.Int.Input("steps", tooltip="Optional: The number of steps to LoRA has been trained for, used to name the saved file.", optional=True),
],
outputs=[],
)
@classmethod
def execute(cls, lora, prefix, steps=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
prefix, folder_paths.get_output_directory()
)
if steps is None:
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
else:
output_checkpoint = f"{filename}_{steps}_steps_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
safetensors.torch.save_file(lora, output_checkpoint)
return io.NodeOutput()
def unpatch(m):
if hasattr(m, "org_forward"):
m.forward = m.org_forward
del m.org_forward
class TrainLoraNode(io.ComfyNode):
@ -656,7 +534,129 @@ class TrainLoraNode(io.ComfyNode):
return io.NodeOutput(mp, lora_sd, loss_map, steps + existing_steps)
NODES_LIST = [
class LoraModelLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LoraModelLoader_V3",
display_name="Load LoRA Model _V3",
category="loaders",
description="Load Trained LoRA weights from Train LoRA node.",
is_experimental=True,
inputs=[
io.Model.Input("model", tooltip="The diffusion model the LoRA will be applied to."),
io.LoraModel.Input("lora", tooltip="The LoRA model to apply to the diffusion model."),
io.Float.Input("strength_model", default=1.0, min=-100.0, max=100.0, step=0.01, tooltip="How strongly to modify the diffusion model. This value can be negative."),
],
outputs=[
io.Model.Output(tooltip="The modified diffusion model."),
],
)
@classmethod
def execute(cls, model, lora, strength_model):
if strength_model == 0:
return io.NodeOutput(model)
model_lora, _ = comfy.sd.load_lora_for_models(model, None, lora, strength_model, 0)
return io.NodeOutput(model_lora)
class SaveLoRA(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveLoRA_V3",
display_name="Save LoRA Weights _V3",
category="loaders",
is_experimental=True,
is_output_node=True,
inputs=[
io.LoraModel.Input("lora", tooltip="The LoRA model to save. Do not use the model with LoRA layers."),
io.String.Input("prefix", default="loras/ComfyUI_trained_lora", tooltip="The prefix to use for the saved LoRA file."),
io.Int.Input("steps", tooltip="Optional: The number of steps to LoRA has been trained for, used to name the saved file.", optional=True),
],
outputs=[],
)
@classmethod
def execute(cls, lora, prefix, steps=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
prefix, folder_paths.get_output_directory()
)
if steps is None:
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
else:
output_checkpoint = f"{filename}_{steps}_steps_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
safetensors.torch.save_file(lora, output_checkpoint)
return io.NodeOutput()
class LossGraphNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="LossGraphNode_V3",
display_name="Plot Loss Graph _V3",
category="training",
description="Plots the loss graph and saves it to the output directory.",
is_experimental=True,
is_output_node=True,
inputs=[
io.LossMap.Input("loss"), # TODO: original V1 node has also `default={}` parameter
io.String.Input("filename_prefix", default="loss_graph"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, loss, filename_prefix):
loss_values = loss["loss"]
width, height = 800, 480
margin = 40
img = Image.new(
"RGB", (width + margin, height + margin), "white"
) # Extend canvas
draw = ImageDraw.Draw(img)
min_loss, max_loss = min(loss_values), max(loss_values)
scaled_loss = [(l_v - min_loss) / (max_loss - min_loss) for l_v in loss_values]
steps = len(loss_values)
prev_point = (margin, height - int(scaled_loss[0] * height))
for i, l_v in enumerate(scaled_loss[1:], start=1):
x = margin + int(i / steps * width) # Scale X properly
y = height - int(l_v * height)
draw.line([prev_point, (x, y)], fill="blue", width=2)
prev_point = (x, y)
draw.line([(margin, 0), (margin, height)], fill="black", width=2) # Y-axis
draw.line(
[(margin, height), (width + margin, height)], fill="black", width=2
) # X-axis
try:
font = ImageFont.truetype("arial.ttf", 12)
except IOError:
font = ImageFont.load_default()
# Add axis labels
draw.text((5, height // 2), "Loss", font=font, fill="black")
draw.text((width // 2, height + 10), "Steps", font=font, fill="black")
# Add min/max loss values
draw.text((margin - 30, 0), f"{max_loss:.2f}", font=font, fill="black")
draw.text(
(margin - 30, height - 10), f"{min_loss:.2f}", font=font, fill="black"
)
return io.NodeOutput(ui=ui.PreviewImage(img, cls=cls))
NODES_LIST: list[type[io.ComfyNode]] = [
LoadImageSetFromFolderNode,
LoadImageTextSetFromFolderNode,
LoraModelLoader,

View File

@ -19,6 +19,35 @@ except Exception:
pass
class UpscaleModelLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="UpscaleModelLoader_V3",
display_name="Load Upscale Model _V3",
category="loaders",
inputs=[
io.Combo.Input("model_name", options=folder_paths.get_filename_list("upscale_models")),
],
outputs=[
io.UpscaleModel.Output(),
],
)
@classmethod
def execute(cls, model_name):
model_path = folder_paths.get_full_path_or_raise("upscale_models", model_name)
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
if "module.layers.0.residual_group.blocks.0.norm1.weight" in sd:
sd = comfy.utils.state_dict_prefix_replace(sd, {"module.":""})
out = ModelLoader().load_from_state_dict(sd).eval()
if not isinstance(out, ImageModelDescriptor):
raise Exception("Upscale model must be a single-image model.")
return io.NodeOutput(out)
class ImageUpscaleWithModel(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -71,36 +100,7 @@ class ImageUpscaleWithModel(io.ComfyNode):
return io.NodeOutput(s)
class UpscaleModelLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="UpscaleModelLoader_V3",
display_name="Load Upscale Model _V3",
category="loaders",
inputs=[
io.Combo.Input("model_name", options=folder_paths.get_filename_list("upscale_models")),
],
outputs=[
io.UpscaleModel.Output(),
],
)
@classmethod
def execute(cls, model_name):
model_path = folder_paths.get_full_path_or_raise("upscale_models", model_name)
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
if "module.layers.0.residual_group.blocks.0.norm1.weight" in sd:
sd = comfy.utils.state_dict_prefix_replace(sd, {"module.":""})
out = ModelLoader().load_from_state_dict(sd).eval()
if not isinstance(out, ImageModelDescriptor):
raise Exception("Upscale model must be a single-image model.")
return io.NodeOutput(out)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
ImageUpscaleWithModel,
UpscaleModelLoader,
]

View File

@ -15,6 +15,108 @@ from comfy_api.latest import io, ui
from comfy_api.util import VideoCodec, VideoComponents, VideoContainer
class SaveWEBM(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveWEBM_V3",
category="image/video",
is_experimental=True,
inputs=[
io.Image.Input("images"),
io.String.Input("filename_prefix", default="ComfyUI"),
io.Combo.Input("codec", options=["vp9", "av1"]),
io.Float.Input("fps", default=24.0, min=0.01, max=1000.0, step=0.01),
io.Float.Input("crf", default=32.0, min=0, max=63.0, step=1, tooltip="Higher crf means lower quality with a smaller file size, lower crf means higher quality higher filesize."),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, images, codec, fps, filename_prefix, crf):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
)
file = f"{filename}_{counter:05}_.webm"
container = av.open(os.path.join(full_output_folder, file), mode="w")
if cls.hidden.prompt is not None:
container.metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
container.metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
codec_map = {"vp9": "libvpx-vp9", "av1": "libsvtav1"}
stream = container.add_stream(codec_map[codec], rate=Fraction(round(fps * 1000), 1000))
stream.width = images.shape[-2]
stream.height = images.shape[-3]
stream.pix_fmt = "yuv420p10le" if codec == "av1" else "yuv420p"
stream.bit_rate = 0
stream.options = {'crf': str(crf)}
if codec == "av1":
stream.options["preset"] = "6"
for frame in images:
frame = av.VideoFrame.from_ndarray(torch.clamp(frame[..., :3] * 255, min=0, max=255).to(device=torch.device("cpu"), dtype=torch.uint8).numpy(), format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
container.mux(stream.encode())
container.close()
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
class SaveVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveVideo_V3",
display_name="Save Video _V3",
category="image/video",
description="Saves the input images to your ComfyUI output directory.",
inputs=[
io.Video.Input("video", tooltip="The video to save."),
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
io.Combo.Input("format", options=VideoContainer.as_input(), default="auto", tooltip="The format to save the video as."),
io.Combo.Input("codec", options=VideoCodec.as_input(), default="auto", tooltip="The codec to use for the video."),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, video: VideoInput, filename_prefix, format, codec):
width, height = video.get_dimensions()
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix,
folder_paths.get_output_directory(),
width,
height
)
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 len(metadata) > 0:
saved_metadata = metadata
file = f"{filename}_{counter:05}_.{VideoContainer.get_extension(format)}"
video.save_to(
os.path.join(full_output_folder, file),
format=format,
codec=codec,
metadata=saved_metadata
)
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
class CreateVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -35,13 +137,9 @@ class CreateVideo(io.ComfyNode):
@classmethod
def execute(cls, images: ImageInput, fps: float, audio: AudioInput = None):
return io.NodeOutput(VideoFromComponents(
VideoComponents(
images=images,
audio=audio,
frame_rate=Fraction(fps),
return io.NodeOutput(
VideoFromComponents(VideoComponents(images=images, audio=audio, frame_rate=Fraction(fps)))
)
))
class GetVideoComponents(io.ComfyNode):
@ -105,106 +203,10 @@ class LoadVideo(io.ComfyNode):
return True
class SaveVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveVideo_V3",
display_name="Save Video _V3",
category="image/video",
description="Saves the input images to your ComfyUI output directory.",
inputs=[
io.Video.Input("video", tooltip="The video to save."),
io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."),
io.Combo.Input("format", options=VideoContainer.as_input(), default="auto", tooltip="The format to save the video as."),
io.Combo.Input("codec", options=VideoCodec.as_input(), default="auto", tooltip="The codec to use for the video."),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, video: VideoInput, filename_prefix, format, codec):
width, height = video.get_dimensions()
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix,
folder_paths.get_output_directory(),
width,
height
)
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 len(metadata) > 0:
saved_metadata = metadata
file = f"{filename}_{counter:05}_.{VideoContainer.get_extension(format)}"
video.save_to(
os.path.join(full_output_folder, file),
format=format,
codec=codec,
metadata=saved_metadata
)
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
class SaveWEBM(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveWEBM_V3",
category="image/video",
is_experimental=True,
inputs=[
io.Image.Input("images"),
io.String.Input("filename_prefix", default="ComfyUI"),
io.Combo.Input("codec", options=["vp9", "av1"]),
io.Float.Input("fps", default=24.0, min=0.01, max=1000.0, step=0.01),
io.Float.Input("crf", default=32.0, min=0, max=63.0, step=1, tooltip="Higher crf means lower quality with a smaller file size, lower crf means higher quality higher filesize."),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True,
)
@classmethod
def execute(cls, images, codec, fps, filename_prefix, crf):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
)
file = f"{filename}_{counter:05}_.webm"
container = av.open(os.path.join(full_output_folder, file), mode="w")
if cls.hidden.prompt is not None:
container.metadata["prompt"] = json.dumps(cls.hidden.prompt)
if cls.hidden.extra_pnginfo is not None:
for x in cls.hidden.extra_pnginfo:
container.metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
codec_map = {"vp9": "libvpx-vp9", "av1": "libsvtav1"}
stream = container.add_stream(codec_map[codec], rate=Fraction(round(fps * 1000), 1000))
stream.width = images.shape[-2]
stream.height = images.shape[-3]
stream.pix_fmt = "yuv420p10le" if codec == "av1" else "yuv420p"
stream.bit_rate = 0
stream.options = {'crf': str(crf)}
if codec == "av1":
stream.options["preset"] = "6"
for frame in images:
frame = av.VideoFrame.from_ndarray(torch.clamp(frame[..., :3] * 255, min=0, max=255).to(device=torch.device("cpu"), dtype=torch.uint8).numpy(), format="rgb24")
for packet in stream.encode(frame):
container.mux(packet)
container.mux(stream.encode())
container.close()
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
NODES_LIST = [CreateVideo, GetVideoComponents, LoadVideo, SaveVideo, SaveWEBM]
NODES_LIST: list[type[io.ComfyNode]] = [
CreateVideo,
GetVideoComponents,
LoadVideo,
SaveVideo,
SaveWEBM,
]

View File

@ -11,40 +11,6 @@ import nodes
from comfy_api.latest import io
class ConditioningSetAreaPercentageVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ConditioningSetAreaPercentageVideo_V3",
category="conditioning",
inputs=[
io.Conditioning.Input("conditioning"),
io.Float.Input("width", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("height", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("temporal", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("x", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("y", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("z", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.01),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, conditioning, width, height, temporal, x, y, z, strength):
c = node_helpers.conditioning_set_values(
conditioning,
{
"area": ("percentage", temporal, height, width, z, y, x),
"strength": strength,
"set_area_to_bounds": False
,}
)
return io.NodeOutput(c)
class ImageOnlyCheckpointLoader(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -75,37 +41,6 @@ class ImageOnlyCheckpointLoader(io.ComfyNode):
return io.NodeOutput(out[0], out[3], out[2])
class ImageOnlyCheckpointSave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageOnlyCheckpointSave_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model"),
io.ClipVision.Input("clip_vision"),
io.Vae.Input("vae"),
io.String.Input("filename_prefix", default="checkpoints/ComfyUI"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, model, clip_vision, vae, filename_prefix):
output_dir = folder_paths.get_output_directory()
comfy_extras.nodes_model_merging.save_checkpoint(
model,
clip_vision=clip_vision,
vae=vae,
filename_prefix=filename_prefix,
output_dir=output_dir,
prompt=cls.hidden.prompt,
extra_pnginfo=cls.hidden.extra_pnginfo,
)
return io.NodeOutput()
class SVD_img2vid_Conditioning(io.ComfyNode):
@classmethod
def define_schema(cls):
@ -222,7 +157,72 @@ class VideoTriangleCFGGuidance(io.ComfyNode):
return io.NodeOutput(m)
NODES_LIST = [
class ImageOnlyCheckpointSave(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageOnlyCheckpointSave_V3",
category="advanced/model_merging",
inputs=[
io.Model.Input("model"),
io.ClipVision.Input("clip_vision"),
io.Vae.Input("vae"),
io.String.Input("filename_prefix", default="checkpoints/ComfyUI"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, model, clip_vision, vae, filename_prefix):
output_dir = folder_paths.get_output_directory()
comfy_extras.nodes_model_merging.save_checkpoint(
model,
clip_vision=clip_vision,
vae=vae,
filename_prefix=filename_prefix,
output_dir=output_dir,
prompt=cls.hidden.prompt,
extra_pnginfo=cls.hidden.extra_pnginfo,
)
return io.NodeOutput()
class ConditioningSetAreaPercentageVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ConditioningSetAreaPercentageVideo_V3",
category="conditioning",
inputs=[
io.Conditioning.Input("conditioning"),
io.Float.Input("width", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("height", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("temporal", default=1.0, min=0, max=1.0, step=0.01),
io.Float.Input("x", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("y", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("z", default=0, min=0, max=1.0, step=0.01),
io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.01),
],
outputs=[
io.Conditioning.Output(),
],
)
@classmethod
def execute(cls, conditioning, width, height, temporal, x, y, z, strength):
c = node_helpers.conditioning_set_values(
conditioning,
{
"area": ("percentage", temporal, height, width, z, y, x),
"strength": strength,
"set_area_to_bounds": False
,}
)
return io.NodeOutput(c)
NODES_LIST: list[type[io.ComfyNode]] = [
ConditioningSetAreaPercentageVideo,
ImageOnlyCheckpointLoader,
ImageOnlyCheckpointSave,

View File

@ -425,7 +425,7 @@ class WanVaceToVideo(io.ComfyNode):
return io.NodeOutput(positive, negative, out_latent, trim_latent)
NODES_LIST = [
NODES_LIST: list[type[io.ComfyNode]] = [
TrimVideoLatent,
WanCameraImageToVideo,
WanFirstLastFrameToVideo,

View File

@ -2316,6 +2316,7 @@ async def init_builtin_extra_nodes():
"v3/nodes_cond.py",
"v3/nodes_controlnet.py",
"v3/nodes_cosmos.py",
"v3/nodes_custom_sampler.py",
"v3/nodes_differential_diffusion.py",
"v3/nodes_edit_model.py",
"v3/nodes_flux.py",
@ -2323,7 +2324,9 @@ async def init_builtin_extra_nodes():
"v3/nodes_fresca.py",
"v3/nodes_gits.py",
"v3/nodes_hidream.py",
# "v3/nodes_hooks.py",
"v3/nodes_hunyuan.py",
"v3/nodes_hunyuan3d.py",
"v3/nodes_hypernetwork.py",
"v3/nodes_hypertile.py",
"v3/nodes_images.py",
@ -2334,10 +2337,13 @@ async def init_builtin_extra_nodes():
"v3/nodes_lotus.py",
"v3/nodes_lt.py",
"v3/nodes_lumina2.py",
"v3/nodes_mahiro.py",
"v3/nodes_mask.py",
"v3/nodes_mochi.py",
"v3/nodes_model_advanced.py",
"v3/nodes_model_downscale.py",
"v3/nodes_model_merging.py",
"v3/nodes_model_merging_model_specific.py",
"v3/nodes_morphology.py",
"v3/nodes_optimalsteps.py",
"v3/nodes_pag.py",
@ -2352,7 +2358,9 @@ async def init_builtin_extra_nodes():
"v3/nodes_sd3.py",
"v3/nodes_sdupscale.py",
"v3/nodes_slg.py",
"v3/nodes_stable3d.py",
"v3/nodes_stable_cascade.py",
"v3/nodes_string.py",
"v3/nodes_tcfg.py",
"v3/nodes_tomesd.py",
"v3/nodes_torch_compile.py",