Refactoring: Started migration to ComfyUI API v3

This commit is contained in:
TenserTensor
2026-02-26 18:03:36 +03:00
parent 83ff801f0f
commit 0716d05f7d
15 changed files with 378 additions and 33 deletions
+2 -4
View File
@@ -1,17 +1,15 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from .node_mappings import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, RESET, BRIGHT_GREEN, BRIGHT_CYAN, CYAN, YELLOW
from .node_mappings import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, NODES_COUNT, RESET, BRIGHT_GREEN, BRIGHT_CYAN, CYAN, YELLOW
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
nodes_count = len(NODE_CLASS_MAPPINGS)
print()
SEP = f"{CYAN}{'=' * 60}{RESET}"
print(SEP)
print(f"{YELLOW}TenserTensor ComfyUI Nodes Pack (v1.4.7): {BRIGHT_GREEN}{nodes_count} Nodes Loaded{RESET}")
print(f"{YELLOW}TenserTensor ComfyUI Nodes Pack (v1.4.7): {BRIGHT_GREEN}{NODES_COUNT} Nodes Loaded{RESET}")
print(SEP)
print(f"{BRIGHT_CYAN}[V] Repository: https://github.com/tenser-tensor/ComfyUI-TenserTensor{RESET}")
print(f"{BRIGHT_CYAN}[V] ComfyUI Registry: https://registry.comfy.org/publishers/tenser-tensor{RESET}")
+141
View File
@@ -0,0 +1,141 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import dataclasses
import re
from typing import override
import torch
from comfy.clip_vision import Output
from comfy_api.latest import IO, ComfyExtension, Input
from .lib.common import Context, TTContext
CATEGORY = "TenserTensor/Context"
class ContextNode(IO.ComfyNode):
"""
Abstract base class for all context nodes.
Handles context creation, delegation to handle(), and building return tuple.
Subclasses should set INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES from a Schema,
and override handle() to implement node-specific logic.
"""
REQUIRED = ()
OPTIONAL = ()
RETURNS = ()
F_TYPES = {}
@classmethod
def fill_field_types(cls):
cls.F_TYPES = {f.name: f.type.__args__[0] for f in dataclasses.fields(TTContext)}
@classmethod
def inputs(cls) -> list[Input]:
def get_input(input_name, optional):
return Context.Input("context", optional=optional) if input_name == "context" else cls.F_TYPES[input_name].Input(input_name,
optional=optional)
len(cls.F_TYPES) == 0 and cls.fill_field_types()
input_schema = []
for f_name in cls.REQUIRED:
input_schema.append(get_input(f_name, optional=False))
for f_name in cls.OPTIONAL:
input_schema.append(get_input(f_name, optional=True))
return input_schema
@classmethod
def outputs(cls) -> list[Output]:
len(cls.F_TYPES) == 0 and cls.fill_field_types()
output_schema = []
for f_name in cls.RETURNS if len(cls.RETURNS) > 0 else cls.OPTIONAL:
output_schema.append(
Context.Output(display_name="CONTEXT") if f_name == "context" else cls.F_TYPES[f_name].Output(display_name=f_name.upper()))
return output_schema
@classmethod
def node_output(cls, context, **kwargs) -> IO.NodeOutput:
args = {}
for f_name in cls.RETURNS if len(cls.RETURNS) > 0 else cls.OPTIONAL:
if f_name != "context":
args[f_name.upper()] = kwargs.get(f_name)
return IO.NodeOutput(**{{"context": context}, 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:
return IO.Schema(
node_id=cls.__name__,
display_name=cls.display_name(),
category=CATEGORY,
inputs=cls.inputs(),
outputs=cls.outputs(),
)
@classmethod
def handle(cls, context, **kwargs):
return (context, kwargs,)
@classmethod
def execute(cls, context=None, **kwargs) -> IO.NodeOutput:
context = TTContext.create(**kwargs) if not context else context
context, kwargs = cls.handle(context, **kwargs)
return cls.node_output(context, **kwargs)
class TT_BaseContextNode(ContextNode):
"""Base context node. Accepts model, clip, vae, latent and optional workflow_config."""
REQUIRED = ("model", "clip", "vae", "latent")
OPTIONAL = ("workflow_config",)
RETURNS = ("context",)
class TT_BaseContextFlux2Node(ContextNode):
"""
Base context node for FLUX2 workflows. Accepts model, clip, vae, latent, sampler, noise, and sigmas.
Optionally accepts workflow_config. Returns context.
"""
REQUIRED = ("model", "clip", "vae", "latent", "sampler", "rnd_noise", "sigmas",)
OPTIONAL = ("workflow_config",)
RETURNS = ("context",)
class TT_BaseContextPassthroughNode(ContextNode):
"""Passes context through without modification."""
REQUIRED = ("context",)
OPTIONAL = ()
RETURNS = ("context",)
class ContextExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
TT_BaseContextNode,
TT_BaseContextFlux2Node,
]
async def comfy_entrypoint() -> ContextExtension:
return ContextExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_BaseContextNode",
"TT_BaseContextFlux2Node",
"TT_BaseContextPassthroughNode",
]
View File
+118
View File
@@ -1,4 +1,12 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import dataclasses
from dataclasses import dataclass
import comfy.samplers as S
from comfy_api.latest import IO
from .workflow_classes import TTWorkflowSettings
class BasicGuider(S.CFGGuider):
@@ -11,3 +19,113 @@ class BasicGuider(S.CFGGuider):
class CommonTypes():
MEGAPIXELS = ["0.25 MP", "0.5 MP", "1 MP", "2 MP", "4 MP", "8 MP"]
@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, **kwargs):
super().__init__(**kwargs)
@dataclass
class TTContext():
"""
Shared context object passed between TenserTensor context nodes.
Holds all pipeline state as optional fields.
All fields default to None and are populated incrementally as the workflow progresses.
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.
"""
model: IO.Model | None = None
clip: IO.Clip | None = None
vae: IO.Vae | None = None
guider: IO.Guider | None = None
latent: IO.Latent | None = None
positive: IO.Conditioning | None = None
negative: IO.Conditioning | None = None
conditioning: IO.Conditioning | None = None
image: IO.Image | None = None
mask: IO.Mask | None = None
control_net: IO.ControlNet | None = None
workflow_config: WorkflowSettings | None = None
sampler_name: IO.String | None = None
sampler: IO.Sampler | None = None
scheduler: list[str] | None = None
rnd_noise: IO.Noise | None = None
sigmas: IO.Sigmas | None = None
@classmethod
def create(cls, **kwargs):
ctx = cls()
for field in dataclasses.fields(ctx):
setattr(ctx, field.name, kwargs.get(field.name, None))
return ctx
@classmethod
def get_attr_type(cls, key):
return
def set_attr(self, key, attr):
if hasattr(self, key):
setattr(self, key, attr)
else:
raise AttributeError(f"ERROR: Context has no attribute '{key}'")
def get_attr(self, key, default=None):
if hasattr(self, key):
val = getattr(self, key)
return val if val is not None else default
else:
raise AttributeError(f"ERROR: Context has no attribute '{key}'")
def update_attrs(self, **kwargs):
for key in kwargs.keys():
if hasattr(self, key):
val = kwargs.get(key)
if val is not None:
setattr(self, key, val)
def set_workflow_config_attr(self, key, attr):
if self.workflow_config:
self.workflow_config.set_attr(key, attr)
else:
raise AttributeError(f"ERROR: Workflow Config is not set in context")
def get_workflow_config_attr(self, key, default=None):
if self.workflow_config:
return self.workflow_config.get_attr(key, default)
else:
raise AttributeError(f"ERROR: Workflow Config is not set in context")
def update_workflow_config_attrs(self, **kwargs):
if self.workflow_config:
for key in kwargs.keys():
if hasattr(self.workflow_config, key):
val = kwargs.get(key)
if val is not None:
self.workflow_config.set_attr(key, val)
else:
raise AttributeError(f"ERROR: Workflow Config is not set in context")
@IO.comfytype(io_type="TT_CONTEXT")
class Context:
Type = TTContext
class Input(IO.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(IO.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
+52
View File
@@ -0,0 +1,52 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import dataclasses
from dataclasses import dataclass
from comfy.samplers import KSampler
@dataclass
class TTWorkflowSettings():
seed: int | None = None
steps: int | None = None
cfg: int | None = None
sampler_name: list[str] | None = None
scheduler_name: list[str] | None = None
clip_l_positive: str | None = None
clip_g_positive: str | None = None
t5xxl_positive: str | None = None
clip_l_negative: str | None = None
clip_g_negative: str | None = None
t5xxl_negative: str | None = None
guidance: float | None = None
ascore_positive: float | None = None
ascore_negative: float | None = None
width: int | None = None
height: int | None = None
target_width: int | None = None
target_height: int | None = None
@classmethod
def create(cls, **kwargs):
wf = cls()
for field in dataclasses.fields(wf):
setattr(wf, field.name, kwargs.get(field.name, None))
setattr(wf, "sampler_name", KSampler.SAMPLERS)
setattr(wf, "scheduler_name", KSampler.SCHEDULERS)
return wf
def set_attr(self, key, attr):
if hasattr(self, key):
setattr(self, key, attr)
else:
raise AttributeError(f"ERROR: Context has no attribute '{key}'")
def get_attr(self, key, default=None):
if hasattr(self, key):
val = getattr(self, key)
return val if val is not None else default
else:
raise AttributeError(f"ERROR: Context has no attribute '{key}'")
+56 -20
View File
@@ -11,90 +11,120 @@ WHITE = "\033[37m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_CYAN = "\033[96m"
NODES_COUNT = 0
try:
before = set(dir())
from .context_nodes import *
from .nodes.context.base_context import TT_BaseContext
from .nodes.context.context import TT_Context
from .nodes.context.context_passthrough import TT_ContextPassthrough
from .nodes.context.context_set_guider import TT_ContextSetGuider
from .nodes.context.context_set_image import TT_ContextSetImage
from .nodes.context.context_set_latent import TT_ContextSetLatent
from .nodes.context.context_set_guider import TT_ContextSetGuider
from .nodes.context.even_larger_context import TT_EvenLargerContext
from .nodes.context.large_context_flux import TT_LargeContextFlux
from .nodes.context.large_context_sdxl import TT_LargeContextSdxl
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Context nodes unavailable{RESET}")
try:
from .nodes.image.image_preview_save import TT_ImagePreviewSave
from .nodes.image.image_preview_upscale_save import TT_ImagePreviewUpscaleSave
from .nodes.image.guider_image_reference import TT_GuiderImageReference
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.image.image_preview_save import TT_ImagePreviewSave
from .nodes.image.image_preview_upscale_save import TT_ImagePreviewUpscaleSave
from .nodes.image.guider_image_reference import TT_GuiderImageReference
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.latent.latent_factory import TT_LatentFactory
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Latent nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.loaders.flux_models_loader import TT_FluxModelsLoader
from .nodes.loaders.flux_models_loader_advanced import TT_FluxModelsLoaderAdvanced
from .nodes.loaders.gguf_models_loader import TT_Flux2GgufModelsLoader
from .nodes.loaders.gguf_models_loader_advanced import TT_Flux2GgufModelsLoaderAdvanced
from .nodes.loaders.sdxl_models_loader import TT_SdxlModelsLoader
from .nodes.loaders.sdxl_models_loader_advanced import TT_SdxlModelsLoaderAdvanced
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Loader nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.postproduction.add_film_grain import TT_AddFilmGrain
from .nodes.postproduction.apply_lut import TT_ApplyLut
from .nodes.postproduction.image_enhancer import TT_ImageEnhancer
from .nodes.postproduction.postproduction import TT_Postproduction
from .nodes.postproduction.postproduction_advanced import TT_PostproductionAdvanced
from .nodes.postproduction.quick_image_upscaler import TT_QuickImageUpscaler
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Postproduction nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.sampling.ksampler import TT_KSampler
from .nodes.sampling.ksampler_advanced import TT_KSamplerAdvanced
from .nodes.sampling.ksampler_context import TT_KSamplerContext
from .nodes.sampling.ksampler_two_stage import TT_KSamplerTwoStage
from .nodes.sampling.ksampler_guided import TT_KSamplerGuided
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print("f{YELLOW}TenserTensor: {RED}ERROR: Sampler nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.text_encoders.clip_text_encode_flux import TT_ClipTextEncodeFlux
from .nodes.text_encoders.clip_text_encode_flux_context import TT_ClipTextEncodeFluxContext
from .nodes.text_encoders.clip_text_encode_sdxl import TT_ClipTextEncodeSdxl
from .nodes.text_encoders.clip_text_encode_sdxl_context import TT_ClipTextEncodeSdxlContext
from .nodes.text_encoders.clip_text_encode_flux2 import TT_ClipTextEncodeFlux2
from .nodes.text_encoders.clip_text_encode_flux2_context import TT_ClipTextEncodeFlux2Context
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Text Encoder nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.vae.vae_decode_context import TT_VaeDecodeContext
from .nodes.vae.vae_decode_tiled import TT_VaeDecodeTiled
from .nodes.vae.vae_encode_context import TT_VaeEncodeContext
from .nodes.vae.vae_encode_tiled import TT_VaeEncodeTiled
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: VAE nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.workflow.flux_workflow_settings import TT_FluxWorkflowSettings
from .nodes.workflow.flux_workflow_settings_advanced import TT_FluxWorkflowSettingsAdvanced
from .nodes.workflow.sdxl_workflow_settings import TT_SdxlWorkflowSettings
from .nodes.workflow.sdxl_workflow_settings_advanced import TT_SdxlWorkflowSettingsAdvanced
from .nodes.workflow.flux2_workflow_settings import TT_Flux2WorkflowSettings
from .nodes.workflow.flux2_workflow_settings_advanced import TT_Flux2WorkflowSettingsAdvanced
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Workflow nodes unavailable{RESET}")
NODE_CLASS_MAPPINGS = {
# Context
# Context Deprecated
"TT_BaseContext": TT_BaseContext,
"TT_Context": TT_Context,
"TT_LargeContextFlux": TT_LargeContextFlux,
@@ -103,8 +133,11 @@ NODE_CLASS_MAPPINGS = {
"TT_ContextSetImage": TT_ContextSetImage,
"TT_ContextSetLatent": TT_ContextSetLatent,
"TT_ContextSetGuider": TT_ContextSetGuider,
# Context Passthrough
"TT_ContextPassthrough": TT_ContextPassthrough,
# Context V3 Nodes
"TT_BaseContextNode": TT_BaseContextNode,
"TT_BaseContextFlux2Node": TT_BaseContextFlux2Node,
"TT_BaseContextPassthroughNode": TT_BaseContextPassthroughNode,
# Image
"TT_ImagePreviewSave": TT_ImagePreviewSave,
"TT_ImagePreviewUpscaleSave": TT_ImagePreviewUpscaleSave,
@@ -153,17 +186,20 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Context
"TT_BaseContext": "TT Base Context",
"TT_Context": "TT Context",
"TT_LargeContextFlux": "TT FLUX Large Context",
"TT_LargeContextSdxl": "TT SDXL Large Context",
"TT_EvenLargerContext": "TT Even Larger Context",
"TT_ContextSetImage": "TT Context Set Image",
"TT_ContextSetLatent": "TT Context Set Latent",
"TT_ContextSetGuider": "TT Context Set Guider",
# Context Passthrough
"TT_ContextPassthrough": "TT Context Passthrough",
# Context Deprecated
"TT_BaseContext": "TT Base Context (Deprecated)",
"TT_Context": "TT Context (Deprecated)",
"TT_LargeContextFlux": "TT FLUX Large Context (Deprecated)",
"TT_LargeContextSdxl": "TT SDXL Large Context (Deprecated)",
"TT_EvenLargerContext": "TT Even Larger Context (Deprecated)",
"TT_ContextSetImage": "TT Context Set Image (Deprecated)",
"TT_ContextSetLatent": "TT Context Set Latent (Deprecated)",
"TT_ContextSetGuider": "TT Context Set Guider (Deprecated)",
"TT_ContextPassthrough": "TT Context Passthrough (Deprecated)",
# Context V3 Nodes
"TT_BaseContextNode": "TT Base Context",
"TT_BaseContextFlux2Node": "TT FLUX2 Base Context",
"TT_BaseContextPassthroughNode": "TT Context Passthrough",
# Image
"TT_ImagePreviewSave": "TT Image Preview / Save",
"TT_ImagePreviewUpscaleSave": "TT Image Preview / Upscale / Save",
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import create_context_schema
class TT_BaseContext:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("base")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import (build_return_tuple, create_context_schema, init_co
class TT_Context:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("standard")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import (build_return_tuple, create_context_schema, init_co
class TT_ContextPassthrough:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("passthrough")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import create_context_schema
class TT_ContextSetGuider:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("set_guider")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import create_context_schema
class TT_ContextSetImage:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("set_image")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import create_context_schema
class TT_ContextSetLatent:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("set_latent")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import (build_return_tuple, create_context_schema, init_co
class TT_EvenLargerContext:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("huge")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import (build_return_tuple, create_context_schema, init_co
class TT_LargeContextFlux:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("big_flux")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):
+1 -1
View File
@@ -6,7 +6,7 @@ from .context_helpers import (build_return_tuple, create_context_schema, init_co
class TT_LargeContextSdxl:
INPUT_TYPES_DICT, RETURN_TYPES, RETURN_NAMES = create_context_schema("big_sdxl")
FUNCTION = "process_context"
CATEGORY = "TenserTensor/Context"
CATEGORY = "TenserTensor/Deprecated/Context"
@classmethod
def INPUT_TYPES(cls):