From 4927403a8754e34b269a0aedc5216a47bc43f613 Mon Sep 17 00:00:00 2001 From: TenserTensor Date: Wed, 25 Mar 2026 00:10:10 +0300 Subject: [PATCH] Added some LTXV nodes, bugfixes, updated workflow & context nodes --- comfy_extension.py | 12 +- gguf/utils.py | 2 +- nodes_context.py | 87 ++++--- nodes_controlnet.py | 12 +- nodes_debug.py | 4 - nodes_image.py | 16 +- nodes_latent.py | 8 +- nodes_latent_d.py | 2 +- nodes_sampling.py | 17 +- nodes_vae.py | 156 +----------- nodes_vae_d.py | 164 ++++++++++++ nodes_workflow.py | 601 +++++++------------------------------------- nodes_workflow_d.py | 575 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 14 files changed, 919 insertions(+), 739 deletions(-) create mode 100644 nodes_vae_d.py create mode 100644 nodes_workflow_d.py diff --git a/comfy_extension.py b/comfy_extension.py index fb767fb..2935095 100644 --- a/comfy_extension.py +++ b/comfy_extension.py @@ -2,7 +2,7 @@ from typing import override -from comfy_api.latest import ComfyExtension, io +from comfy_api.latest import ComfyAPI, ComfyExtension, io from .nodes_context import NODES as NODES_CONTEXT from .nodes_controlnet import NODES as NODES_CONTROLNET from .nodes_debug import NODES as NODES_DEBUG @@ -15,8 +15,12 @@ from .nodes_loaders_d import NODES as NODES_LOADERS_D from .nodes_postproduction import NODES as NODES_POSTPRODUCTION from .nodes_sampling import NODES as NODES_SAMPLING from .nodes_text_encoder import NODES as NODES_TEXT_ENCODER -from .nodes_vae import NODES as NODES_VAE +from .nodes_text_encoder_d import NODES as NODES_TEXT_ENCODER_D +from .nodes_vae_d import NODES as NODES_VAE_D from .nodes_workflow import NODES as NODES_WORKFLOW +from .nodes_workflow_d import NODES as NODES_WORKFLOW_D + +api = ComfyAPI() class TenserTensorExtension(ComfyExtension): @@ -35,6 +39,8 @@ class TenserTensorExtension(ComfyExtension): *NODES_POSTPRODUCTION, *NODES_SAMPLING, *NODES_TEXT_ENCODER, - *NODES_VAE, + *NODES_TEXT_ENCODER_D, + *NODES_VAE_D, *NODES_WORKFLOW, + *NODES_WORKFLOW_D, ] diff --git a/gguf/utils.py b/gguf/utils.py index 9a80653..7f45b7d 100644 --- a/gguf/utils.py +++ b/gguf/utils.py @@ -10,7 +10,7 @@ from comfy import ldm, sd, utils, model_patcher, model_management, lora, float from .dequant import is_quantized, is_torch_compatible from .loader import gguf_clip_loader, gguf_sd_loader from .ops import GGMLOps, move_patch_to_device -from ..utils import raise_unless, get_embedding_directory, apply_lora_stack +from ..utils import raise_unless, get_embedding_directory def attention_override_pytorch(func, *args, **kwargs): diff --git a/nodes_context.py b/nodes_context.py index 0b55e5c..d3e069d 100644 --- a/nodes_context.py +++ b/nodes_context.py @@ -4,9 +4,7 @@ import dataclasses import re from dataclasses import dataclass -from comfy.clip_vision import Output -from comfy_api.latest import IO, ComfyExtension, Input - +from comfy_api.latest import io, Input from .nodes_workflow import TTWorkflowSettings, WorkflowSettings CATEGORY = "TenserTensor/Context" @@ -23,29 +21,46 @@ class TTContext(): Use create() to instantiate from kwargs, and set_attr/get_attr for safe field access. Workflow config fields are accessible via set_workflow_config_attr/get_workflow_config_attr. """ - cfg: IO.Float | None = None - clip: IO.Clip | None = None - control_net: IO.ControlNet | None = None - conditioning: IO.Conditioning | None = None - guidance: IO.Float | None = None - guider: IO.Guider | None = None - image: IO.Image | None = None - latent: IO.Latent | None = None - lora_triggers: IO.String | None = None - mask: IO.Mask | None = None - model: IO.Model | None = None - negative: IO.Conditioning | None = None - positive: IO.Conditioning | None = None - prompt: IO.String | None = None - rnd_noise: IO.Noise | None = None - sampler: IO.Sampler | None = None - seed: IO.Int | None = None - sigmas: IO.Sigmas | None = None - steps: IO.Int | None = None - vae: IO.Vae | None = None + ascore_negative: io.Float | None = None + ascore_positive: io.Float | None = None + cfg: io.Float | None = None + clip: io.Clip | None = None + clip_g_negative: io.String | None = None + clip_g_positive: io.String | None = None + clip_l_negative: io.String | None = None + clip_l_positive: io.String | None = None + conditioning: io.Conditioning | None = None + control_net: io.ControlNet | None = None + frame_rate: io.String | None = None + guidance: io.Float | None = None + guider: io.Guider | None = None + height: io.Int | None = None + image: io.Image | None = None + latent: io.Latent | None = None + length_sec: io.Float | None = None + lora_triggers: io.String | None = None + mask: io.Mask | None = None + model: io.Model | None = None + negative: io.Conditioning | None = None + negative_prompt: io.String | None = None + noise_seed: io.Int | None = None + positive: io.Conditioning | None = None + positive_prompt: io.String | None = None + prompt: io.String | None = None + rnd_noise: io.Noise | None = None + sampler: io.Sampler | None = None + sampler_name: io.String | None = None + scheduler: io.String | None = None + seed: io.Int | None = None + sigmas: io.Sigmas | None = None + steps: io.Int | None = None + t5xxl_negative: io.String | None = None + t5xxl_positive: io.String | None = None + target_height: io.Int | None = None + target_width: io.Int | None = None + vae: io.Vae | None = None + width: io.Int | None = None workflow_config: WorkflowSettings | None = None - sampler_name: IO.String | None = None - scheduler: IO.String | None = None @classmethod def create(cls, **kwargs): @@ -101,20 +116,20 @@ class TTContext(): raise AttributeError(f"ERROR: Workflow Config is not set in context") -@IO.comfytype(io_type="TT_CONTEXT") +@io.comfytype(io_type="TT_CONTEXT") class Context: Type = TTContext - class Input(IO.Input): + class Input(io.Input): def __init__(self, id: str, **kwargs): super().__init__(id, **kwargs) - class Output(IO.Output): + class Output(io.Output): def __init__(self, id: str, **kwargs): super().__init__(id, **kwargs) -class ContextNode(IO.ComfyNode): +class ContextNode(io.ComfyNode): """ Abstract base class for all context nodes. Handles context creation, delegation to handle(), and building return tuple. @@ -139,7 +154,7 @@ class ContextNode(IO.ComfyNode): return Context.Input(input_name, optional=optional) case _: f_type = cls.F_TYPES[input_name] - if f_type in (IO.Boolean, IO.Int, IO.Float, IO.String,): + if f_type in (io.Boolean, io.Int, io.Float, io.String,): return f_type.Input(input_name, optional=optional, force_input=True) else: return f_type.Input(input_name, optional=optional) @@ -155,7 +170,7 @@ class ContextNode(IO.ComfyNode): return input_schema @classmethod - def outputs(cls) -> list[Output]: + def outputs(cls) -> list[io.Output]: output_schema = [] for f_name in cls.RETURNS if len(cls.RETURNS) > 0 else cls.REQUIRED + cls.OPTIONAL: @@ -168,24 +183,24 @@ class ContextNode(IO.ComfyNode): return output_schema @classmethod - def node_output(cls, context) -> IO.NodeOutput: + def node_output(cls, context) -> io.NodeOutput: args = [context] for f_name in cls.RETURNS if len(cls.RETURNS) > 0 else cls.REQUIRED + cls.OPTIONAL: if f_name != "context": args.append(context.get_attr(f_name)) - return IO.NodeOutput(*args) + return io.NodeOutput(*args) @classmethod def display_name(cls): return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', cls.__name__.replace("TT_", "TT ").replace("Node", "")) @classmethod - def define_schema(cls) -> IO.Schema: + def define_schema(cls) -> io.Schema: if len(cls.F_TYPES) == 0: cls.fill_field_types() - return IO.Schema( + return io.Schema( node_id=cls.__name__, display_name=cls.display_name(), category=CATEGORY, @@ -206,7 +221,7 @@ class ContextNode(IO.ComfyNode): return (context, kwargs,) @classmethod - def execute(cls, context=None, **kwargs) -> IO.NodeOutput: + def execute(cls, context=None, **kwargs) -> io.NodeOutput: if not context: context = TTContext.create(**kwargs) cls.handle(context, **kwargs) diff --git a/nodes_controlnet.py b/nodes_controlnet.py index 0835cfc..23a0ebd 100644 --- a/nodes_controlnet.py +++ b/nodes_controlnet.py @@ -2,9 +2,9 @@ import folder_paths from comfy import controlnet -from comfy_api.latest import io, ui, ComfyExtension +from comfy_api.latest import io, ui -from .nodes_image import SingleCondCFGGuider, get_image_files, load_image +from .nodes_image import AdaptiveCFGGuider, get_image_files, load_image CATEGORY = "TenserTensor/ControlNet" @@ -92,8 +92,8 @@ class TT_Flux2ApplyControlNetNode(io.ComfyNode): kwargs.get("strength"), kwargs.get("control_net")) - if not isinstance(guider, SingleCondCFGGuider): - guider = SingleCondCFGGuider.from_cfg_guider(guider) + if not isinstance(guider, AdaptiveCFGGuider): + guider = AdaptiveCFGGuider.from_cfg_guider(guider) if strength == 0.0: return io.NodeOutput(guider) @@ -141,8 +141,8 @@ class TT_Flux2ApplyControlNetAdvancedNode(io.ComfyNode): kwargs.get("strength"), kwargs.get("control_net")) - if not isinstance(guider, SingleCondCFGGuider): - guider = SingleCondCFGGuider.from_cfg_guider(guider) + if not isinstance(guider, AdaptiveCFGGuider): + guider = AdaptiveCFGGuider.from_cfg_guider(guider) if strength == 0.0: return io.NodeOutput(guider) diff --git a/nodes_debug.py b/nodes_debug.py index 7f8c963..e9a13d7 100644 --- a/nodes_debug.py +++ b/nodes_debug.py @@ -193,10 +193,6 @@ class TT_JsonPreviewDebugNode(io.ComfyNode): return io.NodeOutput(markdown, ui=ui.PreviewText(markdown)) - - - - NODES = [ # TT_StableAudioOpenModelsLoaderNode, # TT_AudioLatentFactoryNode, diff --git a/nodes_image.py b/nodes_image.py index 06de666..832ba72 100644 --- a/nodes_image.py +++ b/nodes_image.py @@ -7,17 +7,17 @@ from datetime import datetime from pathlib import Path from typing import Any -import folder_paths import numpy import torch from PIL import Image, ImageOps, ImageSequence -from comfy import model_management, utils -from comfy_api.latest import ComfyExtension, io, ui -from node_helpers import conditioning_set_values, pillow from spandrel import ModelLoader, ImageModelDescriptor -from .nodes_text_encoder import SingleCondCFGGuider -from .nodes_vae import vae_encode +import folder_paths +from comfy import model_management, utils +from comfy_api.latest import io, ui +from node_helpers import conditioning_set_values, pillow +from .nodes_text_encoder import AdaptiveCFGGuider +from .nodes_vae_d import vae_encode CATEGORY = "TenserTensor/Image" RESIZE_METHODS = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"] @@ -457,8 +457,8 @@ class TT_GuiderImageReferenceNode(io.ComfyNode): kwargs.get("dimension_step") ) - if not isinstance(guider, SingleCondCFGGuider): - guider = SingleCondCFGGuider.from_cfg_guider(guider) + if not isinstance(guider, AdaptiveCFGGuider): + guider = AdaptiveCFGGuider.from_cfg_guider(guider) resized = resize_image_to_megapixels(timage, resize_method, megapixels, dimension_step) latent = vae_encode(resized, vae) diff --git a/nodes_latent.py b/nodes_latent.py index cc00451..fe48e8d 100644 --- a/nodes_latent.py +++ b/nodes_latent.py @@ -176,7 +176,7 @@ def prepare_multimodal_latents(**kwargs): kwargs.get("use_ref_frame"), kwargs.get("seed"), kwargs.get("noise_seed"), - kwargs.get("megapixels") + kwargs.get("megapixels"), ) video_latent, width, height, total_length, fps = create_empty_video_latent(**kwargs) @@ -196,6 +196,7 @@ def prepare_multimodal_latents(**kwargs): seed, noise_seed, megapixels, + kwargs.get("length_sec"), total_length, fps, width, @@ -232,7 +233,7 @@ class TT_LtxvLatentFactoryNode(io.ComfyNode): io.Combo.Input("aspect_ratio", options=CommonTypes.ASPECT_RATIOS), io.Combo.Input("megapixels", options=CommonTypes.MEGAPIXELS), io.Combo.Input("orientation", options=CommonTypes.ORIENTATIONS), - io.Int.Input("length_sec", default=5, min=1, max=20), + io.Float.Input("length_sec", default=5.0, min=0.1, max=120.0, step=0.1), io.Combo.Input("frame_rate", options=CommonTypes.FRAME_RATES, default="24fps"), io.Int.Input("batch_size", default=1, min=1, max=64), ], @@ -244,8 +245,9 @@ class TT_LtxvLatentFactoryNode(io.ComfyNode): io.Int.Output(display_name="SEED"), io.Int.Output(display_name="NOISE_SEED"), io.String.Output(display_name="MEGAPIXELS"), + io.String.Output(display_name="LENGTH_SEC"), io.Int.Output(display_name="TOTAL_LENGTH"), - io.Int.Output(display_name="FPS"), + io.Int.Output(display_name="FRAME_RATE"), io.Int.Output(display_name="WIDTH"), io.Int.Output(display_name="HEIGHT"), ] diff --git a/nodes_latent_d.py b/nodes_latent_d.py index 180116a..a4b44b1 100644 --- a/nodes_latent_d.py +++ b/nodes_latent_d.py @@ -9,7 +9,7 @@ from comfy.latent_formats import SDXL, Flux, Flux2, SD3 from comfy_api.latest import io, ComfyExtension from .nodes_image import MEGAPIXELS, resize_image, rotate_image, flip_image -from .nodes_vae import vae_decode, vae_encode +from .nodes_vae_d import vae_decode, vae_encode CATEGORY = "TenserTensor/Latent" diff --git a/nodes_sampling.py b/nodes_sampling.py index edfe64f..3523ec5 100644 --- a/nodes_sampling.py +++ b/nodes_sampling.py @@ -2,12 +2,13 @@ import latent_preview import torch -from comfy import sample, model_management, samplers -from comfy_api.latest import ComfyExtension, io +from comfy import sample, model_management, samplers +from comfy_api.latest import io from .nodes_context import Context -from .nodes_latent import SCALE_FACTORS, SCALE_METHODS, scale_latent -from .nodes_text_encoder import SingleCondCFGGuider +from .nodes_latent_d import scale_latent +from .nodes_text_encoder import AdaptiveCFGGuider +from .utils import CommonTypes CATEGORY = "TenserTensor/Sampling" @@ -262,8 +263,8 @@ def execute_guided_sampling(**kwargs): kwargs.get("preview_latent"), ) - if not isinstance(guider, SingleCondCFGGuider): - guider = SingleCondCFGGuider.from_cfg_guider(guider) + if not isinstance(guider, AdaptiveCFGGuider): + guider = AdaptiveCFGGuider.from_cfg_guider(guider) model_patcher = guider.model_patcher latent_dict = latent.copy() @@ -343,8 +344,8 @@ class TT_GuidedUpscaleKSamplerNode(io.ComfyNode): io.Sampler.Input("sampler"), io.Noise.Input("random_noise"), io.Boolean.Input("scale_latent", default=False, label_on="Scale", label_off="Skip"), - io.Combo.Input("scale_factor", options=SCALE_FACTORS, default="1x", advanced=True), - io.Combo.Input("scale_method", options=SCALE_METHODS, default="bicubic", advanced=True), + io.Combo.Input("scale_factor", options=CommonTypes.SCALE_FACTORS, default="1x", advanced=True), + io.Combo.Input("scale_method", options=CommonTypes.SCALE_METHODS, default="bicubic", advanced=True), io.Boolean.Input("preview_latent", default=False, label_on="Show Preview", label_off="Only Sample"), ], outputs=[ diff --git a/nodes_vae.py b/nodes_vae.py index 9905e2a..a9c5dd4 100644 --- a/nodes_vae.py +++ b/nodes_vae.py @@ -1,155 +1,10 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -from comfy_api.latest import io, ComfyExtension - -from .nodes_context import Context - -CATEGORY = "TenserTensor/VAE" - -TILE_SIZE, OVERLAP = 512, 64 +from comfy_api.latest import io -def vae_decode(latent, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP): - samples = latent["samples"] - - compression = vae.spacial_compression_decode() - image = vae.decode_tiled( - samples, - tile_x=tile_width // compression, - tile_y=tile_height // compression, - overlap=overlap // compression - ) - - return image - - -def vae_encode(image, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP): - samples = vae.encode_tiled( - image, - tile_x=tile_width, - tile_y=tile_height, - overlap=overlap - ) - - return {"samples": samples} - - -class TT_VaeDecodeTiledNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_VaeDecodeTiledNode", - display_name="TT Vae Decode (Tiled)", - category=CATEGORY, - description="", - inputs=[ - io.Vae.Input("vae"), - io.Latent.Input("latent"), - io.Int.Input("tile_width", default=512, min=64, max=4096, step=64), - io.Int.Input("tile_height", default=512, min=64, max=4096, step=64), - io.Int.Input("overlap", default=64, min=8, max=256, step=8), - ], - outputs=[ - io.Image.Output("IMAGE"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - image = vae_decode(**kwargs) - - return io.NodeOutput(image) - - -class TT_VaeDecodeContextNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_VaeDecodeContextNode", - display_name="TT Vae Decode (Context)", - category=CATEGORY, - description="", - inputs=[ - Context.Input("context"), - ], - outputs=[ - Context.Output("CONTEXT"), - io.Image.Output("IMAGE"), - ] - ) - - @classmethod - def execute(cls, context) -> io.NodeOutput: - vae = context.get("vae") - if vae is None: - raise ValueError("ERROR: VAE is required for decode") - - latent = context.get("latent") - if latent is None: - raise ValueError("ERROR: Latent image is required for decode") - - image = vae_decode(latent, vae) - - return io.NodeOutput(context, image) - - -class TT_VaeEncodeTiledNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_VaeEncodeTiledNode", - display_name="TT Vae Encode (Tiled)", - category=CATEGORY, - description="", - inputs=[ - io.Image.Input("image"), - io.Vae.Input("vae"), - io.Int.Input("tile_width", default=512, min=64, max=4096, step=64), - io.Int.Input("tile_height", default=512, min=64, max=4096, step=64), - io.Int.Input("overlap", default=64, min=8, max=256, step=8), - ], - outputs=[ - io.Latent.Output("LATENT"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - latent = vae_encode(**kwargs) - - return io.NodeOutput(latent) - - -class TT_VaeEncodeContextNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_VaeEncodeContextNode", - display_name="TT Vae Encode (Context)", - category=CATEGORY, - description="", - inputs=[ - Context.Input("context"), - ], - outputs=[ - Context.Output("CONTEXT"), - io.Latent.Output("LATENT"), - ] - ) - - @classmethod - def execute(cls, context) -> io.NodeOutput: - vae = context.get("vae") - if vae is None: - raise ValueError("ERROR: VAE is required for encode") - - image = context.get("image") - if image is None: - raise ValueError("ERROR: Pixel image is required for encode") - - latent = vae_encode(image, vae) - - return io.NodeOutput(context, latent) +class TT_GuidedUpscaleKSamplerNode(io.ComfyNode): + pass # ============================================================================== @@ -157,8 +12,5 @@ class TT_VaeEncodeContextNode(io.ComfyNode): # ============================================================================== NODES = [ - TT_VaeDecodeTiledNode, - TT_VaeDecodeContextNode, - TT_VaeEncodeTiledNode, - TT_VaeEncodeContextNode, + ] diff --git a/nodes_vae_d.py b/nodes_vae_d.py new file mode 100644 index 0000000..9905e2a --- /dev/null +++ b/nodes_vae_d.py @@ -0,0 +1,164 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +from comfy_api.latest import io, ComfyExtension + +from .nodes_context import Context + +CATEGORY = "TenserTensor/VAE" + +TILE_SIZE, OVERLAP = 512, 64 + + +def vae_decode(latent, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP): + samples = latent["samples"] + + compression = vae.spacial_compression_decode() + image = vae.decode_tiled( + samples, + tile_x=tile_width // compression, + tile_y=tile_height // compression, + overlap=overlap // compression + ) + + return image + + +def vae_encode(image, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP): + samples = vae.encode_tiled( + image, + tile_x=tile_width, + tile_y=tile_height, + overlap=overlap + ) + + return {"samples": samples} + + +class TT_VaeDecodeTiledNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_VaeDecodeTiledNode", + display_name="TT Vae Decode (Tiled)", + category=CATEGORY, + description="", + inputs=[ + io.Vae.Input("vae"), + io.Latent.Input("latent"), + io.Int.Input("tile_width", default=512, min=64, max=4096, step=64), + io.Int.Input("tile_height", default=512, min=64, max=4096, step=64), + io.Int.Input("overlap", default=64, min=8, max=256, step=8), + ], + outputs=[ + io.Image.Output("IMAGE"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + image = vae_decode(**kwargs) + + return io.NodeOutput(image) + + +class TT_VaeDecodeContextNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_VaeDecodeContextNode", + display_name="TT Vae Decode (Context)", + category=CATEGORY, + description="", + inputs=[ + Context.Input("context"), + ], + outputs=[ + Context.Output("CONTEXT"), + io.Image.Output("IMAGE"), + ] + ) + + @classmethod + def execute(cls, context) -> io.NodeOutput: + vae = context.get("vae") + if vae is None: + raise ValueError("ERROR: VAE is required for decode") + + latent = context.get("latent") + if latent is None: + raise ValueError("ERROR: Latent image is required for decode") + + image = vae_decode(latent, vae) + + return io.NodeOutput(context, image) + + +class TT_VaeEncodeTiledNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_VaeEncodeTiledNode", + display_name="TT Vae Encode (Tiled)", + category=CATEGORY, + description="", + inputs=[ + io.Image.Input("image"), + io.Vae.Input("vae"), + io.Int.Input("tile_width", default=512, min=64, max=4096, step=64), + io.Int.Input("tile_height", default=512, min=64, max=4096, step=64), + io.Int.Input("overlap", default=64, min=8, max=256, step=8), + ], + outputs=[ + io.Latent.Output("LATENT"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + latent = vae_encode(**kwargs) + + return io.NodeOutput(latent) + + +class TT_VaeEncodeContextNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_VaeEncodeContextNode", + display_name="TT Vae Encode (Context)", + category=CATEGORY, + description="", + inputs=[ + Context.Input("context"), + ], + outputs=[ + Context.Output("CONTEXT"), + io.Latent.Output("LATENT"), + ] + ) + + @classmethod + def execute(cls, context) -> io.NodeOutput: + vae = context.get("vae") + if vae is None: + raise ValueError("ERROR: VAE is required for encode") + + image = context.get("image") + if image is None: + raise ValueError("ERROR: Pixel image is required for encode") + + latent = vae_encode(image, vae) + + return io.NodeOutput(context, latent) + + +# ============================================================================== +# V3 entrypoint — registers context nodes with ComfyUI +# ============================================================================== + +NODES = [ + TT_VaeDecodeTiledNode, + TT_VaeDecodeContextNode, + TT_VaeEncodeTiledNode, + TT_VaeEncodeContextNode, +] diff --git a/nodes_workflow.py b/nodes_workflow.py index f70a288..c5967b7 100644 --- a/nodes_workflow.py +++ b/nodes_workflow.py @@ -1,556 +1,132 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) - -import dataclasses import math -from dataclasses import dataclass import torch -from comfy.samplers import KSampler, sampler_object, Sampler -from comfy_api.latest import io, ComfyExtension -from nodes import MAX_RESOLUTION + +from comfy.samplers import KSampler, sampler_object +from comfy_api.latest import io +from .nodes_workflow_d import WorkflowSettings, TTWorkflowSettings +from .utils import CommonTypes CATEGORY = "TenserTensor/Workflow" -@dataclass -class TTWorkflowSettings(): - ascore_negative: float | None = None - ascore_positive: float | None = None - cfg: int | None = None - clip_g_negative: str | None = None - clip_l_negative: str | None = None - clip_g_positive: str | None = None - clip_l_positive: str | None = None - guidance: float | None = None - height: int | None = None - lora_triggers: str | None = None - prompt: str | None = None - sampler: Sampler | None = None - sampler_name: str | None = None - scheduler: str | None = None - seed: int | None = None - sigmas: torch.Tensor | None = None - steps: int | None = None - target_height: int | None = None - target_width: int | None = None - t5xxl_negative: str | None = None - t5xxl_positive: str | None = None - width: int | None = None +# ============================================================================== +# Helper functions — pipeline utilities and loaders +# ============================================================================== - @classmethod - def create(cls, **kwargs) -> TTWorkflowSettings: - wf = cls() - for field in dataclasses.fields(wf): - wf.set_attr(field.name, kwargs.get(field.name, None)) +def ltxv_sigmas(**kwargs): + latent, scheduler_config = ( + kwargs.get("video_latent_opt"), + kwargs.get("scheduler_config"), + ) - return wf + base_shift, max_shift = 0.95, 2.05 + if scheduler_config.get("scheduler_config") == "Manual": + base_shift = scheduler_config.get("schedule_base_shift", 0.95), + max_shift = scheduler_config.get("schedule_max_shift", 2.05) - def update_attrs(self, **kwargs) -> TTWorkflowSettings: - for key in kwargs.keys(): - if hasattr(self, key): - val = kwargs.get(key) - if val is not None: - self.set_attr(key, val) + token_count = math.prod(latent["samples"].shape[2:]) if latent else 4096 + sigmas = torch.linspace(1.0, 0.0, kwargs.get("steps") + 1) - return self + MIN_STEPS, MAX_STEPS = 1024, 4096 - def set_attr(self, key, attr) -> None: - if hasattr(self, key): - setattr(self, key, attr) + slope = (max_shift - base_shift) / (MAX_STEPS - MIN_STEPS) + intercept = base_shift - slope * MIN_STEPS + sigma_shift = (token_count) * slope + intercept + sigma = 1.0 + timesteps = math.exp(sigma_shift) / (math.exp(sigma_shift) + (1 / sigmas - 1) ** sigma) + sigmas = torch.where(sigmas != 0, timesteps, 0) - def get_attr(self, key, default=None) -> None: - if hasattr(self, key): - val = getattr(self, key) - return val if val is not None else default + stretch_sigmas = kwargs.get("stretch_sigmas") + if stretch_sigmas.get("stretch_sigmas") == "In Range": + non_zero_mask = sigmas != 0 + non_zero_sigmas = sigmas[non_zero_mask] + one_minus_z = 1.0 - non_zero_sigmas + scale_factor = one_minus_z[-1] / (1.0 - stretch_sigmas.get("sigmas_terminal", 0.1)) + stretched = 1.0 - (one_minus_z / scale_factor) + sigmas[non_zero_mask] = stretched + + return sigmas -@io.comfytype(io_type="TT_WORKFLOW_CONFIG") -class WorkflowSettings: - Type = TTWorkflowSettings - - class Input(io.Input): - def __init__(self, id: str, **kwargs): - super().__init__(id, **kwargs) - - class Output(io.Output): - def __init__(self, id: str, **kwargs): - super().__init__(id, **kwargs) - - -class TT_SdxlWorkflowSettingsNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_SdxlWorkflowSettingsNode", - display_name="TT SDXL Workflow Settings", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), - io.Float.Input("ascore_positive", default=9.0, min=0.0, max=100.0, step=0.1), - io.Float.Input("ascore_negative", default=6.0, min=0.0, max=100.0, step=0.1), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.String.Output("SAMPLER_NAME"), - io.String.Output("SCHEDULER"), - io.Float.Output("ASCORE_POSITIVE"), - io.Float.Output("ASCORE_NEGATIVE"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), - io.Int.Output("TARGET_WIDTH"), - io.Int.Output("TARGET_HEIGHT"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - workflow_config = TTWorkflowSettings.create(**kwargs) - args = {"workflow_config": workflow_config, **kwargs} - - return io.NodeOutput(*args.values()) - - -class TT_SdxlWorkflowSettingsAdvancedNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_SdxlWorkflowSettingsAdvancedNode", - display_name="TT SDXL Workflow Settings (Advanced)", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), - io.Float.Input("ascore_positive", default=9.0, min=0.0, max=100.0, step=0.1), - io.Float.Input("ascore_negative", default=6.0, min=0.0, max=100.0, step=0.1), - io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), - io.String.Input("clip_g_positive", multiline=True, dynamic_prompts=True), - io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), - io.String.Input("clip_g_negative", multiline=True, dynamic_prompts=True), - io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.String.Output("SAMPLER_NAME"), - io.String.Output("SCHEDULER"), - io.Float.Output("ASCORE_POSITIVE"), - io.Float.Output("ASCORE_NEGATIVE"), - io.String.Output("CLIP_L_POSITIVE"), - io.String.Output("CLIP_G_POSITIVE"), - io.String.Output("CLIP_L_NEGATIVE"), - io.String.Output("CLIP_G_NEGATIVE"), - io.String.Output("LORA_TRIGGERS"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), - io.Int.Output("TARGET_WIDTH"), - io.Int.Output("TARGET_HEIGHT"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - workflow_config = TTWorkflowSettings.create(**kwargs) - args = {"workflow_config": workflow_config, **kwargs} - - return io.NodeOutput(*args.values()) - - -class TT_FluxWorkflowSettingsNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_FluxWorkflowSettingsNode", - display_name="TT FLUX Workflow Settings", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), - io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.String.Output("SAMPLER_NAME"), - io.String.Output("SCHEDULER"), - io.Float.Output("GUIDANCE"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - workflow_config = TTWorkflowSettings.create(**kwargs) - args = {"workflow_config": workflow_config, **kwargs} - - return io.NodeOutput(*args.values()) - - -class TT_FluxWorkflowSettingsAdvancedNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_FluxWorkflowSettingsAdvancedNode", - display_name="TT FLUX Workflow Settings (Advanced)", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), - io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), - io.String.Input("t5xxl_positive", multiline=True, dynamic_prompts=True), - io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), - io.String.Input("t5xxl_negative", multiline=True, dynamic_prompts=True), - io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), - io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.String.Output("SAMPLER_NAME"), - io.String.Output("SCHEDULER"), - io.String.Output("CLIP_L_POSITIVE"), - io.String.Output("T5XXL_POSITIVE"), - io.String.Output("CLIP_L_NEGATIVE"), - io.String.Output("T5XXL_NEGATIVE"), - io.String.Output("LORA_TRIGGERS"), - io.Float.Output("GUIDANCE"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - workflow_config = TTWorkflowSettings.create(**kwargs) - args = {"workflow_config": workflow_config, **kwargs} - - return io.NodeOutput(*args.values()) - - -def compute_empirical_mu(image_seq_len, num_steps): - slope_high_res, intercept_high_res = 8.73809524e-05, 1.89833333 - slope_low_res, intercept_low_res = 0.00016927, 0.45666666 - HIGH_RES_THRESHOLD = 4300 - MIN_STEPS, MAX_STEPS = 10, 200 - mu_at_max_steps = slope_low_res * image_seq_len + intercept_low_res - - if image_seq_len > HIGH_RES_THRESHOLD: - return float(mu_at_max_steps) - - mu_at_min_steps = slope_high_res * image_seq_len + intercept_high_res - slope = (mu_at_max_steps - mu_at_min_steps) / (MAX_STEPS - MIN_STEPS) - intercept = mu_at_max_steps - MAX_STEPS * slope - mu = slope * num_steps + intercept - - return float(mu) - - -def generalized_time_snr_shift(t, mu, sigma): - return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) - - -def get_schedule(steps, width, height): - image_seq_len = (width * height / (16 * 16)) - mu = compute_empirical_mu(image_seq_len, steps) - timesteps = torch.linspace(1, 0, steps + 1) - timesteps = generalized_time_snr_shift(timesteps, mu, 1.0) - - return timesteps - - -def build_sampler_sigmas(sampler_name, steps, width, height): - sampler = sampler_object(sampler_name) - sigmas = get_schedule(steps, width, height) +def build_ltxv_sampler_sigmas(**kwargs): + sampler = sampler_object(kwargs.get("sampler_name")) + sigmas = ltxv_sigmas(**kwargs) return sampler, sigmas -class TT_Flux2WorkflowSettingsNode(io.ComfyNode): +# ============================================================================== +# Node classes — ComfyUI node definitions +# ============================================================================== + + +class TT_LtxvWorkflowSettingsNode(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: return io.Schema( - node_id="TT_Flux2WorkflowSettingsNode", - display_name="TT FLUX2 Workflow Settings", + node_id="TT_LtxvWorkflowSettingsNode", + display_name="TT LTXV Workflow Settings", category=CATEGORY, description="", inputs=[ + io.Latent.Input("video_latent_opt", optional=True), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.DynamicCombo.Input("stretch_sigmas", options=[ + io.DynamicCombo.Option("In Range", [ + io.Float.Input("sigmas_terminal", default=0.1, min=0.0, max=0.99, step=0.01), + ]), + io.DynamicCombo.Option("Bypass", []), + ]), + io.DynamicCombo.Input("scheduler_config", options=[ + io.DynamicCombo.Option("Defaults", []), + io.DynamicCombo.Option("Manual", [ + io.Float.Input("schedule_base_shift", default=0.95, min=0.0, max=10.0, step=0.01), + io.Float.Input("schedule_max_shift", default=2.05, min=0.0, max=10.0, step=0.01), + ]), + ]), io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True), io.Int.Input("steps", default=30, min=1, max=10_000), io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1), - io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS) + io.Float.Input("length_sec", default=5.0, min=0.1, max=120.0, step=0.1), + io.Combo.Input("frame_rate", options=CommonTypes.FRAME_RATES, default="24fps"), ], outputs=[ WorkflowSettings.Output("WORKFLOW_CONFIG"), io.Sampler.Output("SAMPLER"), io.Sigmas.Output("SIGMAS"), io.Int.Output("SEED"), + io.Int.Output("NOISE_SEED"), io.Int.Output("STEPS"), io.Float.Output("CFG"), - io.Float.Output("GUIDANCE"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), + io.String.Output("LENGTH_SEC"), + io.String.Output("FRAME_RATE"), ] ) @classmethod def execute(cls, **kwargs) -> io.NodeOutput: - kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas( - kwargs.get("sampler_name"), + workflow_config = TTWorkflowSettings.create(**kwargs) + + sampler, sigmas = build_ltxv_sampler_sigmas(**kwargs) + workflow_config.set_attr("sampler", sampler) + workflow_config.set_attr("sigmas", sigmas) + + args = ( + workflow_config, + sampler, + sigmas, + kwargs.get("seed"), + kwargs.get("noise_seed"), kwargs.get("steps"), - kwargs.get("width"), - kwargs.get("height"), + kwargs.get("cfg"), + kwargs.get("length_sec"), + kwargs.get("frame_rate"), ) - workflow_config = TTWorkflowSettings.create(**kwargs) - args = { - "workflow_config": workflow_config, - "sampler": kwargs["sampler"], - "sigmas": kwargs["sigmas"], - "seed": kwargs.get("seed"), - "steps": kwargs.get("steps"), - "cfg": kwargs.get("cfg"), - "guidance": kwargs.get("guidance"), - "width": kwargs.get("width"), - "height": kwargs.get("height"), - } - - return io.NodeOutput(*args.values()) - - -class TT_Flux2WorkflowSettingsAdvancedNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_Flux2WorkflowSettingsAdvancedNode", - display_name="TT FLUX2 Workflow Settings (Advanced)", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.String.Input("prompt", multiline=True, dynamic_prompts=True), - io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True), - io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Sampler.Output("SAMPLER"), - io.Sigmas.Output("SIGMAS"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), - io.String.Output("PROMPT"), - io.String.Output("LORA_TRIGGERS"), - io.Float.Output("GUIDANCE"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas( - kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("width"), kwargs.get("height"), - ) - - workflow_config = TTWorkflowSettings.create(**kwargs) - args = { - "workflow_config": workflow_config, - "sampler": kwargs["sampler"], - "sigmas": kwargs["sigmas"], - "seed": kwargs.get("seed"), - "steps": kwargs.get("steps"), - "cfg": kwargs.get("cfg"), - "width": kwargs.get("width"), - "height": kwargs.get("height"), - "prompt": kwargs.get("prompt"), - "lora_triggers": kwargs.get("lora_triggers"), - "guidance": kwargs.get("guidance"), - } - - return io.NodeOutput(*args.values()) - - -def sd3_shift_sigma(sigma, schedule_shift): - return schedule_shift * sigma / (1 + (schedule_shift - 1) * sigma) - - -def build_sd3_sampler_sigmas(sampler_name, steps, schedule_shift=3.0): - sampler = sampler_object(sampler_name) - timesteps = torch.linspace(1, 0, steps + 1) - sigmas = sd3_shift_sigma(timesteps, schedule_shift) - - return sampler, sigmas - - -class TT_Sd35GgufWorkflowSettingsNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_Sd35GgufWorkflowSettingsNode", - display_name="TT SD3.5 GGUF Workflow Settings", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Sampler.Output("SAMPLER"), - io.Sigmas.Output("SIGMAS"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), - io.Int.Output("TARGET_WIDTH"), - io.Int.Output("TARGET_HEIGHT"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas( - kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"), - ) - - workflow_config = TTWorkflowSettings.create(**kwargs) - args = { - "workflow_config": workflow_config, - "sampler": kwargs["sampler"], - "sigmas": kwargs["sigmas"], - "seed": kwargs.get("seed"), - "steps": kwargs.get("steps"), - "cfg": kwargs.get("cfg"), - "width": kwargs.get("width"), - "height": kwargs.get("height"), - "target_width": kwargs.get("target_width"), - "target_height": kwargs.get("target_height"), - } - - return io.NodeOutput(*args.values()) - - -class TT_Sd35GgufWorkflowSettingsAdvancedNode(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="TT_Sd35GgufWorkflowSettingsAdvancedNode", - display_name="TT SD3.5 GGUF Workflow Settings (Advanced)", - category=CATEGORY, - description="", - inputs=[ - io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), - io.Int.Input("steps", default=30, min=1, max=10_000), - io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), - io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), - io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True), - io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), - io.String.Input("clip_g_positive", multiline=True, dynamic_prompts=True), - io.String.Input("t5xxl_positive", multiline=True, dynamic_prompts=True), - io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), - io.String.Input("clip_g_negative", multiline=True, dynamic_prompts=True), - io.String.Input("t5xxl_negative", multiline=True, dynamic_prompts=True), - io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), - io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), - io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), - ], - outputs=[ - WorkflowSettings.Output("WORKFLOW_CONFIG"), - io.Sampler.Output("SAMPLER"), - io.Sigmas.Output("SIGMAS"), - io.Int.Output("SEED"), - io.Int.Output("STEPS"), - io.Float.Output("CFG"), - io.Int.Output("WIDTH"), - io.Int.Output("HEIGHT"), - io.Int.Output("TARGET_WIDTH"), - io.Int.Output("TARGET_HEIGHT"), - io.String.Output("CLIP_L_POSITIVE"), - io.String.Output("CLIP_G_POSITIVE"), - io.String.Output("T5XXL_POSITIVE"), - io.String.Output("CLIP_L_NEGATIVE"), - io.String.Output("CLIP_G_NEGATIVE"), - io.String.Output("T5XXL_NEGATIVE"), - io.String.Output("LORA_TRIGGERS"), - ] - ) - - @classmethod - def execute(cls, **kwargs) -> io.NodeOutput: - kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas( - kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"), - ) - - workflow_config = TTWorkflowSettings.create(**kwargs) - args = { - "workflow_config": workflow_config, - "sampler": kwargs["sampler"], - "sigmas": kwargs["sigmas"], - "seed": kwargs.get("seed"), - "steps": kwargs.get("steps"), - "cfg": kwargs.get("cfg"), - "width": kwargs.get("width"), - "height": kwargs.get("height"), - "target_width": kwargs.get("target_width"), - "target_height": kwargs.get("target_height"), - "clip_l_positive": kwargs.get("clip_l_positive"), - "clip_g_positive": kwargs.get("clip_g_positive"), - "t5xxl_positive": kwargs.get("t5xxl_positive"), - "clip_l_negative": kwargs.get("clip_l_negative"), - "clip_g_negative": kwargs.get("clip_g_negative"), - "t5xxl_negative": kwargs.get("t5xxl_negative"), - "lora_triggers": kwargs.get("lora_triggers"), - } - - return io.NodeOutput(*args.values()) + return io.NodeOutput(workflow_config, *args) # ============================================================================== @@ -558,12 +134,5 @@ class TT_Sd35GgufWorkflowSettingsAdvancedNode(io.ComfyNode): # ============================================================================== NODES = [ - TT_SdxlWorkflowSettingsNode, - TT_SdxlWorkflowSettingsAdvancedNode, - TT_FluxWorkflowSettingsNode, - TT_FluxWorkflowSettingsAdvancedNode, - TT_Flux2WorkflowSettingsNode, - TT_Flux2WorkflowSettingsAdvancedNode, - TT_Sd35GgufWorkflowSettingsNode, - TT_Sd35GgufWorkflowSettingsAdvancedNode, + TT_LtxvWorkflowSettingsNode, ] diff --git a/nodes_workflow_d.py b/nodes_workflow_d.py new file mode 100644 index 0000000..6148fa3 --- /dev/null +++ b/nodes_workflow_d.py @@ -0,0 +1,575 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import dataclasses +import math +from dataclasses import dataclass + +import torch + +from comfy.samplers import KSampler, sampler_object, Sampler +from comfy_api.latest import io +from nodes import MAX_RESOLUTION + +CATEGORY = "TenserTensor/Workflow" + + +@dataclass +class TTWorkflowSettings(): + ascore_negative: float | None = None + ascore_positive: float | None = None + cfg: int | None = None + clip_g_negative: str | None = None + clip_g_positive: str | None = None + clip_l_negative: str | None = None + clip_l_positive: str | None = None + frame_rate: str | None = None + guidance: float | None = None + height: int | None = None + length_sec: float | None = None + lora_triggers: str | None = None + negative_prompt: str | None = None + noise_seed: int | None = None + positive_prompt: str | None = None + prompt: str | None = None + sampler: Sampler | None = None + sampler_name: str | None = None + scheduler: str | None = None + seed: int | None = None + sigmas: torch.Tensor | None = None + steps: int | None = None + target_height: int | None = None + target_width: int | None = None + t5xxl_negative: str | None = None + t5xxl_positive: str | None = None + width: int | None = None + + @classmethod + def create(cls, **kwargs) -> TTWorkflowSettings: + wf = cls() + for field in dataclasses.fields(wf): + wf.set_attr(field.name, kwargs.get(field.name, None)) + + return wf + + def update_attrs(self, **kwargs) -> TTWorkflowSettings: + for key in kwargs.keys(): + if hasattr(self, key): + val = kwargs.get(key) + if val is not None: + self.set_attr(key, val) + + return self + + def set_attr(self, key, attr) -> None: + if hasattr(self, key): + setattr(self, key, attr) + + def get_attr(self, key, default=None) -> None: + if hasattr(self, key): + val = getattr(self, key) + return val if val is not None else default + + +@io.comfytype(io_type="TT_WORKFLOW_CONFIG") +class WorkflowSettings: + Type = TTWorkflowSettings + + class Input(io.Input): + def __init__(self, id: str, **kwargs): + super().__init__(id, **kwargs) + + class Output(io.Output): + def __init__(self, id: str, **kwargs): + super().__init__(id, **kwargs) + + +class TT_SdxlWorkflowSettingsNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_SdxlWorkflowSettingsNode", + display_name="TT SDXL Workflow Settings", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), + io.Float.Input("ascore_positive", default=9.0, min=0.0, max=100.0, step=0.1), + io.Float.Input("ascore_negative", default=6.0, min=0.0, max=100.0, step=0.1), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.String.Output("SAMPLER_NAME"), + io.String.Output("SCHEDULER"), + io.Float.Output("ASCORE_POSITIVE"), + io.Float.Output("ASCORE_NEGATIVE"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + io.Int.Output("TARGET_WIDTH"), + io.Int.Output("TARGET_HEIGHT"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + workflow_config = TTWorkflowSettings.create(**kwargs) + args = {"workflow_config": workflow_config, **kwargs} + + return io.NodeOutput(*args.values()) + + +class TT_SdxlWorkflowSettingsAdvancedNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_SdxlWorkflowSettingsAdvancedNode", + display_name="TT SDXL Workflow Settings (Advanced)", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), + io.Float.Input("ascore_positive", default=9.0, min=0.0, max=100.0, step=0.1), + io.Float.Input("ascore_negative", default=6.0, min=0.0, max=100.0, step=0.1), + io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), + io.String.Input("clip_g_positive", multiline=True, dynamic_prompts=True), + io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), + io.String.Input("clip_g_negative", multiline=True, dynamic_prompts=True), + io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.String.Output("SAMPLER_NAME"), + io.String.Output("SCHEDULER"), + io.Float.Output("ASCORE_POSITIVE"), + io.Float.Output("ASCORE_NEGATIVE"), + io.String.Output("CLIP_L_POSITIVE"), + io.String.Output("CLIP_G_POSITIVE"), + io.String.Output("CLIP_L_NEGATIVE"), + io.String.Output("CLIP_G_NEGATIVE"), + io.String.Output("LORA_TRIGGERS"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + io.Int.Output("TARGET_WIDTH"), + io.Int.Output("TARGET_HEIGHT"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + workflow_config = TTWorkflowSettings.create(**kwargs) + args = {"workflow_config": workflow_config, **kwargs} + + return io.NodeOutput(*args.values()) + + +class TT_FluxWorkflowSettingsNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_FluxWorkflowSettingsNode", + display_name="TT FLUX Workflow Settings", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), + io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.String.Output("SAMPLER_NAME"), + io.String.Output("SCHEDULER"), + io.Float.Output("GUIDANCE"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + workflow_config = TTWorkflowSettings.create(**kwargs) + args = {"workflow_config": workflow_config, **kwargs} + + return io.NodeOutput(*args.values()) + + +class TT_FluxWorkflowSettingsAdvancedNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_FluxWorkflowSettingsAdvancedNode", + display_name="TT FLUX Workflow Settings (Advanced)", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Combo.Input("scheduler", options=KSampler.SCHEDULERS), + io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), + io.String.Input("t5xxl_positive", multiline=True, dynamic_prompts=True), + io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), + io.String.Input("t5xxl_negative", multiline=True, dynamic_prompts=True), + io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), + io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.String.Output("SAMPLER_NAME"), + io.String.Output("SCHEDULER"), + io.String.Output("CLIP_L_POSITIVE"), + io.String.Output("T5XXL_POSITIVE"), + io.String.Output("CLIP_L_NEGATIVE"), + io.String.Output("T5XXL_NEGATIVE"), + io.String.Output("LORA_TRIGGERS"), + io.Float.Output("GUIDANCE"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + workflow_config = TTWorkflowSettings.create(**kwargs) + args = {"workflow_config": workflow_config, **kwargs} + + return io.NodeOutput(*args.values()) + + +def compute_empirical_mu(image_seq_len, num_steps): + slope_high_res, intercept_high_res = 8.73809524e-05, 1.89833333 + slope_low_res, intercept_low_res = 0.00016927, 0.45666666 + HIGH_RES_THRESHOLD = 4300 + MIN_STEPS, MAX_STEPS = 10, 200 + mu_at_max_steps = slope_low_res * image_seq_len + intercept_low_res + + if image_seq_len > HIGH_RES_THRESHOLD: + return float(mu_at_max_steps) + + mu_at_min_steps = slope_high_res * image_seq_len + intercept_high_res + slope = (mu_at_max_steps - mu_at_min_steps) / (MAX_STEPS - MIN_STEPS) + intercept = mu_at_max_steps - MAX_STEPS * slope + mu = slope * num_steps + intercept + + return float(mu) + + +def generalized_time_snr_shift(t, mu, sigma): + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + +def get_schedule(steps, width, height): + image_seq_len = (width * height / (16 * 16)) + mu = compute_empirical_mu(image_seq_len, steps) + timesteps = torch.linspace(1, 0, steps + 1) + timesteps = generalized_time_snr_shift(timesteps, mu, 1.0) + + return timesteps + + +def build_sampler_sigmas(sampler_name, steps, width, height): + sampler = sampler_object(sampler_name) + sigmas = get_schedule(steps, width, height) + + return sampler, sigmas + + +class TT_Flux2WorkflowSettingsNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_Flux2WorkflowSettingsNode", + display_name="TT FLUX2 Workflow Settings", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1), + io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS) + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Sampler.Output("SAMPLER"), + io.Sigmas.Output("SIGMAS"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.Float.Output("GUIDANCE"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas( + kwargs.get("sampler_name"), + kwargs.get("steps"), + kwargs.get("width"), + kwargs.get("height"), + ) + + workflow_config = TTWorkflowSettings.create(**kwargs) + args = { + "workflow_config": workflow_config, + "sampler": kwargs["sampler"], + "sigmas": kwargs["sigmas"], + "seed": kwargs.get("seed"), + "steps": kwargs.get("steps"), + "cfg": kwargs.get("cfg"), + "guidance": kwargs.get("guidance"), + "width": kwargs.get("width"), + "height": kwargs.get("height"), + } + + return io.NodeOutput(*args.values()) + + +class TT_Flux2WorkflowSettingsAdvancedNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_Flux2WorkflowSettingsAdvancedNode", + display_name="TT FLUX2 Workflow Settings (Advanced)", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.String.Input("prompt", multiline=True, dynamic_prompts=True), + io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True), + io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Sampler.Output("SAMPLER"), + io.Sigmas.Output("SIGMAS"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + io.String.Output("PROMPT"), + io.String.Output("LORA_TRIGGERS"), + io.Float.Output("GUIDANCE"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas( + kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("width"), kwargs.get("height"), + ) + + workflow_config = TTWorkflowSettings.create(**kwargs) + args = { + "workflow_config": workflow_config, + "sampler": kwargs["sampler"], + "sigmas": kwargs["sigmas"], + "seed": kwargs.get("seed"), + "steps": kwargs.get("steps"), + "cfg": kwargs.get("cfg"), + "width": kwargs.get("width"), + "height": kwargs.get("height"), + "prompt": kwargs.get("prompt"), + "lora_triggers": kwargs.get("lora_triggers"), + "guidance": kwargs.get("guidance"), + } + + return io.NodeOutput(*args.values()) + + +def sd3_shift_sigma(sigma, schedule_shift): + return schedule_shift * sigma / (1 + (schedule_shift - 1) * sigma) + + +def build_sd3_sampler_sigmas(sampler_name, steps, schedule_shift=3.0): + sampler = sampler_object(sampler_name) + timesteps = torch.linspace(1, 0, steps + 1) + sigmas = sd3_shift_sigma(timesteps, schedule_shift) + + return sampler, sigmas + + +class TT_Sd35GgufWorkflowSettingsNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_Sd35GgufWorkflowSettingsNode", + display_name="TT SD3.5 GGUF Workflow Settings", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Sampler.Output("SAMPLER"), + io.Sigmas.Output("SIGMAS"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + io.Int.Output("TARGET_WIDTH"), + io.Int.Output("TARGET_HEIGHT"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas( + kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"), + ) + + workflow_config = TTWorkflowSettings.create(**kwargs) + args = { + "workflow_config": workflow_config, + "sampler": kwargs["sampler"], + "sigmas": kwargs["sigmas"], + "seed": kwargs.get("seed"), + "steps": kwargs.get("steps"), + "cfg": kwargs.get("cfg"), + "width": kwargs.get("width"), + "height": kwargs.get("height"), + "target_width": kwargs.get("target_width"), + "target_height": kwargs.get("target_height"), + } + + return io.NodeOutput(*args.values()) + + +class TT_Sd35GgufWorkflowSettingsAdvancedNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="TT_Sd35GgufWorkflowSettingsAdvancedNode", + display_name="TT SD3.5 GGUF Workflow Settings (Advanced)", + category=CATEGORY, + description="", + inputs=[ + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff), + io.Int.Input("steps", default=30, min=1, max=10_000), + io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1), + io.Combo.Input("sampler_name", options=KSampler.SAMPLERS), + io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True), + io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True), + io.String.Input("clip_g_positive", multiline=True, dynamic_prompts=True), + io.String.Input("t5xxl_positive", multiline=True, dynamic_prompts=True), + io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True), + io.String.Input("clip_g_negative", multiline=True, dynamic_prompts=True), + io.String.Input("t5xxl_negative", multiline=True, dynamic_prompts=True), + io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True), + io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8), + io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True), + ], + outputs=[ + WorkflowSettings.Output("WORKFLOW_CONFIG"), + io.Sampler.Output("SAMPLER"), + io.Sigmas.Output("SIGMAS"), + io.Int.Output("SEED"), + io.Int.Output("STEPS"), + io.Float.Output("CFG"), + io.Int.Output("WIDTH"), + io.Int.Output("HEIGHT"), + io.Int.Output("TARGET_WIDTH"), + io.Int.Output("TARGET_HEIGHT"), + io.String.Output("CLIP_L_POSITIVE"), + io.String.Output("CLIP_G_POSITIVE"), + io.String.Output("T5XXL_POSITIVE"), + io.String.Output("CLIP_L_NEGATIVE"), + io.String.Output("CLIP_G_NEGATIVE"), + io.String.Output("T5XXL_NEGATIVE"), + io.String.Output("LORA_TRIGGERS"), + ] + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas( + kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"), + ) + + workflow_config = TTWorkflowSettings.create(**kwargs) + args = { + "workflow_config": workflow_config, + "sampler": kwargs["sampler"], + "sigmas": kwargs["sigmas"], + "seed": kwargs.get("seed"), + "steps": kwargs.get("steps"), + "cfg": kwargs.get("cfg"), + "width": kwargs.get("width"), + "height": kwargs.get("height"), + "target_width": kwargs.get("target_width"), + "target_height": kwargs.get("target_height"), + "clip_l_positive": kwargs.get("clip_l_positive"), + "clip_g_positive": kwargs.get("clip_g_positive"), + "t5xxl_positive": kwargs.get("t5xxl_positive"), + "clip_l_negative": kwargs.get("clip_l_negative"), + "clip_g_negative": kwargs.get("clip_g_negative"), + "t5xxl_negative": kwargs.get("t5xxl_negative"), + "lora_triggers": kwargs.get("lora_triggers"), + } + + return io.NodeOutput(*args.values()) + + +# ============================================================================== +# V3 entrypoint — registers context nodes with ComfyUI +# ============================================================================== + +NODES = [ + TT_SdxlWorkflowSettingsNode, + TT_SdxlWorkflowSettingsAdvancedNode, + TT_FluxWorkflowSettingsNode, + TT_FluxWorkflowSettingsAdvancedNode, + TT_Flux2WorkflowSettingsNode, + TT_Flux2WorkflowSettingsAdvancedNode, + TT_Sd35GgufWorkflowSettingsNode, + TT_Sd35GgufWorkflowSettingsAdvancedNode, +] diff --git a/pyproject.toml b/pyproject.toml index 2ef65a3..1766bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ description = """A collection of custom nodes for ComfyUI that streamline common — VAE encoders and decoders: tiled and context-based — Image tools: load, resize, upscale, preview, save, ControlNet reference — Post-production: brightness, contrast, LUT color grading, film grain, upscaler""" -version = "1.7.8" +version = "2.0.0" license = { file = "LICENSE" } dependencies = ["gguf", "kornia"]