diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2cb5ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +## v1.4.7 (2026-02-25) + +### New Nodes +- **TT KSampler (Guided)** — guided diffusion sampling with external guider, sigmas, and sampler +- **TT Guider Image Reference** — encodes reference image into latent space and injects it into Guider conditioning +- **TT Image Preview / Upscale / Save** — all-in-one preview, upscale, and save node +- **TT FLUX2 GGUF Models Loader** / **(Advanced)** — loaders for FLUX2 GGUF-quantized models +- **TT CLIP Text Encode FLUX2** / **(Context)** — text encoders for FLUX2 models +- **TT FLUX2 Workflow Settings** / **(Advanced)** — workflow configuration nodes for FLUX2 +- **TT Context Set Guider** — injects guider into TT_CONTEXT object + +### Improvements +- Added `BasicGuider` and `CommonTypes` to shared lib (`lib/common.py`) +- Fixed missing f-strings in all error print statements across node imports +- Fixed workflow settings module filenames (typo: `workfow` → `workflow`) + +### Docs +- Added docs for all new nodes +- Updated NODES.md with descriptions for all new nodes +- Updated Latent Factory description diff --git a/NODES.md b/NODES.md index 3b5fd95..7ee405c 100644 --- a/NODES.md +++ b/NODES.md @@ -10,6 +10,20 @@ Node for configuring Flux workflow generation parameters. Advanced node for configuring Flux workflow generation parameters with separate CLIP-L and T5XXL prompts. +#### TT Flux2 Workflow Settings + +Use this node as the single source of truth for FLUX2 workflow parameters. The `WORKFLOW_CONFIG` output is +designed for context-aware nodes that consume a TT_WORKFLOW_CONFIG directly. Individual outputs are available +for connecting to standard ComfyUI nodes. The sigma schedule (`SCHEDULER`) is computed automatically from +the seed and resolution via `get_schedule`. + +#### TT Flux2 Workflow Settings (Advanced) + +Advanced version of TT Flux2 Workflow Settings with `prompt` and `lora_triggers` fields included directly in the +node. Both are stored in the `WORKFLOW_CONFIG` output alongside all other sampling parameters, making this node +sufficient as the sole configuration source for full context-driven FLUX2 pipelines. Individual outputs remain +available for connecting to standard ComfyUI nodes. + #### TT SDXL Workflow Settings Node for configuring SDXL workflow generation parameters. @@ -22,9 +36,10 @@ Advanced SDXL workflow configuration with separate CLIP-L/G prompts and aestheti #### TT Latent Factory -Generate empty latent images with precise dimension control based on aspect ratio and megapixel count. - -Automatically calculates correct dimensions for FLUX and SDXL models, plus separate CLIP conditioning sizes. +Select aspect ratio, resolution, and orientation to automatically compute pixel dimensions suited for the chosen +model. FLUX1.D uses 16-channel latents, FLUX2.D uses 128-channel, SDXL uses 4-channel. `clip_multiplier` scales the TARGET +dimensions relative to the image dimensions — useful for multi-resolution CLIP conditioning. Connect `RANDOM_NOISE` +directly to a sampler node for reproducible noise tied to `noise_seed`. ### Loaders @@ -36,6 +51,14 @@ All-in-one model loader for SDXL workflows with checkpoint merging support. Extended all-in-one model loader for SDXL with checkpoint merging and LoRA support. +#### TT GGUF Models Loader + +All-in-one loader for GGUF-quantized models. Place your `.gguf` diffusion model files in the `diffusion_models` or `unet` directory, CLIP files in `text_encoders` or `clip` directory. VAE can be any standard ComfyUI-compatible VAE file. + +#### TT GGUF Models Loader (Advanced) + +Extended version of GGUF Models Loader. Supports up to 4 LoRAs loaded simultaneously. Enable `apply_sampling` to apply flux sampling shift patch — useful for improving quality at non-standard resolutions. Advanced parameters are hidden by default and can be revealed via the node's show/hide controls. + #### TT FLUX Models Loader All-in-one model loader for Flux workflows. @@ -62,6 +85,19 @@ SDXL text encoder with separate CLIP-L and CLIP-G prompt inputs and conditioning Context-based SDXL text encoder that extracts prompts and parameters from workflow config. +#### TT Clip Text Encode Flux2 + +Encodes text prompts specifically for FLUX2 models. Enter your main prompt in `prompt` and any LoRA-specific trigger +words separately in `lora_triggers` — the node combines them internally. Adjust `guidance` to control how strongly +the model follows the prompt (3.5 is a good starting point for FLUX2). + +#### TT Clip Text Encode Flux2 (Context) + +Context-aware version of the FLUX2 text encoder. Instead of individual inputs, it reads `model`, `clip`, and +`workflow_config` (containing `prompt`, `lora_triggers`, and `guidance`) from the passed `context`. The resulting +guider is both returned as an output and stored back into the context under the `guider` key. Raises an error if +any of the required fields are missing from the context. + ### Samplers #### TT KSampler @@ -90,6 +126,12 @@ Automated two-stage sampling workflow. First performs a draft pass with partial a second pass. Each stage can use different samplers, schedulers, and step counts. Useful for quick previews followed by quality refinement, or combining different sampling strategies. +#### TT KSampler (Guided) + +Runs guided diffusion sampling on the input latent. Connect a guider from a text encoder node, sigmas from a +scheduler, and a sampler from a workflow settings node. `random_noise` controls stochasticity — use a seeded +noise node for reproducible results. + ### VAE #### TT VAE Decode (Context) @@ -164,6 +206,10 @@ saturation, sharpness, posterize, and solarize adjustments. All effects are opti Dual-purpose node that can preview images in the UI and/or save them to disk with extensive formatting options. +#### TT Image Preview / Upscale / Save + +All-in-one node that can preview, upscale, and save images in a single step. Upscaling is applied before saving — the upscaled result is both displayed in the UI and written to disk. When `upscale_image` is disabled, the original image is used. When `save_image` is disabled, the image is only previewed in the UI without writing to disk. + ### Context #### TT Base Context @@ -199,6 +245,11 @@ Stores pixel-space image in context for use by downstream nodes. Overwrites exis Stores latent representation in context for use by downstream nodes. Overwrites existing latent if present. +#### TT Context Set Guider + +Sets the `guider` field inside a TT_CONTEXT object. Use this node when you have a guider produced outside the +context pipeline and need to inject it back in for use by subsequent context-aware nodes. + #### TT Context Passthrough Use this node to pass context through your workflow while optionally modifying specific parameters without changing the diff --git a/__init__.py b/__init__.py index c90cd9d..d0687f0 100644 --- a/__init__.py +++ b/__init__.py @@ -6,9 +6,12 @@ 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.3.1): {BRIGHT_GREEN}38 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}") diff --git a/lib/common.py b/lib/common.py new file mode 100644 index 0000000..a693e3b --- /dev/null +++ b/lib/common.py @@ -0,0 +1,13 @@ +import comfy.samplers as S + + +class BasicGuider(S.CFGGuider): + def set_conds(self, conditioning): + self.inner_set_conds({"positive": conditioning}) + + def get_conds(self, key="positive"): + return [[c.get("cross_attn", None), c] for c in self.original_conds[key]] + + +class CommonTypes(): + MEGAPIXELS = ["0.25 MP", "0.5 MP", "1 MP", "2 MP", "4 MP", "8 MP"] diff --git a/node_mappings.py b/node_mappings.py index f80cef5..7705b61 100644 --- a/node_mappings.py +++ b/node_mappings.py @@ -17,31 +17,34 @@ try: from .nodes.context.context_passthrough import TT_ContextPassthrough 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("{YELLOW}TenserTensor: {RED}ERROR: Context nodes unavailable{RESET}") + 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 except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}") try: from .nodes.latent.latent_factory import TT_LatentFactory except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Latent nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Latent nodes unavailable{RESET}") try: 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_GgufModelsLoader - from .nodes.loaders.gguf_models_loader_advanced import TT_GgufModelsLoaderAdvanced + 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 except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Loader nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Loader nodes unavailable{RESET}") try: from .nodes.postproduction.add_film_grain import TT_AddFilmGrain @@ -51,23 +54,26 @@ try: from .nodes.postproduction.postproduction_advanced import TT_PostproductionAdvanced from .nodes.postproduction.quick_image_upscaler import TT_QuickImageUpscaler except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Postproduction nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Postproduction nodes unavailable{RESET}") try: 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 except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Sampler nodes unavailable{RESET}") + print("f{YELLOW}TenserTensor: {RED}ERROR: Sampler nodes unavailable{RESET}") try: 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 except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Text Encoder nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Text Encoder nodes unavailable{RESET}") try: from .nodes.vae.vae_decode_context import TT_VaeDecodeContext @@ -75,15 +81,17 @@ try: from .nodes.vae.vae_encode_context import TT_VaeEncodeContext from .nodes.vae.vae_encode_tiled import TT_VaeEncodeTiled except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: VAE nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: VAE nodes unavailable{RESET}") try: - from .nodes.workflow.flux_workfow_settings import TT_FluxWorkflowSettings - from .nodes.workflow.flux_workfow_settings_advanced import TT_FluxWorkflowSettingsAdvanced - from .nodes.workflow.sdxl_workfow_settings import TT_SdxlWorkflowSettings - from .nodes.workflow.sdxl_workfow_settings_advanced import TT_SdxlWorkflowSettingsAdvanced + 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 except ImportError: - print("{YELLOW}TenserTensor: {RED}ERROR: Workflow nodes unavailable{RESET}") + print(f"{YELLOW}TenserTensor: {RED}ERROR: Workflow nodes unavailable{RESET}") NODE_CLASS_MAPPINGS = { # Context @@ -94,17 +102,20 @@ NODE_CLASS_MAPPINGS = { "TT_EvenLargerContext": TT_EvenLargerContext, "TT_ContextSetImage": TT_ContextSetImage, "TT_ContextSetLatent": TT_ContextSetLatent, + "TT_ContextSetGuider": TT_ContextSetGuider, # Context Passthrough "TT_ContextPassthrough": TT_ContextPassthrough, # Image "TT_ImagePreviewSave": TT_ImagePreviewSave, + "TT_ImagePreviewUpscaleSave": TT_ImagePreviewUpscaleSave, + "TT_GuiderImageReference": TT_GuiderImageReference, # Latent "TT_LatentFactory": TT_LatentFactory, # Loaders "TT_SdxlModelsLoader": TT_SdxlModelsLoader, "TT_SdxlModelsLoaderAdvanced": TT_SdxlModelsLoaderAdvanced, - "TT_GgufModelsLoader": TT_GgufModelsLoader, - "TT_GgufModelsLoaderAdvanced": TT_GgufModelsLoaderAdvanced, + "TT_Flux2GgufModelsLoader": TT_Flux2GgufModelsLoader, + "TT_Flux2GgufModelsLoaderAdvanced": TT_Flux2GgufModelsLoaderAdvanced, "TT_FluxModelsLoader": TT_FluxModelsLoader, "TT_FluxModelsLoaderAdvanced": TT_FluxModelsLoaderAdvanced, # Postproduction @@ -119,11 +130,14 @@ NODE_CLASS_MAPPINGS = { "TT_KSamplerAdvanced": TT_KSamplerAdvanced, "TT_KSamplerContext": TT_KSamplerContext, "TT_KSamplerTwoStage": TT_KSamplerTwoStage, + "TT_KSamplerGuided": TT_KSamplerGuided, # Text Encoder "TT_ClipTextEncodeFlux": TT_ClipTextEncodeFlux, "TT_ClipTextEncodeFluxContext": TT_ClipTextEncodeFluxContext, "TT_ClipTextEncodeSdxl": TT_ClipTextEncodeSdxl, "TT_ClipTextEncodeSdxlContext": TT_ClipTextEncodeSdxlContext, + "TT_ClipTextEncodeFlux2": TT_ClipTextEncodeFlux2, + "TT_ClipTextEncodeFlux2Context": TT_ClipTextEncodeFlux2Context, # VAE "TT_VaeDecodeContext": TT_VaeDecodeContext, "TT_VaeDecodeTiled": TT_VaeDecodeTiled, @@ -134,6 +148,8 @@ NODE_CLASS_MAPPINGS = { "TT_FluxWorkflowSettingsAdvanced": TT_FluxWorkflowSettingsAdvanced, "TT_SdxlWorkflowSettings": TT_SdxlWorkflowSettings, "TT_SdxlWorkflowSettingsAdvanced": TT_SdxlWorkflowSettingsAdvanced, + "TT_Flux2WorkflowSettings": TT_Flux2WorkflowSettings, + "TT_Flux2WorkflowSettingsAdvanced": TT_Flux2WorkflowSettingsAdvanced, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -145,17 +161,20 @@ NODE_DISPLAY_NAME_MAPPINGS = { "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", # Image "TT_ImagePreviewSave": "TT Image Preview / Save", + "TT_ImagePreviewUpscaleSave": "TT Image Preview / Upscale / Save", + "TT_GuiderImageReference": "TT Guider Image Reference", # Latent "TT_LatentFactory": "TT Latent Factory", # Loaders "TT_SdxlModelsLoader": "TT SDXL Models Loader", "TT_SdxlModelsLoaderAdvanced": "TT SDXL Models Loader (Advanced)", - "TT_GgufModelsLoader": "TT GGUF Models Loader", - "TT_GgufModelsLoaderAdvanced": "TT GGUF Models Loader (Advanced)", + "TT_Flux2GgufModelsLoader": "TT FLUX2 GGUF Models Loader", + "TT_Flux2GgufModelsLoaderAdvanced": "TT FLUX2 GGUF Models Loader (Advanced)", "TT_FluxModelsLoader": "TT FLUX Models Loader", "TT_FluxModelsLoaderAdvanced": "TT FLUX Models Loader (Advanced)", # Postproduction @@ -170,11 +189,14 @@ NODE_DISPLAY_NAME_MAPPINGS = { "TT_KSamplerAdvanced": "TT KSampler (Advanced)", "TT_KSamplerContext": "TT KSampler (Context)", "TT_KSamplerTwoStage": "TT KSampler (Two Stages)", + "TT_KSamplerGuided": "TT KSampler (Guided)", # Text Encoder "TT_ClipTextEncodeFlux": "TT CLIP Text Encode FLUX", "TT_ClipTextEncodeFluxContext": "TT CLIP Text Encode FLUX (Context)", "TT_ClipTextEncodeSdxl": "TT CLIP Text Encode SDXL", "TT_ClipTextEncodeSdxlContext": "TT CLIP Text Encode SDXL (Context)", + "TT_ClipTextEncodeFlux2": "TT CLIP Text Encode FLUX2", + "TT_ClipTextEncodeFlux2Context": "TT CLIP Text Encode FLUX2 (Context)", # VAE "TT_VaeDecodeContext": "TT VAE Decode (Context)", "TT_VaeDecodeTiled": "TT VAE Decode (Tiled)", @@ -185,4 +207,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "TT_FluxWorkflowSettingsAdvanced": "TT FLUX Workflow Settings (Advanced)", "TT_SdxlWorkflowSettings": "TT SDXL Workflow Settings", "TT_SdxlWorkflowSettingsAdvanced": "TT SDXL Workflow Settings (Advanced)", + "TT_Flux2WorkflowSettings": "TT FLUX2 Workflow Settings", + "TT_Flux2WorkflowSettingsAdvanced": "TT FLUX2 Workflow Settings (Advanced)", } diff --git a/nodes/context/context_helpers.py b/nodes/context/context_helpers.py index cb4a089..ca249bc 100644 --- a/nodes/context/context_helpers.py +++ b/nodes/context/context_helpers.py @@ -1,6 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import comfy.samplers +import comfy.samplers as S CONTEXT_FIELDS = [ # input_name, return_name, field_type @@ -13,14 +13,18 @@ CONTEXT_FIELDS = [ ("negative", "NEGATIVE", "CONDITIONING"), ("image", "IMAGE", "IMAGE"), ("mask", "MASK", "MASK"), + ("guider", "GUIDER", "GUIDER"), ("control_net", "CONTROL_NET", "CONTROL_NET"), ("workflow_config", "WORKFLOW_CONFIG", "TT_WORKFLOW_CONFIG",), ("seed", "SEED", "INT"), ("steps", "STEPS", "INT"), ("cfg", "CFG", "FLOAT"), - ("sampler_name", "SAMPLER_NAME", comfy.samplers.KSampler.SAMPLERS), - ("scheduler", "SCHEDULER", comfy.samplers.KSampler.SCHEDULERS), + ("sampler_name", "SAMPLER_NAME", S.KSampler.SAMPLERS), + ("sampler", "SAMPLER", "SAMPLER"), + ("scheduler", "SCHEDULER", S.KSampler.SCHEDULERS), ("guidance", "GUIDANCE", "FLOAT"), + ("prompt", "PROMPT", "STRING"), + ("lora_triggers", "LORA_TRIGGERS", "STRING"), ("clip_l_positive", "CLIP_L_POSITIVE", "STRING"), ("clip_g_positive", "CLIP_L_POSITIVE", "STRING"), ("t5xxl_positive", "T5XXL_POSITIVE", "STRING"), @@ -93,6 +97,28 @@ CONTEXTS = { "height", ) }, + "big_flux2": { + "optional": ( + "context", + "workflow_config", + "model", + "clip", + "vae", + "latent", + "image", + "seed", + "steps", + "cfg", + "sampler_name", + "sampler", + "scheduler", + "guidance", + "prompt", + "lora_triggers", + "width", + "height", + ) + }, "big_sdxl": { "optional": ( "context", @@ -130,7 +156,7 @@ CONTEXTS = { "context", ) }, - "set_image":{ + "set_image": { "required": ( "context", "image", @@ -139,7 +165,7 @@ CONTEXTS = { "context", ) }, - "set_latent":{ + "set_latent": { "required": ( "context", "latent", @@ -148,6 +174,15 @@ CONTEXTS = { "context", ) }, + "set_guider": { + "required": ( + "context", + "guider", + ), + "return": ( + "context", + ) + }, } FORCE_INPUT_TYPES = { @@ -162,6 +197,7 @@ WORKFOW_SETTINGS_KEYS = [ "steps", "cfg", "sampler_name", + "sampler", "scheduler", "pos_a_score", "clip_l_positive", diff --git a/nodes/context/context_set_guider.py b/nodes/context/context_set_guider.py new file mode 100644 index 0000000..1bf00a7 --- /dev/null +++ b/nodes/context/context_set_guider.py @@ -0,0 +1,18 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +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" + + @classmethod + def INPUT_TYPES(cls): + return cls.INPUT_TYPES_DICT + + def process_context(self, context, guider): + context["guider"] = guider + + return (context,) diff --git a/nodes/image/guider_image_reference.py b/nodes/image/guider_image_reference.py new file mode 100644 index 0000000..ff5cb5c --- /dev/null +++ b/nodes/image/guider_image_reference.py @@ -0,0 +1,58 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import os + +import folder_paths as FP +from node_helpers import conditioning_set_values + +from ..image.image_helpers import load_image, resize_image_to_megapixels +from ...lib.common import CommonTypes + +UPSCALE_METHODS = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] +TILE_SIZE, OVERLAP = 512, 64 + + +class TT_GuiderImageReference(): + @classmethod + def INPUT_TYPES(cls): + input_dir = FP.get_input_directory() + files = FP.filter_files_content_types( + [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))], ["image"] + ) + + return { + "required": { + "vae": ("VAE",), + "guider": ("GUIDER",), + "megapixels": (CommonTypes.MEGAPIXELS,), + "upscale_method": (UPSCALE_METHODS, {"default": "bicubic", "advanced": True}), + "dimension_step": ("INT", {"default": 1, "min": 1, "max": 10000, "advanced": True}), + "image": (sorted(files), {"image_upload": True}), + }, + } + + RETURN_TYPES = ("VAE", "GUIDER",) + RETURN_NAMES = ("VAE", "GUIDER",) + FUNCTION = "update_latent" + CATEGORY = "TenserTensor/Image" + + def update_latent(self, vae, guider, megapixels, upscale_method, dimension_step, image): + timage, _ = load_image(image) + scaled = resize_image_to_megapixels(timage, upscale_method, megapixels, dimension_step) + + samples = vae.encode_tiled( + scaled, + tile_x=TILE_SIZE, + tile_y=TILE_SIZE, + overlap=OVERLAP + ) + + conditioning = guider.get_conds() + + if conditioning is not None: + conditioning = conditioning_set_values(conditioning, {"reference_latents": [samples]}, append=True) + guider.set_conds(conditioning) + else: + raise ValueError("ERROR: Guider has no conditioning") + + return (vae, guider,) diff --git a/nodes/image/image_helpers.py b/nodes/image/image_helpers.py index b63aad5..16bc8fe 100644 --- a/nodes/image/image_helpers.py +++ b/nodes/image/image_helpers.py @@ -1,14 +1,20 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) import fnmatch +import math import os import random import string as S from datetime import datetime import folder_paths as FP +import numpy import numpy as NP -from PIL import Image +import torch +from PIL import Image, ImageOps, ImageSequence +from comfy import model_management as MM +from comfy import utils as U +from node_helpers import pillow FORMAT_EXT = { "PNG": ".png", @@ -16,9 +22,11 @@ FORMAT_EXT = { "WEBP": ".webp", } + class InvalidSavePathError(Exception): pass + def _build_filename(save_path, filename_mask): count = sum(1 for f in os.scandir(save_path) if fnmatch.fnmatch(f.name, filename_mask)) + 1 @@ -112,3 +120,78 @@ def preview(image, filename_prefix): filename_prefix=prefix, output_path=temp_dir, ) + + +def load_image(image_filename, device=None): + tdevice = MM.get_torch_device() if device == None else torch.device("cpu") + + image_path = FP.get_annotated_filepath(image_filename) + pil_image = pillow(Image.open, image_path) + + images, masks = [], [] + width, height = None, None + + for frame in ImageSequence.Iterator(pil_image): + pil_frame = pillow(ImageOps.exif_transpose, frame) + + if pil_frame.mode == "I": + pil_frame = pil_frame.point(lambda i: i * (1 / 255)) + + if len(images) == 0: + width, height = pil_frame.size + + if pil_frame.size != (width, height,): + continue + + alpha = numpy.zeros((height, width,), dtype=numpy.float32) + if 'A' in pil_frame.getbands(): + alpha = numpy.array(pil_frame.getchannel('A'), dtype=numpy.float32) / 255.0 + elif pil_frame.mode == 'P' and 'transparency' in pil_frame.info: + alpha = numpy.array(pil_frame.convert('RGBA').getchannel('A'), dtype=numpy.float32) / 255.0 + + tmask = torch.from_numpy(1.0 - alpha).to(tdevice) + + rgb_frame = pil_frame.convert("RGB") + np_image = numpy.array(rgb_frame, dtype=numpy.float32) / 255.0 + timage = torch.from_numpy(np_image)[None].to(tdevice) + + images.append(timage) + masks.append(tmask) + + if pil_image.format == "MPO": + break + + if len(images) == 0: + raise ValueError(f"ERROR: No frames found in {image_path}") + + oimage = torch.cat(images, dim=0) if len(images) > 1 else images[0] + omask = torch.cat(masks, dim=0) if len(masks) > 1 else masks[0] + + return oimage, omask + + +def _align_to_step(size, scale_factor, dimension_step): + return round(size * scale_factor / dimension_step) * dimension_step + + +# crop = "center" / "disabled" +def resize_image(timage, to_width, to_height, upscale_method, dimension_step=1, crop="disabled"): + samples = timage.movedim(-1, 1) + orig_height, orig_width = samples.shape[2:] + scale_factor = max(to_width / orig_width, to_height / orig_height) + to_width = _align_to_step(orig_width, scale_factor, dimension_step) + to_height = _align_to_step(orig_height, scale_factor, dimension_step) + + return U.common_upscale(samples, int(to_width), int(to_height), upscale_method, crop).movedim(1, -1) + + +def resize_image_to_megapixels(timage, upscale_method, megapixels, dimension_step=1): + mp_value = float(megapixels.split()[0]) + total_pixels = int(mp_value * 1_000_000) + samples = timage.movedim(-1, 1) + orig_height, orig_width = samples.shape[2:] + scale_factor = math.sqrt(total_pixels / (orig_width * orig_height)) + to_width = _align_to_step(orig_width, scale_factor, dimension_step) + to_height = _align_to_step(orig_height, scale_factor, dimension_step) + + return U.common_upscale(samples, int(to_width), int(to_height), upscale_method, "disabled").movedim(1, -1) diff --git a/nodes/image/image_preview_save.py b/nodes/image/image_preview_save.py index efc2f8a..87102e8 100644 --- a/nodes/image/image_preview_save.py +++ b/nodes/image/image_preview_save.py @@ -14,11 +14,11 @@ class TT_ImagePreviewSave(): "image": ("IMAGE",), "save_image": ("BOOLEAN", {"default": True, "label_on": "Enabled", "label_off": "Disabled"}), "filename_prefix": ("STRING", {"default": "tenser-tensor"}), - "filename_format": (FILENAME_FORMATS, {"advanced": True, "default": "name-###"}), + "filename_format": (FILENAME_FORMATS, {"default": "name-###", "advanced": True}), "subfolder": ("STRING", {"default": "", "advanced": True}), - "image_format": (IMAGE_FORMATS, {"advanced": True, "default": "PNG"}), - "image_quality": ("INT", {"default": 100, "min": 0, "max": 100}), - "compression_level": ("INT", {"default": 9, "min": 0, "max": 9}), + "image_format": (IMAGE_FORMATS, {"default": "PNG", "advanced": True}), + "image_quality": ("INT", {"default": 100, "min": 0, "max": 100, "advanced": True}), + "compression_level": ("INT", {"default": 9, "min": 0, "max": 9, "advanced": True}), } } diff --git a/nodes/image/image_preview_upscale_save.py b/nodes/image/image_preview_upscale_save.py new file mode 100644 index 0000000..fd74d7e --- /dev/null +++ b/nodes/image/image_preview_upscale_save.py @@ -0,0 +1,71 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import folder_paths as FP + +from .image_helpers import store, preview +from ..postproduction.postproduction_helpers import upscale_image as upscale + + +class TT_ImagePreviewUpscaleSave(): + @classmethod + def INPUT_TYPES(cls): + FILENAME_FORMATS = ["name-###", "date-name-###", "name-datetime"] + IMAGE_FORMATS = ["PNG", "JPEG", "WEBP"] + DEVICES = ["default", "cpu"] + UPSCALE_MODELS = FP.get_filename_list("upscale_models") + + return { + "required": { + "image": ("IMAGE",), + "save_image": ("BOOLEAN", {"default": True, "label_on": "Enabled", "label_off": "Disabled"}), + "filename_prefix": ("STRING", {"default": "tenser-tensor"}), + "filename_format": (FILENAME_FORMATS, {"advanced": True, "default": "name-###"}), + "subfolder": ("STRING", {"default": "", "advanced": True}), + "image_format": (IMAGE_FORMATS, {"advanced": True, "default": "PNG"}), + "image_quality": ("INT", {"default": 100, "min": 0, "max": 100, "advanced": True}), + "compression_level": ("INT", {"default": 9, "min": 0, "max": 9, "advanced": True}), + "upscale_image": ("BOOLEAN", {"default": True, "label_on": "Enabled", "label_off": "Disabled"}), + "upscaler_device": (DEVICES, {"advanced": True}), + "upscale_model_name": (UPSCALE_MODELS, {"advanced": True}), + "tile": ("INT", {"default": 512, "min": 256, "max": 4096, "step": 64, "advanced": True}), + "overlap": ("INT", {"default": 64, "min": 0, "max": 256, "step": 8, "advanced": True}), + } + } + + RETURN_TYPES = () + RETURN_NAMES = () + OUTPUT_NODE = True + FUNCTION = "preview_upscale_save_image" + CATEGORY = "TenserTensor/Image" + + def preview_upscale_save_image(self, **kwargs): + image = kwargs.get("image") + timage = image.clone() + + upscale_image = kwargs.get("upscale_image") + if upscale_image: + timage = upscale( + timage=timage, + device=kwargs.get("upscaler_device"), + upscale_model_name=kwargs.get("upscale_model_name"), + tile=kwargs.get("tile"), + overlap=kwargs.get("overlap") + ) + + save_image = kwargs.get("save_image") + if save_image: + timage = store( + image=timage, + filename_prefix=kwargs.get("filename_prefix"), + subfolder=kwargs.get("subfolder"), + filename_format=kwargs.get("filename_format"), + image_format=kwargs.get("image_format"), + image_quality=kwargs.get("image_quality"), + compress_level=kwargs.get("compression_level"), + ) + else: + timage = preview( + image=timage, + filename_prefix=kwargs.get("filename_prefix"), + ) + + return {"ui": {"images": timage}} diff --git a/nodes/latent/latent_factory.py b/nodes/latent/latent_factory.py index 3a0e9e8..d7f84bf 100644 --- a/nodes/latent/latent_factory.py +++ b/nodes/latent/latent_factory.py @@ -1,40 +1,12 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import torch +from .latent_helpers import create_empty_latent +from ...lib.common import CommonTypes -ASPECT_RATIOS = [ - "1:1", - "4:3", - "3:2", - "16:9", - "21:9" -] - -MEGAPIXELS = [ - "0.25 MP", - "0.5 MP", - "1 MP", - "2 MP", - "4 MP", - "8 MP" -] - -ORIENTATIONS = [ - "landscape", - "portrait" -] - -MODEL_TYPES = [ - "FLUX1.D", - "FLUX2.D", - "SDXL" -] - -CLIP_MULTIPLIERS = [ - "1x", - "2x", - "4x" -] +ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "21:9"] +ORIENTATIONS = ["landscape", "portrait"] +MODEL_TYPES = ["FLUX1.D", "FLUX2.D", "SDXL"] +CLIP_MULTIPLIERS = ["1x", "2x", "4x"] class TT_LatentFactory: @@ -42,75 +14,21 @@ class TT_LatentFactory: def INPUT_TYPES(cls): return { "required": { - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff},), + "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff},), "aspect_ratio": (ASPECT_RATIOS,), - "megapixels": (MEGAPIXELS,), + "megapixels": (CommonTypes.MEGAPIXELS,), "orientation": (ORIENTATIONS,), "model_type": (MODEL_TYPES,), - "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), - "clip_multiplier": (CLIP_MULTIPLIERS,) + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64, "advanced": True},), + "clip_multiplier": (CLIP_MULTIPLIERS, {"advanced": True},) } } - RETURN_TYPES = ("LATENT", "INT", "INT", "INT", "INT", "INT") - RETURN_NAMES = ("LATENT", "SEED", "WIDTH", "HEIGHT", "TARGET_WIDTH", "TARGET_HEIGHT") + RETURN_TYPES = ("LATENT", "NOISE", "INT", "INT", CommonTypes.MEGAPIXELS, "INT", "INT", "INT", "INT") + RETURN_NAMES = ("LATENT", "RANDOM_NOISE", "SEED", "NOISE_SEED", "MEGAPIXELS", "WIDTH", "HEIGHT", "TARGET_WIDTH", "TARGET_HEIGHT") FUNCTION = "create_latent" CATEGORY = "TenserTensor/Latent" - def calculate_dimensions(self, total_pixels, ratio_w, ratio_h): - import math - - height = math.sqrt(total_pixels * ratio_h / ratio_w) - width = height * ratio_w / ratio_h - - width = int(width) - height = int(height) - - width = round(width / 64) * 64 - height = round(height / 64) * 64 - - return width, height - - def create_latent(self, seed, aspect_ratio, megapixels, orientation, model_type, batch_size, clip_multiplier): - mp_value = float(megapixels.split()[0]) - total_pixels = int(mp_value * 1_000_000) - - ratio_parts = aspect_ratio.split(':') - ratio_w = int(ratio_parts[0]) - ratio_h = int(ratio_parts[1]) - - if orientation == "portrait": - ratio_w, ratio_h = ratio_h, ratio_w - - width, height = self.calculate_dimensions(total_pixels, ratio_w, ratio_h) - - multiplier = int(clip_multiplier.replace('x', '')) - clip_width = width * multiplier - clip_height = height * multiplier - - match model_type: - case "FLUX1.D": - scale_factor = 8 - channels = 16 - case "FLUX2.D": - scale_factor = 16 - channels = 128 - case "SDXL": - scale_factor = 8 - channels = 4 - - latent_width = width // scale_factor - latent_height = height // scale_factor - - generator = torch.Generator().manual_seed(seed) - latent = torch.randn( - batch_size, - channels, - latent_height, - latent_width, - generator=generator - ) - - latent_dict = {"samples": latent} - - return (latent_dict, seed, width, height, clip_width, clip_height) + def create_latent(self, **kwargs): + return create_empty_latent(**kwargs) diff --git a/nodes/latent/latent_helpers.py b/nodes/latent/latent_helpers.py new file mode 100644 index 0000000..828aef7 --- /dev/null +++ b/nodes/latent/latent_helpers.py @@ -0,0 +1,90 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import comfy.sample as S +import torch +import math + + +class RandomNoise: + def __init__(self, seed): + self.seed = seed + + def generate_noise(self, input_latent): + latent_image = input_latent["samples"] + batch_idx = input_latent["batch_index"] if "batch_index" in input_latent else None + return S.prepare_noise(latent_image, self.seed, batch_idx) + + +LATENT_DIMENSION_STEP = 64 + + +def _calculate_dimensions(total_pixels, ratio_w, ratio_h): + height = math.sqrt(total_pixels * ratio_h / ratio_w) + width = height * ratio_w / ratio_h + width = int(width) + height = int(height) + width = round(width / LATENT_DIMENSION_STEP) * LATENT_DIMENSION_STEP + height = round(height / LATENT_DIMENSION_STEP) * LATENT_DIMENSION_STEP + + return width, height + + +def create_empty_latent(seed, noise_seed, aspect_ratio, megapixels, orientation, model_type, batch_size, clip_multiplier): + mp_value = float(megapixels.split()[0]) + total_pixels = int(mp_value * 1_000_000) + + ratio_parts = aspect_ratio.split(':') + ratio_w = int(ratio_parts[0]) + ratio_h = int(ratio_parts[1]) + + if orientation == "portrait": + ratio_w, ratio_h = ratio_h, ratio_w + + width, height = _calculate_dimensions(total_pixels, ratio_w, ratio_h) + + multiplier = int(clip_multiplier.replace('x', '')) + clip_width = width * multiplier + clip_height = height * multiplier + + match model_type: + case "FLUX1.D": + scale_factor = 8 + channels = 16 + case "FLUX2.D": + scale_factor = 16 + channels = 128 + case "SDXL": + scale_factor = 8 + channels = 4 + + latent_width = width // scale_factor + latent_height = height // scale_factor + + generator = torch.Generator().manual_seed(seed) + latent = torch.randn( + batch_size, + channels, + latent_height, + latent_width, + generator=generator + ) + + latent_dict = {"samples": latent} + noise = RandomNoise(noise_seed) + + return (latent_dict, noise, seed, noise_seed, megapixels, width, height, clip_width, clip_height,) + + +def vae_encode(latent, image, vae): + tile_width, tile_height, overlap = 512, 512, 64 + + samples = vae.encode_tiled( + image, + tile_x=tile_width, + tile_y=tile_height, + overlap=overlap + ) + + latent["samples"] = samples + + return (latent,) diff --git a/nodes/loaders/flux_models_loader_advanced.py b/nodes/loaders/flux_models_loader_advanced.py index b55adfe..3ed2c8a 100644 --- a/nodes/loaders/flux_models_loader_advanced.py +++ b/nodes/loaders/flux_models_loader_advanced.py @@ -7,6 +7,12 @@ from .loader_helpers import load_unet, patch_flux_sampling, load_flux_clip, appl class TT_FluxModelsLoaderAdvanced(): + def __init__(self): + self.loaded_lora_1 = None + self.loaded_lora_2 = None + self.loaded_lora_3 = None + self.loaded_lora_4 = None + @classmethod def INPUT_TYPES(cls): UNET_DTYPES = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"] diff --git a/nodes/loaders/gguf_loader_helpers.py b/nodes/loaders/gguf_loader_helpers.py index 0aa417b..9ac0dba 100644 --- a/nodes/loaders/gguf_loader_helpers.py +++ b/nodes/loaders/gguf_loader_helpers.py @@ -156,3 +156,15 @@ def load_clip(clip_name, type="flux2"): clip_type = getattr(SD.CLIPType, type.upper(), SD.CLIPType.FLUX2) return _load_patcher(clip_type, _load_data([clip_path])) + + +def apply_bypass_lora_for_models(loaded_lora, model, clip, lora_name, strength): + if loaded_lora == None: + lora_path = FP.get_full_path_or_raise("loras", lora_name) + lora = U.load_torch_file(lora_path, safe_load=True) + else: + lora = loaded_lora + + patched_model, patched_clip = SD.load_bypass_lora_for_models(model, clip, lora, strength, strength) + + return (patched_model, patched_clip, lora,) diff --git a/nodes/loaders/gguf_models_loader.py b/nodes/loaders/gguf_models_loader.py index 06e7eed..0342032 100644 --- a/nodes/loaders/gguf_models_loader.py +++ b/nodes/loaders/gguf_models_loader.py @@ -10,7 +10,7 @@ update_folder_names_and_paths("unet_gguf", ["diffusion_models", "unet"]) update_folder_names_and_paths("clip_gguf", ["text_encoders", "clip"]) -class TT_GgufModelsLoader(): +class TT_Flux2GgufModelsLoader(): @classmethod def INPUT_TYPES(cls): UNET_NAMES = [x for x in FP.get_filename_list("unet_gguf")] @@ -28,7 +28,7 @@ class TT_GgufModelsLoader(): RETURN_TYPES = ("MODEL", "CLIP", "VAE",) RETURN_NAMES = ("MODEL", "CLIP", "VAE",) FUNCTION = "load_models" - CATEGORY = "TenserTensor/Loaders/GGUF" + CATEGORY = "TenserTensor/Loaders/FLUX2 GGUF" VAE_DEVICE = "cpu" VAE_DTYPE = "bfloat16" diff --git a/nodes/loaders/gguf_models_loader_advanced.py b/nodes/loaders/gguf_models_loader_advanced.py index 4ba53d8..7ac47ab 100644 --- a/nodes/loaders/gguf_models_loader_advanced.py +++ b/nodes/loaders/gguf_models_loader_advanced.py @@ -3,14 +3,20 @@ import folder_paths as FP from nodes import VAELoader as VL, MAX_RESOLUTION -from .gguf_loader_helpers import update_folder_names_and_paths, load_unet, load_clip -from .loader_helpers import load_vae, patch_flux_sampling, apply_lora +from .gguf_loader_helpers import update_folder_names_and_paths, load_unet, load_clip, apply_bypass_lora_for_models +from .loader_helpers import load_vae, patch_flux_sampling update_folder_names_and_paths("unet_gguf", ["diffusion_models", "unet"]) update_folder_names_and_paths("clip_gguf", ["text_encoders", "clip"]) -class TT_GgufModelsLoaderAdvanced(): +class TT_Flux2GgufModelsLoaderAdvanced(): + def __init__(self): + self.loaded_lora_1 = None + self.loaded_lora_2 = None + self.loaded_lora_3 = None + self.loaded_lora_4 = None + @classmethod def INPUT_TYPES(cls): UNET_NAMES = [x for x in FP.get_filename_list("unet_gguf")] @@ -24,8 +30,8 @@ class TT_GgufModelsLoaderAdvanced(): return { "required": { "unet_name": (UNET_NAMES,), - "dequant_dtype": (DTYPES, {"default": "default", "advanced": True},), - "patch_dtype": (DTYPES, {"default": "default", "advanced": True},), + "dequant_dtype": (DTYPES, {"default": "bfloat16", "advanced": True},), + "patch_dtype": (DTYPES, {"default": "bfloat16", "advanced": True},), "apply_sampling": ("BOOLEAN", {"default": True, "label_on": "Enabled", "label_off": "Disabled"},), "base_sampling_shift": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 100.0, "step": 0.01, "advanced": True},), "max_sampling_shift": ("FLOAT", {"default": 1.15, "min": 0.0, "max": 100.0, "step": 0.01, "advanced": True},), @@ -49,7 +55,7 @@ class TT_GgufModelsLoaderAdvanced(): RETURN_TYPES = ("MODEL", "CLIP", "VAE",) RETURN_NAMES = ("MODEL", "CLIP", "VAE",) FUNCTION = "load_models" - CATEGORY = "TenserTensor/Loaders/GGUF" + CATEGORY = "TenserTensor/Loaders/FLUX2 GGUF" def load_models(self, **kwargs): model = load_unet( @@ -82,7 +88,7 @@ class TT_GgufModelsLoaderAdvanced(): for name, strength, attr in loras: if name != "None" and strength != 0: cached = getattr(self, attr) - model, clip, lora = apply_lora(cached, model, clip, name, strength) + model, clip, lora = apply_bypass_lora_for_models(cached, model, clip, name, strength) setattr(self, attr, lora) vae = load_vae(kwargs.get("vae_name"), kwargs.get("vae_device"), kwargs.get("vae_dtype")) diff --git a/nodes/postproduction/postproduction_helpers.py b/nodes/postproduction/postproduction_helpers.py index 7e04c3b..0ca190a 100644 --- a/nodes/postproduction/postproduction_helpers.py +++ b/nodes/postproduction/postproduction_helpers.py @@ -196,6 +196,7 @@ def solarize(image, thresholds=0.5, additions=None): return kornia.enhance.solarize(timage, thresholds, additions).movedim(1, -1) + def enhance_image( timage, brightness_factor, diff --git a/nodes/postproduction/quick_image_upscaler.py b/nodes/postproduction/quick_image_upscaler.py index 1afddc7..eea0785 100644 --- a/nodes/postproduction/quick_image_upscaler.py +++ b/nodes/postproduction/quick_image_upscaler.py @@ -26,7 +26,6 @@ class TT_QuickImageUpscaler(): FUNCTION = "upscale_image" CATEGORY = "TenserTensor/Postproduction" - def upscale_image(self, image, device, upscale_model_name, tile, overlap): timage = image.clone() diff --git a/nodes/sampling/ksampler.py b/nodes/sampling/ksampler.py index 3a48874..0b9402f 100644 --- a/nodes/sampling/ksampler.py +++ b/nodes/sampling/ksampler.py @@ -23,7 +23,7 @@ class TT_KSampler(): } RETURN_TYPES = ("LATENT",) - RETURN_NAMES = ("LATENT",) + RETURN_NAMES = ("SAMPLES",) FUNCTION = "execute_sampling" CATEGORY = "TenserTensor/Sampling" diff --git a/nodes/sampling/ksampler_advanced.py b/nodes/sampling/ksampler_advanced.py index 6321f0c..4c2e7c4 100644 --- a/nodes/sampling/ksampler_advanced.py +++ b/nodes/sampling/ksampler_advanced.py @@ -28,7 +28,7 @@ class TT_KSamplerAdvanced(): } RETURN_TYPES = ("LATENT",) - RETURN_NAMES = ("LATENT",) + RETURN_NAMES = ("SAMPLES",) FUNCTION = "execute_sampling" CATEGORY = "TenserTensor/Sampling" diff --git a/nodes/sampling/ksampler_context.py b/nodes/sampling/ksampler_context.py index 3c46fef..ae6a56d 100644 --- a/nodes/sampling/ksampler_context.py +++ b/nodes/sampling/ksampler_context.py @@ -13,7 +13,7 @@ class TT_KSamplerContext(): } RETURN_TYPES = ("TT_CONTEXT", "LATENT",) - RETURN_NAMES = ("CONTEXT", "LATENT",) + RETURN_NAMES = ("CONTEXT", "SAMPLES",) FUNCTION = "execute_sampling" CATEGORY = "TenserTensor/Sampling" diff --git a/nodes/sampling/ksampler_guided.py b/nodes/sampling/ksampler_guided.py new file mode 100644 index 0000000..9689972 --- /dev/null +++ b/nodes/sampling/ksampler_guided.py @@ -0,0 +1,25 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +from .ksampler_helpers import guided_sample_latents + + +class TT_KSamplerGuided: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "latent": ("LATENT",), + "guider": ("GUIDER",), + "sigmas": ("SIGMAS",), + "sampler": ("SAMPLER",), + "random_noise": ("NOISE",), + }, + } + + RETURN_TYPES = ("LATENT",) + RETURN_NAMES = ("SAMPLES",) + FUNCTION = "execute_sampling" + CATEGORY = "TenserTensor/Sampling" + + def execute_sampling(self, latent, guider, sigmas, sampler, random_noise): + return (guided_sample_latents(latent, guider, sigmas, sampler, random_noise),) diff --git a/nodes/sampling/ksampler_helpers.py b/nodes/sampling/ksampler_helpers.py index 6131782..af72207 100644 --- a/nodes/sampling/ksampler_helpers.py +++ b/nodes/sampling/ksampler_helpers.py @@ -1,8 +1,9 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) import comfy -import latent_preview +import latent_preview as LP import torch +from comfy import sample as S, utils as U, model_management as MM, nested_tensor as NT def sample_latents(**kwargs): @@ -26,7 +27,7 @@ def sample_latents(**kwargs): else: noise = torch.zeros(latent_img.size(), dtype=latent_img.dtype, layout=latent_img.layout, device="cpu") - callback = latent_preview.prepare_callback(model, steps) + callback = LP.prepare_callback(model, steps) args = { "model": model, @@ -55,3 +56,39 @@ def sample_latents(**kwargs): retval["samples"] = samples return retval + + +def guided_sample_latents(latent, guider, sigmas, sampler, random_noise): + model_patcher = guider.model_patcher + latent_dict = latent.copy() + latent_tensor = S.fix_empty_latent_channels(model_patcher, latent_dict["samples"], latent.get("downscale_ratio_spacial", None)) + latent_dict["samples"] = latent_tensor + + noise_mask = latent.get("noise_mask", None) + + interim_buffer = {} + callback = LP.prepare_callback(model_patcher, sigmas.shape[-1] - 1, interim_buffer) + disable_pbar = not U.PROGRESS_BAR_ENABLED + + samples = guider.sample( + random_noise.generate_noise(latent), + latent_tensor, + sampler, + sigmas, + denoise_mask=noise_mask, + callback=callback, + disable_pbar=disable_pbar, + seed=random_noise.seed + ).to(MM.intermediate_device()) + + retval = latent_dict.copy() + retval.pop("downscale_ratio_spacial", None) + retval["samples"] = samples + +# if "x0" in interim_buffer: +# clean_latent = guider.model_patcher.model.process_latent_out(interim_buffer["x0"].cpu()) +# if samples.is_nested: +# clean_latent = NT.NestedTensor(U.unpack_latents(clean_latent, [t.shape for t in samples.unbind()])) +# retval["samples"] = clean_latent + + return retval diff --git a/nodes/sampling/ksampler_two_stage.py b/nodes/sampling/ksampler_two_stage.py index 43ebf32..97b8a78 100644 --- a/nodes/sampling/ksampler_two_stage.py +++ b/nodes/sampling/ksampler_two_stage.py @@ -28,7 +28,7 @@ class TT_KSamplerTwoStage(): } RETURN_TYPES = ("LATENT",) - RETURN_NAMES = ("LATENT",) + RETURN_NAMES = ("SAMPLES",) FUNCTION = "execute_sampling" CATEGORY = "TenserTensor/Sampling" diff --git a/nodes/text_encoders/clip_text_encode_flux.py b/nodes/text_encoders/clip_text_encode_flux.py index 8f206ef..23b3f1c 100644 --- a/nodes/text_encoders/clip_text_encode_flux.py +++ b/nodes/text_encoders/clip_text_encode_flux.py @@ -9,22 +9,10 @@ class TT_ClipTextEncodeFlux(): return { "required": { "clip": ("CLIP",), - "clip_l_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True} - ), - "t5xxl_positive": ( - "STRING", - {"multiline": True, "placeholder": "T5XXL Positive", "dynamicPrompts": True} - ), - "clip_l_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), - "t5xxl_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), + "clip_l_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True}), + "t5xxl_positive": ("STRING", {"multiline": True, "placeholder": "T5XXL Positive", "dynamicPrompts": True}), + "clip_l_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), + "t5xxl_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) } } @@ -34,5 +22,5 @@ class TT_ClipTextEncodeFlux(): FUNCTION = "execute" CATEGORY = "TenserTensor/Text Encoders/FLUX" - def execute(self, clip, clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative, guidance): - return encode_prompts_flux(clip, clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative, guidance) + def execute(self, **kwargs): + return encode_prompts_flux(**kwargs) diff --git a/nodes/text_encoders/clip_text_encode_flux2.py b/nodes/text_encoders/clip_text_encode_flux2.py new file mode 100644 index 0000000..1d05d31 --- /dev/null +++ b/nodes/text_encoders/clip_text_encode_flux2.py @@ -0,0 +1,25 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +from .encoder_helpers import encode_prompts_flux2 + + +class TT_ClipTextEncodeFlux2(): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "prompt": ("STRING", {"multiline": True, "placeholder": "Prompt", "dynamicPrompts": True}), + "lora_triggers": ("STRING", {"multiline": True, "placeholder": "LoRA Triggers", "dynamicPrompts": True}), + "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) + } + } + + RETURN_TYPES = ("GUIDER",) + RETURN_NAMES = ("GUIDER",) + FUNCTION = "execute" + CATEGORY = "TenserTensor/Text Encoders/FLUX2" + + def execute(self, model, clip, prompt, lora_triggers, guidance): + return encode_prompts_flux2(model, clip, prompt, lora_triggers, guidance) diff --git a/nodes/text_encoders/clip_text_encode_flux2_context.py b/nodes/text_encoders/clip_text_encode_flux2_context.py new file mode 100644 index 0000000..16d3005 --- /dev/null +++ b/nodes/text_encoders/clip_text_encode_flux2_context.py @@ -0,0 +1,41 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +from .encoder_helpers import encode_prompts_flux2 + + +class TT_ClipTextEncodeFlux2Context(): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "context": ("TT_CONTEXT",) + } + } + + RETURN_TYPES = ("TT_CONTEXT", "GUIDER",) + RETURN_NAMES = ("CONTEXT", "GUIDER",) + FUNCTION = "execute" + CATEGORY = "TenserTensor/Text Encoders/FLUX2" + + def execute(self, context): + model = context.get("model", None) + clip = context.get("clip", None) + config = context.get("workflow_config", None) + + if model is None: + raise ValueError("Model is required for text encoder") + if clip is None: + raise ValueError("CLIP is required for text encoder") + if config is None: + raise ValueError("Workflow Config is required for text encoder") + + guider = encode_prompts_flux2( + model, + clip, + config.get("prompt", None), + config.get("lora_triggers", None), + config.get("guidance", None), + ) + context["guider"] = guider + + return (context, guider,) diff --git a/nodes/text_encoders/clip_text_encode_sdxl.py b/nodes/text_encoders/clip_text_encode_sdxl.py index aa9eba3..6b4ac58 100644 --- a/nodes/text_encoders/clip_text_encode_sdxl.py +++ b/nodes/text_encoders/clip_text_encode_sdxl.py @@ -1,6 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import nodes +from nodes import MAX_RESOLUTION from .encoder_helpers import encode_prompts_sdxl @@ -10,28 +10,16 @@ class TT_ClipTextEncodeSdxl(): return { "required": { "clip": ("CLIP",), - "clip_l_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True} - ), - "clip_g_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_G Positive", "dynamicPrompts": True} - ), - "clip_l_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), - "clip_g_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_G Negative", "dynamicPrompts": True} - ), + "clip_l_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True}), + "clip_g_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_G Positive", "dynamicPrompts": True}), + "clip_l_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), + "clip_g_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_G Negative", "dynamicPrompts": True}), "ascore_positive": ("FLOAT", {"default": 9.0, "min": 0.0, "max": 1000.0, "step": 0.1}), "ascore_negative": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.1}), - "width": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "height": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "target_width": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "target_height": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), + "width": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "height": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "target_width": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "target_height": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), } } @@ -40,7 +28,5 @@ class TT_ClipTextEncodeSdxl(): FUNCTION = "execute" CATEGORY = "TenserTensor/Text Encoders/SDXL" - def execute(self, clip, clip_l_positive, clip_g_positive, clip_l_negative, clip_g_negative, - ascore_positive, ascore_negative, width, height, target_width, target_height): - return encode_prompts_sdxl(clip, clip_l_positive, clip_g_positive, clip_l_negative, clip_g_negative, - ascore_positive, ascore_negative, width, height, target_width, target_height) + def execute(self, **kwargs): + return encode_prompts_sdxl(**kwargs) diff --git a/nodes/text_encoders/encoder_helpers.py b/nodes/text_encoders/encoder_helpers.py index a1ec360..70a9cc2 100644 --- a/nodes/text_encoders/encoder_helpers.py +++ b/nodes/text_encoders/encoder_helpers.py @@ -1,25 +1,20 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -def encode_prompts_flux(clip, clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative, guidance): - positive_tokens = clip.tokenize(clip_l_positive) - positive_tokens["t5xxl"] = clip.tokenize(t5xxl_positive)["t5xxl"] - negative_tokens = clip.tokenize(clip_l_negative) - negative_tokens["t5xxl"] = clip.tokenize(t5xxl_negative)["t5xxl"] +import comfy.samplers as S - return ( - clip.encode_from_tokens_scheduled( - positive_tokens, - add_dict={"guidance": guidance, } - ), - clip.encode_from_tokens_scheduled( - negative_tokens, - add_dict={"guidance": guidance, } - ) - ) +from ...lib.common import BasicGuider + + +class Guider_Basic(S.CFGGuider): + def set_conds(self, positive): + self.inner_set_conds({"positive": positive}) def encode_prompts_sdxl(clip, clip_l_positive, clip_g_positive, clip_l_negative, clip_g_negative, ascore_positive, ascore_negative, width, height, target_width, target_height): + if clip is None: + raise ValueError("ERROR: CLIP is required for text encoder") + positive_tokens = clip.tokenize(clip_g_positive) positive_tokens["l"] = clip.tokenize(clip_l_positive)["l"] negative_tokens = clip.tokenize(clip_g_negative) @@ -47,3 +42,31 @@ def encode_prompts_sdxl(clip, clip_l_positive, clip_g_positive, clip_l_negative, } ) ) + + +def encode_prompts_flux(clip, clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative, guidance): + if clip is None: + raise ValueError("ERROR: CLIP is required for text encoder") + + positive_tokens = clip.tokenize(clip_l_positive) + positive_tokens["t5xxl"] = clip.tokenize(t5xxl_positive)["t5xxl"] + negative_tokens = clip.tokenize(clip_l_negative) + negative_tokens["t5xxl"] = clip.tokenize(t5xxl_negative)["t5xxl"] + + return ( + clip.encode_from_tokens_scheduled(positive_tokens, add_dict={"guidance": guidance, }), + clip.encode_from_tokens_scheduled(negative_tokens, add_dict={"guidance": guidance, }) + ) + + +def encode_prompts_flux2(model, clip, prompt, lora_triggers, guidance): + if clip is None: + raise ValueError("ERROR: CLIP is required for text encoder") + + full_prompt = f"{lora_triggers}, {prompt}" if lora_triggers.strip() else prompt + tokens = clip.tokenize(full_prompt) + conditioning = clip.encode_from_tokens_scheduled(tokens, add_dict={"guidance": guidance, }) + guider = BasicGuider(model) + guider.set_conds(conditioning) + + return (guider,) diff --git a/nodes/vae/vae_decode_context.py b/nodes/vae/vae_decode_context.py index 9cbca26..75103c9 100644 --- a/nodes/vae/vae_decode_context.py +++ b/nodes/vae/vae_decode_context.py @@ -1,4 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +from .vae_helpers import vae_decode + class TT_VaeDecodeContext: @classmethod @@ -19,19 +21,11 @@ class TT_VaeDecodeContext: latent = context["latent"] if vae is None: - raise ValueError("VAE is required for decode") + raise ValueError("ERROR: VAE is required for decode") if latent is None: - raise ValueError("Latent image is required for decode") - - samples = latent["samples"] - if samples.is_nested: - samples = samples.unbind()[0] - - images = vae.decode(samples) - - if len(images.shape) == 5: - images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + raise ValueError("ERROR: Latent image is required for decode") + images = vae_decode(latent, vae) context["image"] = images return (context, images,) diff --git a/nodes/vae/vae_decode_tiled.py b/nodes/vae/vae_decode_tiled.py index 9eb4e0d..5060224 100644 --- a/nodes/vae/vae_decode_tiled.py +++ b/nodes/vae/vae_decode_tiled.py @@ -1,6 +1,7 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import torch + +from .vae_helpers import vae_decode class TT_VaeDecodeTiled: @@ -23,23 +24,4 @@ class TT_VaeDecodeTiled: CATEGORY = "TenserTensor/VAE" def vae_decode(self, latent, vae, tile_width, tile_height, overlap, circular): - samples = latent["samples"] - if samples.is_nested: - samples = samples.unbind()[0] - - if circular == True: - for layer in [layer for layer in vae.first_stage_model.modules() if isinstance(layer, torch.nn.Conv2d)]: - layer.padding_mode = "circular" - - compression = vae.spacial_compression_decode() - images = vae.decode_tiled( - samples, - tile_x=tile_width // compression, - tile_y=tile_height // compression, - overlap=overlap // compression - ) - - if len(images.shape) == 5: - images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) - - return (images,) + return (vae_decode(latent, vae, tile_width, tile_height, overlap, circular),) diff --git a/nodes/vae/vae_encode_context.py b/nodes/vae/vae_encode_context.py index 7bec44e..b692a3b 100644 --- a/nodes/vae/vae_encode_context.py +++ b/nodes/vae/vae_encode_context.py @@ -1,5 +1,8 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +from .vae_helpers import vae_encode + + class TT_VaeEncodeContext: @classmethod def INPUT_TYPES(cls): @@ -23,8 +26,7 @@ class TT_VaeEncodeContext: if image is None: raise ValueError("Pixel image is required for encode") - samples = vae.encode(image) - latent_dict = {"samples": samples} - context["latent"] = latent_dict + samples = vae_encode(image, vae) + context["latent"] = samples - return (context, latent_dict,) + return (context, samples,) diff --git a/nodes/vae/vae_encode_tiled.py b/nodes/vae/vae_encode_tiled.py index a568239..1ea73e4 100644 --- a/nodes/vae/vae_encode_tiled.py +++ b/nodes/vae/vae_encode_tiled.py @@ -1,5 +1,8 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +from .vae_helpers import vae_encode + + class TT_VaeEncodeTiled: @classmethod def INPUT_TYPES(cls): @@ -19,12 +22,4 @@ class TT_VaeEncodeTiled: CATEGORY = "TenserTensor/VAE" def vae_encode(self, image, vae, tile_width, tile_height, overlap): - samples = vae.encode_tiled( - image, - tile_x=tile_width, - tile_y=tile_height, - overlap=overlap - ) - latent_dict = {"samples": samples} - - return (latent_dict,) + return (vae_encode(image, vae, tile_width, tile_height, overlap),) diff --git a/nodes/vae/vae_helpers.py b/nodes/vae/vae_helpers.py index 99e9c89..7c85049 100644 --- a/nodes/vae/vae_helpers.py +++ b/nodes/vae/vae_helpers.py @@ -1,2 +1,38 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import torch + + +def vae_encode(image, vae, tile_width=512, tile_height=512, overlap=64): + samples = vae.encode_tiled( + image, + tile_x=tile_width, + tile_y=tile_height, + overlap=overlap + ) + latent_dict = {"samples": samples} + + return latent_dict + + +def vae_decode(latent, vae, tile_width=512, tile_height=512, overlap=64, circular=False): + samples = latent["samples"] + if samples.is_nested: + samples = samples.unbind()[0] + + if circular == True: + for layer in [layer for layer in vae.first_stage_model.modules() if isinstance(layer, torch.nn.Conv2d)]: + layer.padding_mode = "circular" + + compression = vae.spacial_compression_decode() + images = vae.decode_tiled( + samples, + tile_x=tile_width // compression, + tile_y=tile_height // compression, + overlap=overlap // compression + ) + + if len(images.shape) == 5: + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + + return images diff --git a/nodes/workflow/flux2_workflow_settings.py b/nodes/workflow/flux2_workflow_settings.py new file mode 100644 index 0000000..897f6df --- /dev/null +++ b/nodes/workflow/flux2_workflow_settings.py @@ -0,0 +1,67 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import comfy.samplers as S + +from nodes import MAX_RESOLUTION +from .workflow_settings_helpers import get_schedule + + +class TT_Flux2WorkflowSettings: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "sampler_name": (S.KSampler.SAMPLERS,), + "width": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) + } + } + + RETURN_TYPES = ( + "TT_WORKFLOW_CONFIG", + "INT", + "INT", + "FLOAT", + "SAMPLER", + S.KSampler.SAMPLERS, + "SIGMAS", + "INT", + "INT", + "FLOAT" + ) + RETURN_NAMES = ( + "WORKFLOW_CONFIG", + "SEED", + "STEPS", + "CFG", + "SAMPLER", + "SAMPLER_NAME", + "SCHEDULER", + "WIDTH", + "HEIGHT", + "GUIDANCE" + ) + FUNCTION = "get_settings" + CATEGORY = "TenserTensor/Workflow/FLUX2" + + def get_settings(self, seed, steps, cfg, sampler_name, width, height, guidance): + scheduler = get_schedule(steps, width, height) + sampler = S.sampler_object(sampler_name) + + workflow_config = { + "seed": seed, + "steps": steps, + "cfg": cfg, + "sampler": sampler, + "sampler_name": sampler_name, + "scheduler": scheduler, + "width": width, + "height": height, + "guidance": guidance + } + + return (workflow_config, seed, steps, cfg, sampler, sampler_name, scheduler, width, height, guidance) diff --git a/nodes/workflow/flux2_workflow_settings_advanced.py b/nodes/workflow/flux2_workflow_settings_advanced.py new file mode 100644 index 0000000..16eb7bc --- /dev/null +++ b/nodes/workflow/flux2_workflow_settings_advanced.py @@ -0,0 +1,75 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import comfy.samplers as S + +from nodes import MAX_RESOLUTION +from .workflow_settings_helpers import get_schedule + + +class TT_Flux2WorkflowSettingsAdvanced: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "sampler_name": (S.KSampler.SAMPLERS,), + "width": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "prompt": ("STRING", {"multiline": True, "placeholder": "Prompt", "dynamicPrompts": True}), + "lora_triggers": ("STRING", {"multiline": True, "placeholder": "LoRA Triggers", "dynamicPrompts": True}), + "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) + } + } + + RETURN_TYPES = ( + "TT_WORKFLOW_CONFIG", + "INT", + "INT", + "FLOAT", + S.KSampler.SAMPLERS, + "SAMPLER", + "SIGMAS", + "INT", + "INT", + "STRING", + "STRING", + "FLOAT" + ) + RETURN_NAMES = ( + "WORKFLOW_CONFIG", + "SEED", + "STEPS", + "CFG", + "SAMPLER_NAME", + "SAMPLER", + "SCHEDULER", + "WIDTH", + "HEIGHT", + "PROMPT", + "LORA_TRIGGERS", + "GUIDANCE" + ) + FUNCTION = "get_settings" + CATEGORY = "TenserTensor/Workflow/FLUX2" + + def get_settings(self, seed, steps, cfg, sampler_name, width, height, prompt, lora_triggers, guidance): + scheduler = get_schedule(steps, width, height) + sampler = S.sampler_object(sampler_name) + + workflow_config = { + "seed": seed, + "steps": steps, + "cfg": cfg, + "sampler": sampler, + "sampler_name": sampler_name, + "scheduler": scheduler, + "width": width, + "height": height, + "prompt": prompt, + "lora_triggers": lora_triggers, + "guidance": guidance + } + + return (workflow_config, seed, steps, cfg, sampler_name, sampler, scheduler, width, height, prompt, lora_triggers, guidance) diff --git a/nodes/workflow/flux_workfow_settings.py b/nodes/workflow/flux_workflow_settings.py similarity index 68% rename from nodes/workflow/flux_workfow_settings.py rename to nodes/workflow/flux_workflow_settings.py index 77a4f5a..43a03e8 100644 --- a/nodes/workflow/flux_workfow_settings.py +++ b/nodes/workflow/flux_workflow_settings.py @@ -1,6 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import comfy.samplers +import comfy.samplers as S class TT_FluxWorkflowSettings: @@ -11,14 +11,30 @@ class TT_FluxWorkflowSettings: "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + "sampler_name": (S.KSampler.SAMPLERS,), + "scheduler": (S.KSampler.SCHEDULERS,), "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) } } - RETURN_TYPES = ("TT_WORKFLOW_CONFIG", "INT", "INT", "FLOAT", "STRING", "STRING", "FLOAT") - RETURN_NAMES = ("WORKFLOW_CONFIG", "SEED", "STEPS", "CFG", "SAMPLER_NAME", "SCHEDULER", "GUIDANCE") + RETURN_TYPES = ( + "TT_WORKFLOW_CONFIG", + "INT", + "INT", + "FLOAT", + S.KSampler.SAMPLERS, + S.KSampler.SCHEDULERS, + "FLOAT" + ) + RETURN_NAMES = ( + "WORKFLOW_CONFIG", + "SEED", + "STEPS", + "CFG", + "SAMPLER_NAME", + "SCHEDULER", + "GUIDANCE" + ) FUNCTION = "get_settings" CATEGORY = "TenserTensor/Workflow/FLUX" diff --git a/nodes/workflow/flux_workfow_settings_advanced.py b/nodes/workflow/flux_workflow_settings_advanced.py similarity index 59% rename from nodes/workflow/flux_workfow_settings_advanced.py rename to nodes/workflow/flux_workflow_settings_advanced.py index 21136d0..752fb52 100644 --- a/nodes/workflow/flux_workfow_settings_advanced.py +++ b/nodes/workflow/flux_workflow_settings_advanced.py @@ -1,6 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import comfy.samplers +import comfy.samplers as S class TT_FluxWorkflowSettingsAdvanced: @@ -11,24 +11,12 @@ class TT_FluxWorkflowSettingsAdvanced: "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), - "clip_l_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True} - ), - "t5xxl_positive": ( - "STRING", - {"multiline": True, "placeholder": "T5XXL Positive", "dynamicPrompts": True} - ), - "clip_l_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), - "t5xxl_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), + "sampler_name": (S.KSampler.SAMPLERS,), + "scheduler": (S.KSampler.SCHEDULERS,), + "clip_l_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True}), + "t5xxl_positive": ("STRING", {"multiline": True, "placeholder": "T5XXL Positive", "dynamicPrompts": True}), + "clip_l_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), + "t5xxl_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), "guidance": ("FLOAT", {"default": 3.5, "min": 1.0, "max": 10.0, "step": 0.1}) } } @@ -38,8 +26,8 @@ class TT_FluxWorkflowSettingsAdvanced: "INT", "INT", "FLOAT", - "STRING", - "STRING", + S.KSampler.SAMPLERS, + S.KSampler.SCHEDULERS, "STRING", "STRING", "STRING", @@ -62,8 +50,7 @@ class TT_FluxWorkflowSettingsAdvanced: FUNCTION = "get_settings" CATEGORY = "TenserTensor/Workflow/FLUX" - def get_settings(self, seed, steps, cfg, sampler_name, scheduler, guidance, clip_l_positive, t5xxl_positive, - clip_l_negative, t5xxl_negative): + def get_settings(self, seed, steps, cfg, sampler_name, scheduler, guidance, clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative): workflow_config = { "seed": seed, "steps": steps, @@ -77,5 +64,5 @@ class TT_FluxWorkflowSettingsAdvanced: "guidance": guidance } - return (workflow_config, seed, steps, cfg, sampler_name, scheduler, guidance, - clip_l_positive, t5xxl_positive, clip_l_negative, t5xxl_negative) + return (workflow_config, seed, steps, cfg, sampler_name, scheduler, guidance, clip_l_positive, t5xxl_positive, clip_l_negative, + t5xxl_negative) diff --git a/nodes/workflow/sdxl_workfow_settings.py b/nodes/workflow/sdxl_workflow_settings.py similarity index 82% rename from nodes/workflow/sdxl_workfow_settings.py rename to nodes/workflow/sdxl_workflow_settings.py index 2e37890..15c5c6b 100644 --- a/nodes/workflow/sdxl_workfow_settings.py +++ b/nodes/workflow/sdxl_workflow_settings.py @@ -1,6 +1,6 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import comfy.samplers +import comfy.samplers as S class TT_SdxlWorkflowSettings: @@ -11,8 +11,8 @@ class TT_SdxlWorkflowSettings: "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 30, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + "sampler_name": (S.KSampler.SAMPLERS,), + "scheduler": (S.KSampler.SCHEDULERS,), } } @@ -21,8 +21,8 @@ class TT_SdxlWorkflowSettings: "INT", "INT", "FLOAT", - comfy.samplers.KSampler.SAMPLERS, - comfy.samplers.KSampler.SCHEDULERS, + S.KSampler.SAMPLERS, + S.KSampler.SCHEDULERS, ) RETURN_NAMES = ( diff --git a/nodes/workflow/sdxl_workfow_settings_advanced.py b/nodes/workflow/sdxl_workflow_settings_advanced.py similarity index 72% rename from nodes/workflow/sdxl_workfow_settings_advanced.py rename to nodes/workflow/sdxl_workflow_settings_advanced.py index fdbc3d9..799a112 100644 --- a/nodes/workflow/sdxl_workfow_settings_advanced.py +++ b/nodes/workflow/sdxl_workflow_settings_advanced.py @@ -1,8 +1,8 @@ # (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) -import comfy.samplers +import comfy.samplers as S -import nodes +from nodes import MAX_RESOLUTION class TT_SdxlWorkflowSettingsAdvanced: @@ -13,30 +13,18 @@ class TT_SdxlWorkflowSettingsAdvanced: "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 30, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), - "clip_l_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True} - ), - "clip_g_positive": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_G Positive", "dynamicPrompts": True} - ), - "clip_l_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True} - ), - "clip_g_negative": ( - "STRING", - {"multiline": True, "placeholder": "CLIP_G Negative", "dynamicPrompts": True} - ), + "sampler_name": (S.KSampler.SAMPLERS,), + "scheduler": (S.KSampler.SCHEDULERS,), + "clip_l_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_L Positive", "dynamicPrompts": True}), + "clip_g_positive": ("STRING", {"multiline": True, "placeholder": "CLIP_G Positive", "dynamicPrompts": True}), + "clip_l_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_L Negative", "dynamicPrompts": True}), + "clip_g_negative": ("STRING", {"multiline": True, "placeholder": "CLIP_G Negative", "dynamicPrompts": True}), "ascore_positive": ("FLOAT", {"default": 9.0, "min": 0.0, "max": 1000.0, "step": 0.1}), "ascore_negative": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.1}), - "width": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "height": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "target_width": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), - "target_height": ("INT", {"default": 512, "min": 0.0, "max": nodes.MAX_RESOLUTION}), + "width": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "height": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "target_width": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), + "target_height": ("INT", {"default": 512, "min": 0.0, "max": MAX_RESOLUTION}), } } @@ -45,8 +33,8 @@ class TT_SdxlWorkflowSettingsAdvanced: "INT", "INT", "FLOAT", - "STRING", - "STRING", + S.KSampler.SAMPLERS, + S.KSampler.SCHEDULERS, "STRING", "STRING", "STRING", diff --git a/nodes/workflow/workflow_settings_helpers.py b/nodes/workflow/workflow_settings_helpers.py new file mode 100644 index 0000000..e5e3fc5 --- /dev/null +++ b/nodes/workflow/workflow_settings_helpers.py @@ -0,0 +1,36 @@ +# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0) + +import math + +import torch + + +def _compute_empirical_mu(image_seq_len, num_steps): + a1, b1 = 8.73809524e-05, 1.89833333 + a2, b2 = 0.00016927, 0.45666666 + + if image_seq_len > 4300: + mu = a2 * image_seq_len + b2 + return float(mu) + + m_200 = a2 * image_seq_len + b2 + m_10 = a1 * image_seq_len + b1 + + a = (m_200 - m_10) / 190.0 + b = m_200 - 200.0 * a + mu = a * num_steps + b + + 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(num_steps, width, height): + image_seq_len = (width * height / (16 * 16)) + mu = _compute_empirical_mu(image_seq_len, num_steps) + timesteps = torch.linspace(1, 0, num_steps + 1) + timesteps = _generalized_time_snr_shift(timesteps, mu, 1.0) + + return timesteps diff --git a/pyproject.toml b/pyproject.toml index be67656..169aed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ # pyproject.toml [project] name = "ComfyUI-TenserTensor" -description = "A collection of custom nodes for ComfyUI that streamline common workflows. The main goal is to reduce visual clutter by combining frequently used node chains into single, convenient loaders." -version = "1.3.1" +description = "A collection of custom nodes for ComfyUI that streamline common workflows. Includes all-in-one model loaders (checkpoint/unet, LoRA, CLIP, VAE), GGUF quantized model support (FLUX, FLUX2, SDXL), workflow settings nodes, latent factory (aspect ratio, megapixels, FLUX/SDXL), context nodes, CLIP text encoders, KSamplers (context, advanced, two-stage - draft/refine), VAE encoders/decoders (context, tiled), and post-production tools (brightness, contrast, LUT color grading, film grain, upscaler)." +version = "1.4.7" license = { file = "LICENSE" } dependencies = ["gguf"] diff --git a/web/docs/TT_ClipTextEncodeFlux2.md b/web/docs/TT_ClipTextEncodeFlux2.md new file mode 100644 index 0000000..149f56d --- /dev/null +++ b/web/docs/TT_ClipTextEncodeFlux2.md @@ -0,0 +1,31 @@ +## TT Clip Text Encode Flux2 *TT_ClipTextEncodeFlux2* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Text prompt encoder for FLUX2 models with LoRA trigger word support and built-in guidance configuration. + +### Inputs + +| Parameter | Type | Required | Description | +|-----------------|--------|----------|--------------------------------------------------------------| +| `model` | MODEL | Yes | Diffusion model | +| `clip` | CLIP | Yes | CLIP model for text encoding | +| `prompt` | STRING | Yes | Main generation prompt (multiline, supports dynamic prompts) | +| `lora_triggers` | STRING | Yes | LoRA trigger words (multiline, supports dynamic prompts) | +| `guidance` | FLOAT | Yes | Guidance scale for generation (1.0–10.0, default: 3.5) | + +### Outputs + +| Parameter | Type | Description | +|-----------|--------|----------------------------------------------------| +| `GUIDER` | GUIDER | Configured guider with encoded prompt and guidance | + +### Usage + +Encodes text prompts specifically for FLUX2 models. Enter your main prompt in `prompt` and any LoRA-specific trigger +words separately in `lora_triggers` — the node combines them internally. Adjust `guidance` to control how strongly +the model follows the prompt (3.5 is a good starting point for FLUX2). + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_ClipTextEncodeFlux2Context.md b/web/docs/TT_ClipTextEncodeFlux2Context.md new file mode 100644 index 0000000..c87c98e --- /dev/null +++ b/web/docs/TT_ClipTextEncodeFlux2Context.md @@ -0,0 +1,29 @@ +## TT Clip Text Encode Flux2 (Context) *TT_ClipTextEncodeFlux2Context* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Context-based text prompt encoder for FLUX2 models. Extracts model, CLIP, and prompt configuration directly from a TT_CONTEXT object. + +### Inputs + +| Parameter | Type | Required | Description | +|-----------|------------|----------|---------------------------------------------------------------------------------------| +| `context` | TT_CONTEXT | Yes | Context object containing `model`, `clip`, and `workflow_config` with prompt settings | + +### Outputs + +| Parameter | Type | Description | +|-----------|------------|----------------------------------------------------| +| `CONTEXT` | TT_CONTEXT | Updated context with `guider` written back into it | +| `GUIDER` | GUIDER | Configured guider with encoded prompt and guidance | + +### Usage + +Context-aware version of the FLUX2 text encoder. Instead of individual inputs, it reads `model`, `clip`, and +`workflow_config` (containing `prompt`, `lora_triggers`, and `guidance`) from the passed `context`. The resulting +guider is both returned as an output and stored back into the context under the `guider` key. Raises an error if +any of the required fields are missing from the context. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_ContextSetGuider.md b/web/docs/TT_ContextSetGuider.md new file mode 100644 index 0000000..feb7ca6 --- /dev/null +++ b/web/docs/TT_ContextSetGuider.md @@ -0,0 +1,27 @@ +## TT Context Set Guider *TT_ContextSetGuider* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Writes a GUIDER object into the TT_CONTEXT, making it available for downstream context-aware nodes. + +### Inputs + +| Parameter | Type | Required | Description | +|-----------|------------|----------|--------------------------------| +| `context` | TT_CONTEXT | Yes | Context object to update | +| `guider` | GUIDER | Yes | Guider to store in the context | + +### Outputs + +| Parameter | Type | Description | +|-----------|------------|-----------------------------------------------| +| `CONTEXT` | TT_CONTEXT | Updated context with `guider` written into it | + +### Usage + +Sets the `guider` field inside a TT_CONTEXT object. Use this node when you have a guider produced outside the +context pipeline and need to inject it back in for use by subsequent context-aware nodes. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_ContextSetImage.md b/web/docs/TT_ContextSetImage.md index fb6d387..26ee356 100644 --- a/web/docs/TT_ContextSetImage.md +++ b/web/docs/TT_ContextSetImage.md @@ -6,16 +6,16 @@ Adds or updates image in context object. ### Inputs -| Parameter | Type | Required | Description | -|-----------|------------|----------|--------------------------------| -| `context` | TT_CONTEXT | Yes | Context to update | -| `image` | IMAGE | Yes | Image to store in context | +| Parameter | Type | Required | Description | +|-----------|------------|----------|---------------------------| +| `context` | TT_CONTEXT | Yes | Context to update | +| `image` | IMAGE | Yes | Image to store in context | ### Outputs -| Parameter | Type | Description | -|-----------|------------|-------------------------| -| `CONTEXT` | TT_CONTEXT | Updated context object | +| Parameter | Type | Description | +|-----------|------------|------------------------| +| `CONTEXT` | TT_CONTEXT | Updated context object | ### Usage diff --git a/web/docs/TT_GgufModelsLoader.md b/web/docs/TT_Flux2GgufModelsLoader.md similarity index 56% rename from web/docs/TT_GgufModelsLoader.md rename to web/docs/TT_Flux2GgufModelsLoader.md index d761790..e9857cf 100644 --- a/web/docs/TT_GgufModelsLoader.md +++ b/web/docs/TT_Flux2GgufModelsLoader.md @@ -1,4 +1,4 @@ -## TT GGUF Models Loader *TT_GgufModelsLoader* +## TT FLUX2 GGUF Models Loader *TT_GgufModelsLoader* *This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* @@ -14,15 +14,16 @@ Load GGUF-quantized diffusion model (UNet), CLIP text encoder, and VAE in a sing ### Outputs -| Parameter | Type | Description | -|-----------|-------|-------------------------| -| `MODEL` | MODEL | Loaded diffusion model | -| `CLIP` | CLIP | Loaded text encoder | -| `VAE` | VAE | Loaded VAE decoder | +| Parameter | Type | Description | +|-----------|-------|------------------------| +| `MODEL` | MODEL | Loaded diffusion model | +| `CLIP` | CLIP | Loaded text encoder | +| `VAE` | VAE | Loaded VAE decoder | ### Usage -All-in-one loader for GGUF-quantized models. Place your `.gguf` diffusion model files in the `diffusion_models` or `unet` directory, CLIP files in `text_encoders` or `clip` directory. VAE can be any standard ComfyUI-compatible VAE file. +All-in-one loader for GGUF-quantized FLUX2 models. Place your `.gguf` diffusion model files in the `diffusion_models` or `unet` directory, CLIP files in +`text_encoders` or `clip` directory. VAE can be any standard ComfyUI-compatible VAE file. --- diff --git a/web/docs/TT_GgufModelsLoaderAdvanced.md b/web/docs/TT_Flux2GgufModelsLoaderAdvanced.md similarity index 54% rename from web/docs/TT_GgufModelsLoaderAdvanced.md rename to web/docs/TT_Flux2GgufModelsLoaderAdvanced.md index b12900e..f8304f6 100644 --- a/web/docs/TT_GgufModelsLoaderAdvanced.md +++ b/web/docs/TT_Flux2GgufModelsLoaderAdvanced.md @@ -1,4 +1,4 @@ -## TT GGUF Models Loader (Advanced) *TT_GgufModelsLoaderAdvanced* +## TT FLUX2 GGUF Models Loader (Advanced) *TT_GgufModelsLoaderAdvanced* *This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* @@ -6,22 +6,22 @@ All-in-one loader for GGUF-quantized models with extended control over quantizat ### Inputs -| Parameter | Type | Required | Advanced | Description | -|-----------------------|---------|----------|----------|----------------------------------------------------------| -| `unet_name` | STRING | Yes | | GGUF UNet/diffusion model filename | +| Parameter | Type | Required | Advanced | Description | +|-----------------------|---------|----------|----------|------------------------------------------------------------------------| +| `unet_name` | STRING | Yes | | GGUF UNet/diffusion model filename | | `dequant_dtype` | STRING | Yes | Yes | Data type for dequantization (default/target/float32/float16/bfloat16) | -| `patch_dtype` | STRING | Yes | Yes | Data type for weight patching | -| `apply_sampling` | BOOLEAN | Yes | | Enable/disable flux sampling shift patch | -| `base_sampling_shift` | FLOAT | Yes | Yes | Base shift value for flux sampling (default: 0.5) | -| `max_sampling_shift` | FLOAT | Yes | Yes | Maximum shift value for flux sampling (default: 1.15) | -| `sampling_width` | INT | Yes | Yes | Reference width for sampling shift calculation | -| `sampling_height` | INT | Yes | Yes | Reference height for sampling shift calculation | -| `lora_name_1..4` | STRING | Yes | Yes | LoRA filename (None to skip) | -| `strength_1..4` | FLOAT | Yes | Yes | LoRA blend strength (-10.0 to 10.0, default: 1.0) | -| `clip_name` | STRING | Yes | | GGUF CLIP text encoder filename | -| `vae_name` | STRING | Yes | | VAE model filename | -| `vae_device` | STRING | Yes | Yes | Device for VAE (default/cpu) | -| `vae_dtype` | STRING | Yes | Yes | Data type for VAE (bfloat16/float16/float32) | +| `patch_dtype` | STRING | Yes | Yes | Data type for weight patching | +| `apply_sampling` | BOOLEAN | Yes | | Enable/disable flux sampling shift patch | +| `base_sampling_shift` | FLOAT | Yes | Yes | Base shift value for flux sampling (default: 0.5) | +| `max_sampling_shift` | FLOAT | Yes | Yes | Maximum shift value for flux sampling (default: 1.15) | +| `sampling_width` | INT | Yes | Yes | Reference width for sampling shift calculation | +| `sampling_height` | INT | Yes | Yes | Reference height for sampling shift calculation | +| `lora_name_1..4` | STRING | Yes | Yes | LoRA filename (None to skip) | +| `strength_1..4` | FLOAT | Yes | Yes | LoRA blend strength (-10.0 to 10.0, default: 1.0) | +| `clip_name` | STRING | Yes | | GGUF CLIP text encoder filename | +| `vae_name` | STRING | Yes | | VAE model filename | +| `vae_device` | STRING | Yes | Yes | Device for VAE (default/cpu) | +| `vae_dtype` | STRING | Yes | Yes | Data type for VAE (bfloat16/float16/float32) | ### Outputs @@ -33,7 +33,9 @@ All-in-one loader for GGUF-quantized models with extended control over quantizat ### Usage -Extended version of GGUF Models Loader. Supports up to 4 LoRAs loaded simultaneously. Enable `apply_sampling` to apply flux sampling shift patch — useful for improving quality at non-standard resolutions. Advanced parameters are hidden by default and can be revealed via the node's show/hide controls. +Extended version of FLUX2 GGUF Models Loader. Supports up to 4 LoRAs loaded simultaneously. Enable `apply_sampling` to apply flux sampling shift patch — +useful for improving quality at non-standard resolutions. Advanced parameters are hidden by default and can be revealed via the node's show/hide +controls. --- diff --git a/web/docs/TT_Flux2WorkflowSettings.md b/web/docs/TT_Flux2WorkflowSettings.md new file mode 100644 index 0000000..b8af403 --- /dev/null +++ b/web/docs/TT_Flux2WorkflowSettings.md @@ -0,0 +1,44 @@ +## TT Flux2 Workflow Settings *TT_Flux2WorkflowSettings* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Central workflow configuration node for FLUX2 pipelines. Consolidates all sampling parameters into a single +TT_WORKFLOW_CONFIG object while also exposing each value as an individual output. + +### Inputs + +| Parameter | Type | Required | Description | +|----------------|--------|----------|---------------------------------------------------------------------------| +| `seed` | INT | Yes | Generation seed (0 – 18446744073709551615, default: 0) | +| `steps` | INT | Yes | Number of sampling steps (1–10000, default: 25) | +| `cfg` | FLOAT | Yes | CFG scale (0.0–100.0, default: 1.5) | +| `sampler_name` | STRING | Yes | Sampler algorithm (KSampler sampler list) | +| `width` | INT | Yes | Output image width in pixels (16–MAX_RESOLUTION, step: 8, default: 1024) | +| `height` | INT | Yes | Output image height in pixels (16–MAX_RESOLUTION, step: 8, default: 1024) | +| `guidance` | FLOAT | Yes | Guidance scale for text encoder (1.0–10.0, default: 3.5) | + +### Outputs + +| Parameter | Type | Description | +|-------------------|--------------------|------------------------------------------------------| +| `WORKFLOW_CONFIG` | TT_WORKFLOW_CONFIG | Full configuration object with all settings bundled | +| `SEED` | INT | Generation seed | +| `STEPS` | INT | Number of sampling steps | +| `CFG` | FLOAT | CFG scale | +| `SAMPLER` | SAMPLER | Instantiated sampler object | +| `SAMPLER_NAME` | STRING | Name of the selected sampler | +| `SCHEDULER` | SIGMAS | Computed sigma schedule based on seed and resolution | +| `WIDTH` | INT | Output image width | +| `HEIGHT` | INT | Output image height | +| `GUIDANCE` | FLOAT | Guidance scale | + +### Usage + +Use this node as the single source of truth for FLUX2 workflow parameters. The `WORKFLOW_CONFIG` output is +designed for context-aware nodes that consume a TT_WORKFLOW_CONFIG directly. Individual outputs are available +for connecting to standard ComfyUI nodes. The sigma schedule (`SCHEDULER`) is computed automatically from +the seed and resolution via `get_schedule`. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_Flux2WorkflowSettingsAdvanced.md b/web/docs/TT_Flux2WorkflowSettingsAdvanced.md new file mode 100644 index 0000000..d9b4059 --- /dev/null +++ b/web/docs/TT_Flux2WorkflowSettingsAdvanced.md @@ -0,0 +1,47 @@ +## TT Flux2 Workflow Settings (Advanced) *TT_Flux2WorkflowSettingsAdvanced* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Extended workflow configuration node for FLUX2 pipelines. Includes all parameters from the base version plus +built-in prompt and LoRA trigger fields, bundling everything into a single TT_WORKFLOW_CONFIG object. + +### Inputs + +| Parameter | Type | Required | Description | +|-----------------|--------|----------|---------------------------------------------------------------------------| +| `seed` | INT | Yes | Generation seed (0 – 18446744073709551615, default: 0) | +| `steps` | INT | Yes | Number of sampling steps (1–10000, default: 25) | +| `cfg` | FLOAT | Yes | CFG scale (0.0–100.0, default: 1.5) | +| `sampler_name` | STRING | Yes | Sampler algorithm (KSampler sampler list) | +| `width` | INT | Yes | Output image width in pixels (16–MAX_RESOLUTION, step: 8, default: 1024) | +| `height` | INT | Yes | Output image height in pixels (16–MAX_RESOLUTION, step: 8, default: 1024) | +| `prompt` | STRING | Yes | Main generation prompt (multiline, supports dynamic prompts) | +| `lora_triggers` | STRING | Yes | LoRA trigger words (multiline, supports dynamic prompts) | +| `guidance` | FLOAT | Yes | Guidance scale for text encoder (1.0–10.0, default: 3.5) | + +### Outputs + +| Parameter | Type | Description | +|-------------------|--------------------|------------------------------------------------------| +| `WORKFLOW_CONFIG` | TT_WORKFLOW_CONFIG | Full configuration object with all settings bundled | +| `SEED` | INT | Generation seed | +| `STEPS` | INT | Number of sampling steps | +| `CFG` | FLOAT | CFG scale | +| `SAMPLER` | SAMPLER | Instantiated sampler object | +| `SAMPLER_NAME` | STRING | Name of the selected sampler | +| `SCHEDULER` | SIGMAS | Computed sigma schedule based on seed and resolution | +| `WIDTH` | INT | Output image width | +| `HEIGHT` | INT | Output image height | +| `PROMPT` | STRING | Main generation prompt | +| `GUIDANCE` | FLOAT | Guidance scale | + +### Usage + +Advanced version of TT Flux2 Workflow Settings with `prompt` and `lora_triggers` fields included directly in the +node. Both are stored in the `WORKFLOW_CONFIG` output alongside all other sampling parameters, making this node +sufficient as the sole configuration source for full context-driven FLUX2 pipelines. Individual outputs remain +available for connecting to standard ComfyUI nodes. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_GuiderImageReference.md b/web/docs/TT_GuiderImageReference.md new file mode 100644 index 0000000..2c58d36 --- /dev/null +++ b/web/docs/TT_GuiderImageReference.md @@ -0,0 +1,32 @@ +## TT Guider Image Reference *TT_GuiderImageReference* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Encode a reference image into latent space and inject it into a Guider's positive conditioning for reference-based generation. + +### Inputs + +| Parameter | Type | Required | Description | +|------------------|--------|----------|---------------------------------------------------------------------------| +| `vae` | VAE | Yes | VAE model used to encode the reference image | +| `guider` | GUIDER | Yes | Guider to inject reference latent into | +| `megapixels` | COMBO | Yes | Target resolution for the reference image before encoding | +| `upscale_method` | STRING | Yes | Resampling method for resizing (default: bicubic) - advanced | +| `dimension_step` | INT | Yes | Snaps image dimensions to multiples of this value (default: 1) - advanced | +| `image` | STRING | Yes | Reference image from input directory | + +### Outputs + +| Parameter | Type | Description | +|-----------|--------|---------------------------------------| +| `VAE` | VAE | Passthrough VAE for chaining | +| `GUIDER` | GUIDER | Guider with reference latent injected | + +### Usage + +Loads a reference image, resizes it to the target megapixel resolution, encodes it with VAE, and injects the resulting latent into the Guider's +positive conditioning as `reference_latents`. Raises an error if the Guider has no positive conditioning. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_ImagePreviewUpscaleSave.md b/web/docs/TT_ImagePreviewUpscaleSave.md new file mode 100644 index 0000000..93fd13a --- /dev/null +++ b/web/docs/TT_ImagePreviewUpscaleSave.md @@ -0,0 +1,35 @@ +## TT Image Preview / Upscale / Save *TT_ImagePreviewUpscaleSave* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Combined preview, upscale, and save node with flexible output options. + +### Inputs + +| Parameter | Type | Required | Description | +|----------------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------------------| +| `image` | IMAGE | Yes | Input image tensor to preview, upscale, or save | +| `save_image` | BOOLEAN | Yes | Enable/disable saving to disk (default: Enabled) | +| `filename_prefix` | STRING | Yes | Prefix for output filename (default: "tenser-tensor") | +| `filename_format` | STRING | Yes | Naming pattern: "name-###" (prefix-00001), "date-name-###" (2026-02-16-prefix-00001), "name-datetime" (prefix-20260216-143045) - advanced | +| `subfolder` | STRING | Yes | Optional subfolder within output directory (empty = root output folder) - advanced | +| `image_format` | STRING | Yes | Output format: PNG, JPEG, or WEBP (default: PNG) - advanced | +| `image_quality` | INT | Yes | Quality for JPEG and WEBP formats (0-100, default: 100). Ignored for PNG - advanced | +| `compression_level` | INT | Yes | Compression level for PNG format (0-9, default: 9). Ignored for JPEG and WEBP - advanced | +| `upscale_image` | BOOLEAN | Yes | Enable/disable upscaling (default: Enabled) | +| `upscaler_device` | STRING | Yes | Device for upscaling: "default" or "cpu" - advanced | +| `upscale_model_name` | STRING | Yes | Upscale model selected from available upscale_models - advanced | +| `tile` | INT | Yes | Tile size for tiled upscaling (256-4096, step 64, default: 512) - advanced | +| `overlap` | INT | Yes | Overlap between tiles in pixels (0-256, step 8, default: 64) - advanced | + +### Outputs + +This is an output node with no return values. Images are displayed in the UI and optionally upscaled and saved to disk. + +### Usage + +All-in-one node that can preview, upscale, and save images in a single step. Upscaling is applied before saving — the upscaled result is both displayed in the UI and written to disk. When `upscale_image` is disabled, the original image is used. When `save_image` is disabled, the image is only previewed in the UI without writing to disk. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_KSamplerGuided.md b/web/docs/TT_KSamplerGuided.md new file mode 100644 index 0000000..dd28806 --- /dev/null +++ b/web/docs/TT_KSamplerGuided.md @@ -0,0 +1,31 @@ +## TT KSampler (Guided) *TT_KSamplerGuided* + +*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* + +Guided latent sampler that takes a pre-configured guider, sigmas, sampler, and noise to produce denoised latent samples. + +### Inputs + +| Parameter | Type | Required | Description | +|----------------|---------|----------|-----------------------------------------| +| `latent` | LATENT | Yes | Input latent to sample from | +| `guider` | GUIDER | Yes | Configured guider with encoded prompt | +| `sigmas` | SIGMAS | Yes | Sigma schedule for the sampling process | +| `sampler` | SAMPLER | Yes | Instantiated sampler object | +| `random_noise` | NOISE | Yes | Noise source for the diffusion process | + +### Outputs + +| Parameter | Type | Description | +|-----------|--------|-------------------------| +| `SAMPLES` | LATENT | Denoised latent samples | + +### Usage + +Runs guided diffusion sampling on the input latent. Connect a guider from a text encoder node, sigmas from a +scheduler, and a sampler from a workflow settings node. `random_noise` controls stochasticity — use a seeded +noise node for reproducible results. + +--- + +[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor) diff --git a/web/docs/TT_LatentFactory.md b/web/docs/TT_LatentFactory.md index 513db63..f9f8a74 100644 --- a/web/docs/TT_LatentFactory.md +++ b/web/docs/TT_LatentFactory.md @@ -1,37 +1,43 @@ -# TT Latent Factory *TT_LatentFactory* +## TT Latent Factory *TT_LatentFactory* *This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.* -Generate empty latent images with precise dimension control based on aspect ratio and megapixel count. - +Generates empty latent images with precise dimension control based on aspect ratio and megapixel count. Automatically calculates correct dimensions for FLUX and SDXL models, plus separate CLIP conditioning sizes. -## Parameters +### Inputs -| Parameter | Type | Description | -|-------------------|----------|---------------------------------------------------| -| `aspect_ratio` | DROPDOWN | Image proportions (1:1, 4:3, 3:2, 16:9, 21:9) | -| `seed` | INT | Seed for reproducible results (0 - 2^64-1) | -| `megapixels` | DROPDOWN | Total resolution (0.5 MP, 1 MP, 2 MP, 4 MP, 8 MP) | -| `orientation` | DROPDOWN | landscape or portrait | -| `model_type` | DROPDOWN | FLUX (16 channels) or SDXL (4 channels) | -| `clip_multiplier` | DROPDOWN | CLIP target size multiplier (1x, 2x, 4x) | -| `batch_size` | INT | Number of latents to generate (default: 1) | +| Parameter | Type | Required | Description | +|-------------------|--------|----------|---------------------------------------------------------------| +| `seed` | INT | Yes | Seed for reproducible results (0 – 18446744073709551615) | +| `noise_seed` | INT | Yes | Separate seed for noise generation (0 – 18446744073709551615) | +| `aspect_ratio` | STRING | Yes | Image proportions (1:1, 4:3, 3:2, 16:9, 21:9) | +| `megapixels` | COMBO | Yes | Total resolution (0.25 MP, 0.5 MP, 1 MP, 2 MP, 4 MP, 8 MP) | +| `orientation` | STRING | Yes | Image orientation (landscape / portrait) | +| `model_type` | STRING | Yes | Target model architecture (FLUX1.D, FLUX2.D, SDXL) | +| `batch_size` | INT | Yes | Number of latents to generate (1–64, default: 1) - advanced | +| `clip_multiplier` | STRING | Yes | CLIP target size multiplier (1x, 2x, 4x) - advanced | -## Outputs +### Outputs -| Output | Type | Description | -|-----------------|--------|-----------------------------------------------------| -| `LATENT` | LATENT | Empty latent image tensor with random noise | -| `SEED` | INT | Seed value | -| `WIDTH` | INT | Calculated image width (rounded to multiple of 64) | -| `HEIGHT` | INT | Calculated image height (rounded to multiple of 64) | -| `TARGET_WIDTH` | INT | CLIP conditioning target width | -| `TARGET_HEIGHT` | INT | CLIP conditioning target height | +| Parameter | Type | Description | +|-----------------|--------|------------------------------------------| +| `LATENT` | LATENT | Empty latent tensor ready for sampling | +| `RANDOM_NOISE` | NOISE | Noise object seeded with `noise_seed` | +| `SEED` | INT | Generation seed | +| `NOISE_SEED` | INT | Noise seed | +| `MEGAPIXELS` | COMBO | Selected megapixel value for chaining | +| `WIDTH` | INT | Calculated image width (multiple of 64) | +| `HEIGHT` | INT | Calculated image height (multiple of 64) | +| `TARGET_WIDTH` | INT | CLIP conditioning target width | +| `TARGET_HEIGHT` | INT | CLIP conditioning target height | -## Example +### Usage -Select 16:9, 2 MP, landscape, FLUX, 2x → generates 1920×1088 latent with 3840×2176 CLIP dimensions. +Select aspect ratio, resolution, and orientation to automatically compute pixel dimensions suited for the chosen +model. FLUX1.D uses 16-channel latents, FLUX2.D uses 128-channel, SDXL uses 4-channel. `clip_multiplier` scales the TARGET +dimensions relative to the image dimensions — useful for multi-resolution CLIP conditioning. Connect `RANDOM_NOISE` +directly to a sampler node for reproducible noise tied to `noise_seed`. --- diff --git a/web/docs/TT_PostproductionAdvanced.md b/web/docs/TT_PostproductionAdvanced.md index febd568..e60b7bd 100644 --- a/web/docs/TT_PostproductionAdvanced.md +++ b/web/docs/TT_PostproductionAdvanced.md @@ -41,7 +41,7 @@ Extended post-production node with image enhancement, LUT color grading, film gr | Parameter | Type | Description | |-----------|-------|--------------------------------------| -| `IMAGE` | IMAGE | Processed image with applied effects | +| `IMAGE` | IMAGE | Processed image with applied effects | ### Usage