Merge pull request #4 from tenser-tensor/feature/image-nodes

feature/image-nodes
This commit is contained in:
Tenser Tensor
2026-03-11 17:19:47 +03:00
committed by GitHub
62 changed files with 3883 additions and 505 deletions
+33 -3
View File
@@ -1,9 +1,39 @@
[1.6.20] - 2026-03-11
Added
**TT_Sd35GgufModelsLoaderNode, TT_Sd35GgufModelsLoaderAdvancedNode** — SD3.5 GGUF model loaders with LoRA support
**TT_Sd35WorkflowSettingsNode** — workflow settings for SD3.5 with schedule shift control
**TT_Sd35TextEncoderContextNode** — SD3.5 text encoder with separate CLIP-L, CLIP-G, and T5-XXL prompts
**TT_LatentFactoryByModelNode** — latent factory with automatic format detection from connected model
**TT_GuidedKSamplerNode, TT_GuidedUpscaleKSamplerNode** — guided KSampler variants
**TT_GuidedKSamplerWithPreviewNode** — guided KSampler with step-by-step latent preview
**TT_SdxlModelsLoaderNode, TT_SdxlModelsLoaderAdvancedNode** — SDXL model loaders
**TT_FluxModelsLoaderNode, TT_FluxModelsLoaderAdvancedNode** — FLUX model loaders
**TT_SdxlClipTextEncoderNode, TT_SdxlClipTextEncoderContextNode** — SDXL text encoders
**TT_Flux1ClipTextEncoderNode, TT_Flux1ClipTextEncoderContextNode** — FLUX1 text encoders
**TT_Flux2TextEncoderNode, TT_Flux2TextEncoderContextNode** — FLUX2 text encoders
**TT_CannyEdgeDetectorNode** — Canny edge detection for ControlNet
**TT_ImageLoaderResizerNode, TT_ImagePreviewSaveNode, TT_ImagePreviewUpscaleSaveNode** — image tools
**TT_GuiderImageReferenceNode** — image reference for guided conditioning
**TT_LatentMultiTransformNode, TT_LatentMultiTransformOnPixelSpaceNode** — latent transform nodes
**TT_Flux2ApplyControlNetNode** — ControlNet support for FLUX2
Changed
Latent factory refactored — channel count and spatial downscale ratio derived from model automatically
GGUF folder registration extended — unet_gguf, diffusion_models_gguf, text_encoders_gguf folders registered automatically
Deprecated
All migrated to API V3 nodes moved to Deprecated/ category. Will be removed in a future major release.
[1.5.11] - 2026-03-01
Added
New context nodes (API V3): Base Context, Context, FLUX2 Context, Passthrough, Set Guider / Image / Latent, Extract Encoder / Guided Sampler / VAE / Image for FLUX2
TT_LatentFactoryNode — V3 replacement for TT_LatentFactory
TT_Flux2WorkflowSettingsNode and TT_Flux2WorkflowSettingsAdvancedNode — V3 replacements for FLUX2 workflow settings nodes
**TT_LatentFactoryNode** — V3 replacement for TT_LatentFactory
**TT_Flux2WorkflowSettingsNode** and TT_Flux2WorkflowSettingsAdvancedNode — V3 replacements for FLUX2 workflow settings nodes
Changed
@@ -12,7 +42,7 @@ Per-group NODES_COUNT tracking and isolated error handling on import failures
Deprecated
All pre-V3 context, latent, and FLUX2 workflow nodes moved to Deprecated/ categories. Will be removed in a future major release.
All pre-V3 context, latent, and FLUX2 workflow nodes moved to Deprecated/ category. Will be removed in a future major release.
## v1.4.7 (2026-02-25)
-256
View File
@@ -1,256 +0,0 @@
## Nodes List
### Workflow
#### TT FLUX Workflow Settings
Node for configuring Flux workflow generation parameters.
#### TT FLUX Workflow Settings (Advanced)
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.
#### TT SDXL Workflow Settings (Advanced)
Advanced SDXL workflow configuration with separate CLIP-L/G prompts and aesthetic scores.
### Latent
#### TT Latent Factory
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
#### TT SDXL Models Loader
All-in-one model loader for SDXL workflows with checkpoint merging support.
#### TT SDXL Models Loader (Advanced)
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.
#### TT FLUX Models Loader (Advanced)
Extended all-in-one model loader for Flux with sampling patches, LoRA support, and advanced settings.
### Text Encoder
#### TT CLIP Text Encode FLUX
Flux text encoder with separate CLIP-L and T5-XXL prompt inputs.
#### TT CLIP Text Encode FLUX (Context)
Context-based Flux text encoder that extracts prompts from workflow config.
#### TT CLIP Text Encode SDXL
SDXL text encoder with separate CLIP-L and CLIP-G prompt inputs and conditioning parameters.
#### TT CLIP Text Encode SDXL (Context)
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
Standard KSampler for generating images. Accepts all common sampling parameters and returns a denoised latent ready for
VAE decoding.
#### TT KSampler (Advanced)
Advanced sampler for multi-stage workflows and fine control. Use start_step/last_step for partial denoising, add_noise
to control noise injection, and denoise for strength control. Useful for img2img, refinement passes, and complex
sampling workflows.
#### TT KSampler (Context)
Simplified sampler for context-based workflows. Automatically extracts all sampling parameters from the context object:
- Model, positive/negative conditioning, and latent from context
- Seed, steps, cfg, sampler_name, and scheduler from workflow_config
Requires context to contain: model, latent, and workflow_config. Raises error if any required component is missing.
#### TT KSampler (Two Stages)
Automated two-stage sampling workflow. First performs a draft pass with partial denoising, then refines the result with
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)
Simplified decoder for context-based workflows. Automatically extracts VAE and latent from context, decodes to image
space, and stores the result back in context. Handles nested tensors and reshapes output to standard format. Raises
error if VAE or latent is missing from context.
#### TT VAE Decode (Tiled)
Decode large latent images by processing them in smaller tiles. Useful for generating high-resolution images that would
otherwise exceed VRAM capacity. The overlap parameter helps reduce visible seams between tiles. Enable circular padding
for seamless texture generation.
#### TT Vae Encode (Context)
Encodes pixel-space images into latent representations for diffusion processing. Extracts VAE and image from context,
performs encoding, and stores the result back in context.
Requires context to contain both `vae` and `image` components. Raises error if either is missing.
#### TT Vae Encode (Tiled)
Processes large images in smaller tiles to reduce memory requirements during VAE encoding. Useful for encoding
high-resolution images that would otherwise cause out-of-memory errors.
Tile size should be set based on available VRAM (smaller tiles = less memory). Overlap prevents visible seams at tile
boundaries - higher overlap produces smoother results but increases processing time. Recommended overlap is 10-15% of
tile size.
### Postproduction
#### TT Apply LUT
Apply professional color grading using LUT files. Place your .cube LUT files in the designated LUT directory. Adjust
strength to blend between original and graded image (1.0 = full LUT, 0.0 = original). Choose colorspace based on your
LUT file's intended working space.
#### TT Add Film Grain
Add realistic film grain texture to images for a cinematic aesthetic. Adjust scale for grain size, strength for
intensity, and saturation for color. The toe parameter controls the distribution curve of grain in shadows/highlights.
Use different seeds for varied grain patterns.
#### TT Quick Image Upscaler
Quick and simple AI upscaling node using tiled processing for memory efficiency. Select an upscale model from your
upscale_models directory. Adjust tile size based on VRAM - smaller tiles use less memory. Increase overlap to reduce
visible seams between tiles.
#### TT Image Enhancer
All-in-one image enhancement node for adjusting brightness, contrast, colors, and applying effects. Use default values (
brightness: 0.0, contrast/gamma/saturation: 1.0, posterize: 8, solarize: 1.0) for no effect. Adjust individual
parameters as needed - effects are applied sequentially.
#### TT Postproduction
Complete post-production pipeline in one node. Toggle each effect on/off with boolean switches. Processing order: LUT →
Film Grain → Upscale. Disable any effect by setting its toggle to False. All effects are optional and can be used
independently or combined.
#### TT Postproduction (Advanced)
Complete post-production pipeline with full enhancement controls. Processing order: Image Enhancement → LUT → Film
Grain → Upscale. Toggle each effect on/off independently. Image enhancement includes brightness, contrast, gamma, hue,
saturation, sharpness, posterize, and solarize adjustments. All effects are optional.
### Image
#### TT Image Preview / Save
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
Use this node to package your core workflow components into a single context object that can be passed through your
workflow and modified by other context nodes.
#### TT Context
Use this node to create a new context from scratch or modify specific parameters in an existing context. All parameters
are optional - provide only what you need to set or change.
#### TT FLUX Large Context
Use this node for Flux workflows when you need fine-grained control over CLIP-L and T5-XXL prompts separately, along
with Flux-specific guidance settings.
#### TT SDXL Large Context
Use this node for SDXL workflows when you need separate control over CLIP-L and CLIP-G prompts, aesthetic scores, and
target dimensions for conditioning.
#### TT Even Larger Context
Use this node when you need comprehensive control over all workflow parameters including ControlNet and masking.
Supports both SDXL (CLIP-L/G) and Flux (T5-XXL) conditioning simultaneously.
#### TT Context Set Image
Stores pixel-space image in context for use by downstream nodes. Overwrites existing image if present.
#### TT Context Set Latent
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
rest of the context.
+46 -22
View File
@@ -11,28 +11,25 @@
*— Alfred Bester, The Demolished Man*
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.
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.
> [!IMPORTANT]
> ⚠️ **Note:** The pack is currently undergoing refactoring and migration to ComfyUI API V3. Some nodes are marked as deprecated and will be removed in a future major release. Please check node descriptions for migration guidance.
> ⚠️ **Note:** The pack is currently undergoing refactoring and migration to ComfyUI API V3. Some nodes are marked as deprecated and will be removed
> in a future major release. Please check node descriptions for migration guidance.
## Features
- **Workflow settings nodes** - configure entire workflow parameters (basic and advanced variants)
- **All-in-one model loaders** - load checkpoint, LoRAs, CLIP, and VAE in single nodes
- **Latent factory** - generate latents with aspect ratio, megapixel count, orientation, and type selection (Flux/SDXL)
- **Context nodes** - from Basic to Even Larger Context
- Basic: combines latent, model, CLIP, VAE, and workflow config into context
- Others: pass-through nodes that modify specific context values while preserving the rest
- **CLIP text encoders** - context-based and prompt-based encoding with model-specific controls
- **KSamplers** - Different implementations of KSampler for generating images. Accepts all common sampling parameters
and returns a denoised latent ready for VAE decoding.
- **VAE decoders** - basic, context-based, extended, and two-pass (draft + refinement)
- **Post-production tools** - image adjustments (brightness, contrast, saturation), color correction (apply LUT), film
grain, upscaler
- All-in-one post-production node combining all tools
- Simplified version without adjustments
- **Workflow settings** — configure global workflow parameters for FLUX, SDXL, and SD3.5
- **Model loaders** — all-in-one loaders for checkpoint, LoRA, CLIP, and VAE; GGUF support for FLUX2 and SD3.5
- **Context nodes** — pass pipeline state between nodes as a single context object
- **Latent factory** — create latents with aspect ratio, megapixel, and orientation selection; automatic format detection from model
- **CLIP text encoders** — prompt encoding with model-specific controls for FLUX, SDXL, and SD3.5
- **Samplers** — KSampler variants including guided sampling with Guider/Sigmas support and step-by-step latent preview
- **Image tools** — load, resize, upscale, preview, save, and reference images for ControlNet
- **VAE** — encode and decode with tiled and context-based variants
- **Post-production** — brightness, contrast, saturation, LUT, film grain, and upscaling
-
## Installation
@@ -61,10 +58,6 @@ comfy node install tenser-tensor/ComfyUI-TenserTensor
Restart ComfyUI after installation.
## Nodes List
[Nodes List](NODES.md)
## Acknowledgments
This project uses GGUF file handling code from [City96](https://github.com/city96).
@@ -75,10 +68,41 @@ Great thanks and appreciation for your excellent work!
## Additional
> [!NOTE]
> ⚠️ **Custom Type Compatibility Notice:** The custom data types `TT_CONTEXT` and `TT_WORKFLOW_CONFIG` are **proprietary to this node pack** and are
> not compatible with native ComfyUI nodes or third-party nodes.
> These types can only be connected to inputs and outputs within the **ComfyUI-TenserTensor** pack. Attempting to pass them to external nodes will
> result in a type mismatch error.
> [!NOTE]
> **For GGUF users:** ComfyUI does not scan for GGUF files by default.
> Place your files according to the structure below — the node pack registers these folders automatically.
```
📂 ComfyUI/
├── 📂 models/
│ ├── 📂 diffusion_models/
│ │ └── # GGUF diffusion models (main location)
│ ├── 📂 unet/
│ │ └── # GGUF diffusion models (alternative location, deprecated)
│ ├── 📂 diffusion_models_gguf/
│ │ └── # GGUF diffusion models (recommended)
│ ├── 📂 text_encoders/
│ │ └── # Text encoders: CLIP-L, CLIP-G, T5-XXL (safetensors or GGUF, main location)
│ ├── 📂 clip/
│ │ └── # Text encoders (alternative location, deprecated)
│ ├── 📂 text_encoders_gguf/
│ │ └── # Text encoders (recommended)
│ ├── 📂 loras/
│ │ └── # LoRA files
│ └── 📂 vae/
│ └── # VAE files (safetensors)
```
> [!TIP]
> You can download Flux.2-dev along with additional models (CLIP, VAE) via the link below:
> [Comfy-Org/flux2-dev](https://huggingface.co/Comfy-Org/flux2-dev/tree/main/split_files)
---
(c) 2026 TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
_(c) 2026 TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)_
+2 -1
View File
@@ -9,9 +9,10 @@ __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
print()
SEP = f"{CYAN}{'=' * 60}{RESET}"
print(SEP)
print(f"{YELLOW}TenserTensor ComfyUI Nodes Pack (v1.5.6): {BRIGHT_GREEN}{NODES_COUNT} Nodes Loaded{RESET}")
print(f"{YELLOW}TenserTensor ComfyUI Nodes Pack (v1.6.20): {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}")
print(f"{BRIGHT_CYAN}[V] Please check README file: https://github.com/tenser-tensor/ComfyUI-TenserTensor/blob/main/README.md{RESET}")
print(SEP)
print()
+105 -33
View File
@@ -1,4 +1,4 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
RESET = "\033[0m"
BOLD = "\033[1m"
@@ -11,10 +11,10 @@ WHITE = "\033[37m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_CYAN = "\033[96m"
NODES_COUNT = 0
import inspect
import sys
try:
before = set(dir())
from .nodes_context import *
from .nodes.context.base_context import TT_BaseContext
from .nodes.context.context import TT_Context
@@ -25,40 +25,41 @@ try:
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
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}")
print(f"{YELLOW}TenserTensor: {RED}ERROR: Context nodes unavailable{RESET}")
try:
from .nodes_detector import *
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Detector nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.image.image_preview_save import TT_ImagePreviewSave
from .nodes.image.image_preview_upscale_save import TT_ImagePreviewUpscaleSave
from .nodes.image.guider_image_reference import TT_GuiderImageReference
NODES_COUNT += len(set(dir()) - before)
from .nodes_image import *
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Image nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes_controlnet import *
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: ControlNet nodes unavailable{RESET}")
try:
from .nodes.latent.latent_factory import TT_LatentFactory
from .nodes_latent import *
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Latent nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.loaders.flux_models_loader import TT_FluxModelsLoader
from .nodes.loaders.flux_models_loader_advanced import TT_FluxModelsLoaderAdvanced
from .nodes.loaders.gguf_models_loader import TT_Flux2GgufModelsLoader
from .nodes.loaders.gguf_models_loader_advanced import TT_Flux2GgufModelsLoaderAdvanced
from .nodes.loaders.sdxl_models_loader import TT_SdxlModelsLoader
from .nodes.loaders.sdxl_models_loader_advanced import TT_SdxlModelsLoaderAdvanced
NODES_COUNT += len(set(dir()) - before)
from .nodes_loaders import *
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Loader nodes unavailable{RESET}")
@@ -70,44 +71,35 @@ try:
from .nodes.postproduction.postproduction import TT_Postproduction
from .nodes.postproduction.postproduction_advanced import TT_PostproductionAdvanced
from .nodes.postproduction.quick_image_upscaler import TT_QuickImageUpscaler
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Postproduction nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.sampling.ksampler import TT_KSampler
from .nodes.sampling.ksampler_advanced import TT_KSamplerAdvanced
from .nodes.sampling.ksampler_context import TT_KSamplerContext
from .nodes.sampling.ksampler_two_stage import TT_KSamplerTwoStage
from .nodes.sampling.ksampler_guided import TT_KSamplerGuided
NODES_COUNT += len(set(dir()) - before)
from .nodes_sampling import *
except ImportError:
print("f{YELLOW}TenserTensor: {RED}ERROR: Sampler nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.text_encoders.clip_text_encode_flux import TT_ClipTextEncodeFlux
from .nodes.text_encoders.clip_text_encode_flux_context import TT_ClipTextEncodeFluxContext
from .nodes.text_encoders.clip_text_encode_sdxl import TT_ClipTextEncodeSdxl
from .nodes.text_encoders.clip_text_encode_sdxl_context import TT_ClipTextEncodeSdxlContext
from .nodes.text_encoders.clip_text_encode_flux2 import TT_ClipTextEncodeFlux2
from .nodes.text_encoders.clip_text_encode_flux2_context import TT_ClipTextEncodeFlux2Context
NODES_COUNT += len(set(dir()) - before)
from .nodes_text_encoder import *
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Text Encoder nodes unavailable{RESET}")
try:
before = set(dir())
from .nodes.vae.vae_decode_context import TT_VaeDecodeContext
from .nodes.vae.vae_decode_tiled import TT_VaeDecodeTiled
from .nodes.vae.vae_encode_context import TT_VaeEncodeContext
from .nodes.vae.vae_encode_tiled import TT_VaeEncodeTiled
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: VAE nodes unavailable{RESET}")
@@ -120,11 +112,17 @@ try:
from .nodes.workflow.flux2_workflow_settings import TT_Flux2WorkflowSettings
from .nodes.workflow.flux2_workflow_settings_advanced import TT_Flux2WorkflowSettingsAdvanced
from .nodes_workflow import *
NODES_COUNT += len(set(dir()) - before)
except ImportError:
print(f"{YELLOW}TenserTensor: {RED}ERROR: Workflow nodes unavailable{RESET}")
ADDED = {
name
for name, obj
in inspect.getmembers(sys.modules[__name__], inspect.isclass)
if name.startswith("TT_")
}
NODES_COUNT = len(ADDED)
NODE_CLASS_MAPPINGS = {
# Context Deprecated
"TT_BaseContext": TT_BaseContext,
@@ -139,7 +137,7 @@ NODE_CLASS_MAPPINGS = {
# Context V3 Nodes
"TT_BaseContextNode": TT_BaseContextNode,
"TT_BaseContextFlux2Node": TT_BaseContextFlux2Node,
"TT_BaseContextPassthroughNode": TT_BaseContextPassthroughNode,
"TT_ContextPassthroughNode": TT_ContextPassthroughNode,
"TT_ContextNode": TT_ContextNode,
"TT_ContextFlux2Node": TT_ContextFlux2Node,
"TT_ContextSetGuiderNode": TT_ContextSetGuiderNode,
@@ -149,14 +147,27 @@ NODE_CLASS_MAPPINGS = {
"TT_ContextExtractGuidedSamplerFlux2Node": TT_ContextExtractGuidedSamplerFlux2Node,
"TT_ContextExtractVaeNode": TT_ContextExtractVaeNode,
"TT_ContextExtractImageNode": TT_ContextExtractImageNode,
# ControlNet V3 Nodes
"TT_Flux2ApplyControlNetNode": TT_Flux2ApplyControlNetNode,
"TT_Flux2ApplyControlNetAdvancedNode": TT_Flux2ApplyControlNetAdvancedNode,
# Detector V3 Nodes
"TT_CannyEdgeDetectorNode": TT_CannyEdgeDetectorNode,
# Image
"TT_ImagePreviewSave": TT_ImagePreviewSave,
"TT_ImagePreviewUpscaleSave": TT_ImagePreviewUpscaleSave,
"TT_GuiderImageReference": TT_GuiderImageReference,
# Image V3 Nodes
"TT_ImageLoaderResizerNode": TT_ImageLoaderResizerNode,
"TT_ImagePreviewSaveNode": TT_ImagePreviewSaveNode,
"TT_ImagePreviewUpscaleSaveNode": TT_ImagePreviewUpscaleSaveNode,
"TT_GuiderImageReferenceNode": TT_GuiderImageReferenceNode,
# Latent
"TT_LatentFactory": TT_LatentFactory,
# Latent V3 Nodes
"TT_LatentFactoryNode": TT_LatentFactoryNode,
"TT_LatentFactoryByModelNode": TT_LatentFactoryByModelNode,
"TT_LatentMultiTransformNode": TT_LatentMultiTransformNode,
"TT_LatentMultiTransformOnPixelSpaceNode": TT_LatentMultiTransformOnPixelSpaceNode,
# Loaders
"TT_SdxlModelsLoader": TT_SdxlModelsLoader,
"TT_SdxlModelsLoaderAdvanced": TT_SdxlModelsLoaderAdvanced,
@@ -164,6 +175,15 @@ NODE_CLASS_MAPPINGS = {
"TT_Flux2GgufModelsLoaderAdvanced": TT_Flux2GgufModelsLoaderAdvanced,
"TT_FluxModelsLoader": TT_FluxModelsLoader,
"TT_FluxModelsLoaderAdvanced": TT_FluxModelsLoaderAdvanced,
# Loaders V3 Nodes
"TT_SdxlModelsLoaderNode": TT_SdxlModelsLoaderNode,
"TT_SdxlModelsLoaderAdvancedNode": TT_SdxlModelsLoaderAdvancedNode,
"TT_FluxModelsLoaderNode": TT_FluxModelsLoaderNode,
"TT_FluxModelsLoaderAdvancedNode": TT_FluxModelsLoaderAdvancedNode,
"TT_GgufModelsLoaderNode": TT_Flux2GgufModelsLoaderNode,
"TT_GgufModelsLoaderAdvancedNode": TT_Flux2GgufModelsLoaderAdvancedNode,
"TT_Sd35GgufModelsLoaderNode": TT_Sd35GgufModelsLoaderNode,
# "TT_Sd35GgufModelsLoaderAdvancedNode": TT_Sd35GgufModelsLoaderAdvancedNode,
# Postproduction
"TT_ApplyLut": TT_ApplyLut,
"TT_AddFilmGrain": TT_AddFilmGrain,
@@ -177,6 +197,10 @@ NODE_CLASS_MAPPINGS = {
"TT_KSamplerContext": TT_KSamplerContext,
"TT_KSamplerTwoStage": TT_KSamplerTwoStage,
"TT_KSamplerGuided": TT_KSamplerGuided,
# Samplers V3 Nodes
"TT_GuidedKSamplerNode": TT_GuidedKSamplerNode,
"TT_GuidedUpscaleKSamplerNode": TT_GuidedUpscaleKSamplerNode,
"TT_GuidedKSamplerWithPreviewNode": TT_GuidedKSamplerWithPreviewNode,
# Text Encoder
"TT_ClipTextEncodeFlux": TT_ClipTextEncodeFlux,
"TT_ClipTextEncodeFluxContext": TT_ClipTextEncodeFluxContext,
@@ -184,6 +208,15 @@ NODE_CLASS_MAPPINGS = {
"TT_ClipTextEncodeSdxlContext": TT_ClipTextEncodeSdxlContext,
"TT_ClipTextEncodeFlux2": TT_ClipTextEncodeFlux2,
"TT_ClipTextEncodeFlux2Context": TT_ClipTextEncodeFlux2Context,
# Text Encoder V3 Nodes
"TT_SdxlClipTextEncoderNode": TT_SdxlClipTextEncoderNode,
"TT_SdxlClipTextEncoderContextNode": TT_SdxlClipTextEncoderContextNode,
"TT_Flux1ClipTextEncoderNode": TT_Flux1ClipTextEncoderNode,
"TT_Flux1ClipTextEncoderContextNode": TT_Flux1ClipTextEncoderContextNode,
"TT_Flux2TextEncoderNode": TT_Flux2TextEncoderNode,
"TT_Flux2TextEncoderContextNode": TT_Flux2TextEncoderContextNode,
"TT_Sd35TextEncoderNode": TT_Sd35TextEncoderNode,
"TT_Sd35TextEncoderContextNode": TT_Sd35TextEncoderContextNode,
# VAE
"TT_VaeDecodeContext": TT_VaeDecodeContext,
"TT_VaeDecodeTiled": TT_VaeDecodeTiled,
@@ -199,6 +232,8 @@ NODE_CLASS_MAPPINGS = {
# Workflow V3 Nodes
"TT_Flux2WorkflowSettingsNode": TT_Flux2WorkflowSettingsNode,
"TT_Flux2WorkflowSettingsAdvancedNode": TT_Flux2WorkflowSettingsAdvancedNode,
"TT_Sd35WorkflowSettingsNode": TT_Sd35WorkflowSettingsNode,
"TT_Sd35WorkflowSettingsAdvancedNode": TT_Sd35WorkflowSettingsAdvancedNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -215,7 +250,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
# Context V3 Nodes
"TT_BaseContextNode": "TT Base Context",
"TT_BaseContextFlux2Node": "TT FLUX2 Base Context",
"TT_BaseContextPassthroughNode": "TT Context Passthrough",
"TT_ContextPassthroughNode": "TT Context Passthrough",
"TT_ContextNode": "TT Context",
"TT_ContextFlux2Node": "TT FLUX2 Context",
"TT_ContextSetGuiderNode": "TT Context Set Guider",
@@ -225,14 +260,27 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"TT_ContextExtractGuidedSamplerFlux2Node": "TT Context Extract Guided Sampler Flux2",
"TT_ContextExtractVaeNode": "TT Context Extract VAE Flux2",
"TT_ContextExtractImageNode": "TT Context Extract Image Flux2",
# ControlNet V3 Nodes
"TT_Flux2ApplyControlNetNode": "TT Flux2 Apply ControlNet",
"TT_Flux2ApplyControlNetAdvancedNode": "TT FLUX2 Apply ControlNet (Advanced)",
# Detector V3 Nodes
"TT_CannyEdgeDetectorNode": "TT_Canny Edge Detector",
# Image
"TT_ImagePreviewSave": "TT Image Preview / Save",
"TT_ImagePreviewUpscaleSave": "TT Image Preview / Upscale / Save",
"TT_GuiderImageReference": "TT Guider Image Reference",
"TT_ImagePreviewSave": "TT Image Preview / Save (Deprecated)",
"TT_ImagePreviewUpscaleSave": "TT Image Preview / Upscale / Save (Deprecated)",
"TT_GuiderImageReference": "TT Guider Image Reference (Deprecated)",
# Image V3 Nodes
"TT_ImageLoaderResizerNode": "TT Image Loader / Resizer",
"TT_ImagePreviewSaveNode": "TT Image Preview / Save",
"TT_ImagePreviewUpscaleSaveNode": "TT Image Preview / Upscale / Save",
"TT_GuiderImageReferenceNode": "TT Guider Image Reference",
# Latent
"TT_LatentFactory": "TT Latent Factory (Deprecated)",
# Latent V3 Nodes
"TT_LatentFactoryNode": "TT Latent Factory",
"TT_LatentFactoryByModelNode": "TT Latent Factory (By Model)",
"TT_LatentMultiTransformNode": "TT Latent Multi Transform",
"TT_LatentMultiTransformOnPixelSpaceNode": "TT Latent MultiTransform (On Pixel Space)",
# Loaders
"TT_SdxlModelsLoader": "TT SDXL Models Loader",
"TT_SdxlModelsLoaderAdvanced": "TT SDXL Models Loader (Advanced)",
@@ -240,6 +288,15 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"TT_Flux2GgufModelsLoaderAdvanced": "TT FLUX2 GGUF Models Loader (Advanced)",
"TT_FluxModelsLoader": "TT FLUX Models Loader",
"TT_FluxModelsLoaderAdvanced": "TT FLUX Models Loader (Advanced)",
# Loaders V3 Nodes
"TT_SdxlModelsLoaderNode": "TT SDXL Models Loader",
"TT_SdxlModelsLoaderAdvancedNode": "TT SDXL Models Loader (Advanced)",
"TT_FluxModelsLoaderNode": "TT FLUX Models Loader",
"TT_FluxModelsLoaderAdvancedNode": "TT FLUX Models Loader (Advanced)",
"TT_GgufModelsLoaderNode": "TT GGUF Models Loader",
"TT_GgufModelsLoaderAdvancedNode": "TT GGUF Models Loader (Advanced)",
"TT_Sd35GgufModelsLoaderNode": "TT SD3.5 GGUF Models Loader",
# "TT_Sd35ModelsLoaderAdvancedNode": "TT SD3.5 GGUF Models Loader (Advanced)",
# Postproduction
"TT_ApplyLut": "TT Apply LUT",
"TT_AddFilmGrain": "TT Add Film Grain",
@@ -253,6 +310,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"TT_KSamplerContext": "TT KSampler (Context)",
"TT_KSamplerTwoStage": "TT KSampler (Two Stages)",
"TT_KSamplerGuided": "TT KSampler (Guided)",
# Samplers V3 Nodes
"TT_GuidedKSamplerNode": "TT Guided KSampler",
"TT_GuidedUpscaleKSamplerNode": "TT Guided Upscale KSampler",
"TT_GuidedKSamplerWithPreviewNode": "TT Guided KSampler (With Preview)",
# Text Encoder
"TT_ClipTextEncodeFlux": "TT CLIP Text Encode FLUX",
"TT_ClipTextEncodeFluxContext": "TT CLIP Text Encode FLUX (Context)",
@@ -260,6 +321,15 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"TT_ClipTextEncodeSdxlContext": "TT CLIP Text Encode SDXL (Context)",
"TT_ClipTextEncodeFlux2": "TT CLIP Text Encode FLUX2",
"TT_ClipTextEncodeFlux2Context": "TT CLIP Text Encode FLUX2 (Context)",
# Text Encoder V3 Nodes
"TT_SdxlClipTextEncoderNode": "TT SDXL CLIP Text Encoder",
"TT_SdxlClipTextEncoderContextNode": "TT SDXL CLIP Text Encoder (Context)",
"TT_Flux1ClipTextEncoderNode": "TT FLUX1 CLIP Text Encoder",
"TT_Flux1ClipTextEncoderContextNode": "TT FLUX1 CLIP Text Encoder (Context)",
"TT_Flux2TextEncoderNode": "TT FLUX2 Text Encoder",
"TT_Flux2TextEncoderContextNode": "TT FLUX2 Text Encoder (Context)",
"TT_Sd35TextEncoderNode": "TT SD3.5 Text Encoder",
"TT_Sd35TextEncoderContextNode": "TT SD3.5 Text Encoder (Context)",
# VAE
"TT_VaeDecodeContext": "TT VAE Decode (Context)",
"TT_VaeDecodeTiled": "TT VAE Decode (Tiled)",
@@ -275,4 +345,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
# Workflow V3 Nodes
"TT_Flux2WorkflowSettingsNode": "TT FLUX2 Workflow Settings",
"TT_Flux2WorkflowSettingsAdvancedNode": "TT FLUX2 Workflow Settings (Advanced)",
"TT_Sd35WorkflowSettingsNode": "TT SD3.5 Workflow Settings",
"TT_Sd35WorkflowSettingsAdvancedNode": "TT SD3.5 Workflow Settings (Advanced)",
}
+1 -1
View File
@@ -34,7 +34,7 @@ class TT_GuiderImageReference():
RETURN_TYPES = ("VAE", "GUIDER",)
RETURN_NAMES = ("VAE", "GUIDER",)
FUNCTION = "update_latent"
CATEGORY = "TenserTensor/Image"
CATEGORY = "TenserTensor/Deprecated/Image"
def update_latent(self, vae, guider, megapixels, upscale_method, dimension_step, image):
timage, _ = load_image(image)
+1 -1
View File
@@ -26,7 +26,7 @@ class TT_ImagePreviewSave():
RETURN_NAMES = ()
OUTPUT_NODE = True
FUNCTION = "preview_save_image"
CATEGORY = "TenserTensor/Image"
CATEGORY = "TenserTensor/Deprecated/Image"
def preview_save_image(
self,
+1 -1
View File
@@ -35,7 +35,7 @@ class TT_ImagePreviewUpscaleSave():
RETURN_NAMES = ()
OUTPUT_NODE = True
FUNCTION = "preview_upscale_save_image"
CATEGORY = "TenserTensor/Image"
CATEGORY = "TenserTensor/Deprecated/Image"
def preview_upscale_save_image(self, **kwargs):
image = kwargs.get("image")
+1 -1
View File
@@ -22,7 +22,7 @@ class TT_FluxModelsLoader():
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/FLUX"
CATEGORY = "TenserTensor/Deprecated/Loaders/FLUX"
def load_models(
self,
+1 -1
View File
@@ -48,7 +48,7 @@ class TT_FluxModelsLoaderAdvanced():
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/FLUX"
CATEGORY = "TenserTensor/Deprecated/Loaders/FLUX"
def load_models(self, **kwargs):
unet_name = kwargs.get("unet_name")
+1 -1
View File
@@ -28,7 +28,7 @@ class TT_Flux2GgufModelsLoader():
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/FLUX2 GGUF"
CATEGORY = "TenserTensor/Deprecated/Loaders/FLUX2 GGUF"
VAE_DEVICE = "cpu"
VAE_DTYPE = "bfloat16"
+1 -1
View File
@@ -55,7 +55,7 @@ class TT_Flux2GgufModelsLoaderAdvanced():
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/FLUX2 GGUF"
CATEGORY = "TenserTensor/Deprecated/Loaders/FLUX2 GGUF"
def load_models(self, **kwargs):
model = load_unet(
+1 -1
View File
@@ -24,7 +24,7 @@ class TT_SdxlModelsLoader:
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/SDXL"
CATEGORY = "TenserTensor/Deprecated/Loaders/SDXL"
def load_models(self, primary_ckpt, secondary_ckpt, primary_weight, clip_l, clip_g, clip_device, vae_name):
model = load_checkpoint(primary_ckpt)[0]
+1 -1
View File
@@ -43,7 +43,7 @@ class TT_SdxlModelsLoaderAdvanced:
RETURN_TYPES = ("MODEL", "CLIP", "VAE",)
RETURN_NAMES = ("MODEL", "CLIP", "VAE",)
FUNCTION = "load_models"
CATEGORY = "TenserTensor/Loaders/SDXL"
CATEGORY = "TenserTensor/Deprecated/Loaders/SDXL"
def load_models(self, primary_ckpt, secondary_ckpt, primary_weight, clip_l, clip_g, clip_device,
lora_name_1, strength_1, lora_name_2, strength_2, lora_name_3, strength_3, lora_name_4, strength_4,
+1 -1
View File
@@ -19,7 +19,7 @@ class TT_KSamplerGuided:
RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("SAMPLES",)
FUNCTION = "execute_sampling"
CATEGORY = "TenserTensor/Sampling"
CATEGORY = "TenserTensor/Deprecated/Sampling"
def execute_sampling(self, latent, guider, sigmas, sampler, random_noise):
return (guided_sample_latents(latent, guider, sigmas, sampler, random_noise),)
+7 -6
View File
@@ -70,6 +70,13 @@ def guided_sample_latents(latent, guider, sigmas, sampler, random_noise):
callback = LP.prepare_callback(model_patcher, sigmas.shape[-1] - 1, interim_buffer)
disable_pbar = not U.PROGRESS_BAR_ENABLED
print(f"latent: {latent}")
print(f"sampler: {sampler}")
print(f"sigmas: {sigmas}")
print(f"random_noise: {random_noise}")
print(f"GUIDER: {guider}")
samples = guider.sample(
random_noise.generate_noise(latent),
latent_tensor,
@@ -85,10 +92,4 @@ def guided_sample_latents(latent, guider, sigmas, sampler, random_noise):
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
+1 -1
View File
@@ -20,7 +20,7 @@ class TT_ClipTextEncodeFlux():
RETURN_TYPES = ("CONDITIONING", "CONDITIONING",)
RETURN_NAMES = ("POSITIVE", "NEGATIVE",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/FLUX"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/FLUX"
def execute(self, **kwargs):
return encode_prompts_flux(**kwargs)
@@ -19,7 +19,7 @@ class TT_ClipTextEncodeFlux2():
RETURN_TYPES = ("GUIDER",)
RETURN_NAMES = ("GUIDER",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/FLUX2"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/FLUX2"
def execute(self, model, clip, prompt, lora_triggers, guidance):
return encode_prompts_flux2(model, clip, prompt, lora_triggers, guidance)
@@ -15,7 +15,7 @@ class TT_ClipTextEncodeFlux2Context():
RETURN_TYPES = ("TT_CONTEXT", "GUIDER",)
RETURN_NAMES = ("CONTEXT", "GUIDER",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/FLUX2"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/FLUX2"
def execute(self, context):
model = context.get("model", None)
@@ -15,7 +15,7 @@ class TT_ClipTextEncodeFluxContext():
RETURN_TYPES = ("TT_CONTEXT", "CONDITIONING", "CONDITIONING",)
RETURN_NAMES = ("CONTEXT", "POSITIVE", "NEGATIVE",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/FLUX"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/FLUX"
def execute(self, context):
clip = context["clip"]
+1 -1
View File
@@ -26,7 +26,7 @@ class TT_ClipTextEncodeSdxl():
RETURN_TYPES = ("CONDITIONING", "CONDITIONING",)
RETURN_NAMES = ("POSITIVE", "NEGATIVE",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/SDXL"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/SDXL"
def execute(self, **kwargs):
return encode_prompts_sdxl(**kwargs)
@@ -15,7 +15,7 @@ class TT_ClipTextEncodeSdxlContext():
RETURN_TYPES = ("TT_CONTEXT", "CONDITIONING", "CONDITIONING",)
RETURN_NAMES = ("CONTEXT", "POSITIVE", "NEGATIVE",)
FUNCTION = "execute"
CATEGORY = "TenserTensor/Text Encoders/SDXL"
CATEGORY = "TenserTensor/Deprecated/Text Encoders/SDXL"
def execute(self, context):
clip = context["clip"]
+11 -11
View File
@@ -1,4 +1,4 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import dataclasses
import re
@@ -112,8 +112,8 @@ class Context:
super().__init__(id, **kwargs)
class Output(IO.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class ContextNode(IO.ComfyNode):
@@ -162,9 +162,9 @@ class ContextNode(IO.ComfyNode):
for f_name in cls.RETURNS if len(cls.RETURNS) > 0 else cls.REQUIRED + cls.OPTIONAL:
output_schema.append(
Context.Output(display_name="CONTEXT")
Context.Output("CONTEXT")
if f_name == "context"
else cls.F_TYPES[f_name].Output(display_name=f_name.upper())
else cls.F_TYPES[f_name].Output(id=f_name.upper())
)
return output_schema
@@ -235,7 +235,7 @@ class TT_BaseContextFlux2Node(ContextNode):
RETURNS = ("context",)
class TT_BaseContextPassthroughNode(ContextNode):
class TT_ContextPassthroughNode(ContextNode):
"""Passes context through without modification."""
REQUIRED = ("context",)
OPTIONAL = ()
@@ -315,13 +315,13 @@ class TT_ContextExtractImageNode(ContextNode):
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class ContextExtension(ComfyExtension):
class ContextNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
TT_BaseContextNode,
TT_BaseContextFlux2Node,
TT_BaseContextPassthroughNode,
TT_ContextPassthroughNode,
TT_ContextNode,
TT_ContextFlux2Node,
TT_ContextSetGuiderNode,
@@ -334,8 +334,8 @@ class ContextExtension(ComfyExtension):
]
async def comfy_entrypoint() -> ContextExtension:
return ContextExtension()
async def comfy_entrypoint() -> ContextNodesExtension:
return ContextNodesExtension()
# ==============================================================================
@@ -345,7 +345,7 @@ async def comfy_entrypoint() -> ContextExtension:
__all__ = [
"TT_BaseContextNode",
"TT_BaseContextFlux2Node",
"TT_BaseContextPassthroughNode",
"TT_ContextPassthroughNode",
"TT_ContextNode",
"TT_ContextFlux2Node",
"TT_ContextSetGuiderNode",
+193
View File
@@ -0,0 +1,193 @@
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
import folder_paths
from comfy import controlnet
from comfy_api.latest import io, ui, ComfyExtension
from .nodes_image import SingleCondCFGGuider, get_image_files, load_image
CATEGORY = "TenserTensor/ControlNet"
def get_control_net_files():
return folder_paths.get_filename_list("controlnet")
def load_control_net(filename):
cnet_path = folder_paths.get_full_path_or_raise("controlnet", filename)
print(f"CNET path: {cnet_path}")
cnet = controlnet.load_controlnet(cnet_path)
if cnet is None:
raise RuntimeError("ERROR: ControlNet file is invalid or corrupted.")
return cnet
def apply_control_net(**kwargs):
guider, image, loaded_cnet = (
kwargs.get("guider"),
kwargs.get("cond_hint"),
kwargs.get("loaded_cnet"),
)
conditioning = guider.get_conds()
cnet_cache = {}
cond_hint = image.movedim(-1, 1)
for tensor in conditioning:
cond_copy = tensor[1].copy()
prev_cnet = cond_copy.get("control", None)
if prev_cnet in cnet_cache:
applied_cnet = cnet_cache[prev_cnet]
else:
applied_cnet = loaded_cnet.copy()
applied_cnet.set_cond_hint(
cond_hint,
kwargs.get("strength"),
(kwargs.get("start_percent") / 100, kwargs.get("end_percent") / 100),
vae=kwargs.get("vae_opt"),
extra_concat=kwargs.get("extra_concat", None)
)
applied_cnet.set_previous_controlnet(prev_cnet)
cnet_cache[prev_cnet] = applied_cnet
cond_copy["control"] = applied_cnet
cond_copy['control_apply_to_uncond'] = False
updated_cond = [tensor[0], cond_copy]
guider.set_conds(updated_cond)
return guider
class TT_Flux2ApplyControlNetNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2ApplyControlNetNode",
display_name="TT FLUX2 Apply ControlNet",
category=CATEGORY,
description="",
inputs=[
io.Guider.Input("guider"),
io.Image.Input("reference_image"),
io.ControlNet.Input("control_net_opt", optional=True),
io.Vae.Input("vae_opt", optional=True),
io.Combo.Input("control_net", options=get_control_net_files()),
io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.1),
io.Float.Input("start_percent", default=0.0, min=0.0, max=100.0, step=0.1, advanced=True),
io.Float.Input("end_percent", default=100.0, min=0.0, max=100.0, step=0.1, advanced=True),
],
outputs=[
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
guider, strength, control_net = (
kwargs.get("guider"),
kwargs.get("strength"),
kwargs.get("control_net"))
if not isinstance(guider, SingleCondCFGGuider):
guider = SingleCondCFGGuider.from_cfg_guider(guider)
if strength == 0.0:
return io.NodeOutput(guider)
loaded_cnet = kwargs.get("control_net_opt")
kwargs["loaded_cnet"] = (
loaded_cnet
if loaded_cnet is not None
else load_control_net(kwargs.get("control_net"))
)
kwargs["cond_hint"] = kwargs.get("reference_image")
guider = apply_control_net(**kwargs)
return io.NodeOutput(guider)
class TT_Flux2ApplyControlNetAdvancedNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2ApplyControlNetAdvancedNode",
display_name="TT FLUX2 Apply ControlNet (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Guider.Input("guider"),
io.ControlNet.Input("control_net_opt", optional=True),
io.Image.Input("reference_image_opt", optional=True),
io.Vae.Input("vae_opt", optional=True),
io.Combo.Input("control_net", options=get_control_net_files()),
io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.1),
io.Float.Input("start_percent", default=0.0, min=0.0, max=100.0, step=0.1, advanced=True),
io.Float.Input("end_percent", default=100.0, min=0.0, max=100.0, step=0.1, advanced=True),
io.Combo.Input("reference_image", options=get_image_files(), upload=io.UploadType.image)
],
outputs=[
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
guider, strength, control_net = (
kwargs.get("guider"),
kwargs.get("strength"),
kwargs.get("control_net"))
if not isinstance(guider, SingleCondCFGGuider):
guider = SingleCondCFGGuider.from_cfg_guider(guider)
if strength == 0.0:
return io.NodeOutput(guider)
reference_image = kwargs.get("reference_image_opt")
kwargs["cond_hint"] = (
reference_image
if reference_image is not None
else load_image(kwargs.get("reference_image"))[0]
)
loaded_cnet = kwargs.get("control_net_opt")
kwargs["loaded_cnet"] = (
loaded_cnet
if loaded_cnet is not None
else load_control_net(kwargs.get("control_net"))
)
guider = apply_control_net(**kwargs)
return io.NodeOutput(guider, ui=ui.PreviewImage(kwargs["cond_hint"], cls=cls))
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class ControlNetNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_Flux2ApplyControlNetNode,
TT_Flux2ApplyControlNetAdvancedNode,
]
async def comfy_entrypoint() -> ControlNetNodesExtension:
return ControlNetNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_Flux2ApplyControlNetNode",
"TT_Flux2ApplyControlNetAdvancedNode",
]
+72
View File
@@ -0,0 +1,72 @@
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
from kornia.filters import canny
from comfy import model_management
from comfy_api.latest import io, ui, ComfyExtension
from .nodes_image import get_image_files, load_image
CATEGORY = "TenserTensor/Detector"
def detect_edge(image, low_threshold, high_threshold):
torch_device = model_management.get_torch_device()
intermediate_device = model_management.intermediate_device()
timage = image.to(torch_device).movedim(-1, 1)
o_images = canny(timage, low_threshold, high_threshold)
return o_images[1].to(intermediate_device).expand(-1, 3, -1, -1).movedim(1, -1)
class TT_CannyEdgeDetectorNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_CannyEdgeDetectorNode",
display_name="TT Canny Edge Detector",
category=CATEGORY,
description="",
inputs=[
io.Float.Input("low_threshold", default=0.4, min=0.01, max=0.99, step=0.01),
io.Float.Input("high_threshold", default=0.8, min=0.01, max=0.99, step=0.01),
io.Combo.Input("reference_image", options=get_image_files(), upload=io.UploadType.image)
],
outputs=[
io.Image.Output("IMAGE")
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
timage, _ = load_image(kwargs.get("reference_image"))
cn_image = detect_edge(timage, kwargs.get("low_threshold"), kwargs.get("high_threshold"))
return io.NodeOutput(cn_image, ui=ui.PreviewImage(timage, cls=cls))
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class DetectorNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_CannyEdgeDetectorNode,
]
async def comfy_entrypoint() -> DetectorNodesExtension:
return DetectorNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_CannyEdgeDetectorNode",
]
+503 -1
View File
@@ -1 +1,503 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import fnmatch
import math
import os
from datetime import datetime
from pathlib import Path
from typing import override, Any
import numpy
import torch
from PIL import Image, ImageOps, ImageSequence
from spandrel import ModelLoader, ImageModelDescriptor
import folder_paths
from comfy import model_management, utils, samplers
from comfy_api.latest import ComfyExtension, io, ui
from node_helpers import conditioning_set_values, pillow
from .nodes_vae import vae_encode
from .nodes_text_encoder import SingleCondCFGGuider
CATEGORY = "TenserTensor/Image"
RESIZE_METHODS = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
FILENAME_FORMATS = ["name-###", "date-name-###", "name-datetime"]
FORMAT_EXT = {"PNG": ".png", "JPEG": ".jpg", "WEBP": ".webp", }
MEGAPIXELS = ["0.25 MP", "0.5 MP", "1 MP", "2 MP", "4 MP", "8 MP"]
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") -> torch.Tensor:
is_mask = timage.ndim == 3
if is_mask:
timage = timage.unsqueeze(-1)
samples = timage.movedim(-1, 1)
orig_height, orig_width = samples.shape[2:]
if crop == "disabled":
scale_factor = max(to_width / orig_width, to_height / orig_height)
final_width = align_to_step(orig_width, scale_factor, dimension_step)
final_height = align_to_step(orig_height, scale_factor, dimension_step)
else:
final_width = round(to_width / dimension_step) * dimension_step
final_height = round(to_height / dimension_step) * dimension_step
final_width, final_height = int(max(final_width, dimension_step)), int(max(final_height, dimension_step))
resized = utils.common_upscale(samples, final_width, final_height, upscale_method, crop).movedim(1, -1)
return resized.squeeze(-1) if is_mask else resized
def resize_image_to_megapixels(timage, resize_method, megapixels, dimension_step=1, crop="disabled") -> torch.Tensor:
is_mask = timage.ndim == 3
if is_mask:
timage = timage.unsqueeze(-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))
if crop == "disabled":
final_width = align_to_step(orig_width, scale_factor, dimension_step)
final_height = align_to_step(orig_height, scale_factor, dimension_step)
else:
to_width, to_height = orig_width * scale_factor, orig_height * scale_factor
final_width = round(to_width / dimension_step) * dimension_step
final_height = round(to_height / dimension_step) * dimension_step
final_width, final_height = int(max(final_width, dimension_step)), int(max(final_height, dimension_step))
resized = utils.common_upscale(samples, final_width, final_height, resize_method, crop).movedim(1, -1)
return resized.squeeze(-1) if is_mask else resized
def rotate_image(pixels, turns):
return torch.rot90(pixels, k=turns, dims=[1, 2])
def flip_image(pixels, axis):
return torch.flip(pixels, dims=[2 if axis == "x" else 1])
def get_torch_device(device=None):
return (
model_management.get_torch_device()
if device is None or device == "default"
else torch.device(device)
)
def load_image(filename, device=None) -> tuple[torch.Tensor, torch.Tensor]:
tdevice = get_torch_device(device)
path = folder_paths.get_annotated_filepath(filename)
pil_image = pillow(Image.open, 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)
masks.append(tmask)
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)
if pil_image.format == "MPO":
break
if len(images) == 0:
raise ValueError(f"ERROR: No frames found in {path}")
o_image = torch.cat(images, dim=0) if len(images) > 1 else images[0]
match len(masks):
case 0:
o_mask = torch.zeros((1, height, width), dtype=torch.float32)
case 1:
o_mask = masks[0].unsqueeze(0)
case _:
o_mask = torch.cat(masks, dim=0)
return o_image, o_mask,
def get_image_files():
return sorted([
f.name for f in os.scandir(folder_paths.get_input_directory())
if f.is_file() and Path(f.name).suffix.lower() in (".png", ".jpg", ".webp", ".bmp")
])
class TT_ImageLoaderResizerNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_ImageLoaderResizerNode",
display_name="TT Image Loader / Resizer",
category=CATEGORY,
description="",
inputs=[
io.Boolean.Input("resize_image", default=False, label_on="Scale", label_off="Skip"),
io.Combo.Input("resize_method", options=RESIZE_METHODS, advanced=True),
io.Int.Input("dimension_step", default=1, min=1, max=256, advanced=True),
io.Combo.Input("megapixels", options=MEGAPIXELS, advanced=True),
io.Combo.Input("image", options=get_image_files(), upload=io.UploadType.image)
],
outputs=[
io.Image.Output(display_name="IMAGE"),
io.Mask.Output(display_name="MASK"),
],
)
@classmethod
def fingerprint_inputs(cls, **kwargs) -> dict[str, Any]:
path = folder_paths.get_annotated_filepath(kwargs.get("image"))
resize_image = kwargs.get("resize_image")
result = {
"image": path + str(os.path.getmtime(path)),
"resize_image": resize_image,
}
if resize_image:
result["megapixels"] = kwargs.get("megapixels")
result["resize_method"] = kwargs.get("resize_method")
result["dimension_step"] = kwargs.get("dimension_step")
return result
@classmethod
def validate_inputs(cls, **kwargs) -> bool | str:
path = folder_paths.get_annotated_filepath(kwargs.get("image"))
if not os.path.isfile(path):
return f"Image file not found: {path}"
return True
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
timage, tmask = load_image(kwargs.get("image"))
if kwargs.get("resize_image"):
resize_method, megapixels, dimension_step = (
kwargs.get("resize_method"),
kwargs.get("megapixels"),
kwargs.get("dimension_step")
)
timage = resize_image_to_megapixels(timage, resize_method, megapixels, dimension_step)
tmask = resize_image_to_megapixels(tmask, resize_method, megapixels, dimension_step)
return io.NodeOutput(timage, tmask, ui=ui.PreviewImage(timage, cls=cls))
class InvalidSavePathError(Exception):
pass
def build_filename(save_path, filename_mask) -> str:
count = sum(1 for f in os.scandir(save_path) if fnmatch.fnmatch(f.name, filename_mask)) + 1
return filename_mask.replace("*", f"{count:05}")
def build_save_path(**kwargs) -> tuple[str, str]:
output_path, subfolder, image_format, filename_prefix = (
kwargs.get("output_path"),
kwargs.get("subfolder"),
kwargs.get("image_format"),
kwargs.get("filename_prefix"),
)
save_path = os.path.join(output_path, subfolder)
if os.path.commonpath((output_path, os.path.abspath(save_path))) != output_path:
raise InvalidSavePathError("Saving image outside the output folder is not allowed.")
os.makedirs(save_path, exist_ok=True)
ext = FORMAT_EXT[image_format]
filename = ""
match kwargs.get("filename_format"):
case "name-###":
filename = build_filename(save_path, f"{filename_prefix}-*{ext}")
case "date-name-###":
date = datetime.now().strftime("%Y-%m-%d")
filename = build_filename(save_path, f"{date}-{filename_prefix}-*{ext}")
case "name-datetime":
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"{filename_prefix}-{timestamp}{ext}"
case _:
filename = f"{filename_prefix}{ext}"
return save_path, filename
def save_image(**kwargs) -> None:
kwargs["output_path"] = folder_paths.get_output_directory()
image = kwargs.get("image")
for frame in image:
np_frame = frame.cpu().numpy() * 255.0
tframe = Image.fromarray(numpy.clip(np_frame, 0, 255).astype(numpy.uint8))
save_path, filename = build_save_path(**kwargs)
image_format = kwargs.get("image_format")
params = (
{"compress_level": kwargs.get("compression_level")}
if kwargs.get("image_format") == "PNG"
else {"quality": kwargs.get("image_quality")}
)
tframe.save(os.path.join(save_path, filename), format=image_format, **params)
class TT_ImagePreviewSaveNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_ImagePreviewSaveNode",
display_name="TT Image Preview / Save",
category=CATEGORY,
description="",
is_output_node=True,
inputs=[
io.Image.Input("image"),
io.Boolean.Input("save_image", default=True, label_on="Save image", label_off="Only preview"),
io.String.Input("filename_prefix", default="TT"),
io.Combo.Input("filename_format", options=FILENAME_FORMATS, default="name-datetime", advanced=True),
io.String.Input("subfolder", default="", advanced=True),
io.Combo.Input("image_format", options=list(FORMAT_EXT.keys()), default="PNG", advanced=True),
io.Int.Input("image_quality", default=75, min=1, max=100, advanced=True),
io.Int.Input("compression_level", default=6, min=0, max=9, advanced=True),
],
outputs=[]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
timage = kwargs.get("image")
if kwargs.get("save_image"):
save_image(**kwargs)
return io.NodeOutput(ui=ui.PreviewImage(timage, cls=cls))
DEVICES = ["default", "cpu"]
def get_upscale_models() -> list[str]:
return folder_paths.get_filename_list("upscale_models")
def load_upscale_model(upscale_model) -> ImageModelDescriptor:
model_path = folder_paths.get_full_path_or_raise("upscale_models", upscale_model)
try:
sd = utils.load_torch_file(model_path, safe_load=True)
except Exception as e:
raise RuntimeError(f"Failed to load model file {model_path}: {e}")
sd = utils.state_dict_prefix_replace(sd, {"module.": ""})
upscale_model = ModelLoader().load_from_state_dict(sd).eval()
if not isinstance(upscale_model, ImageModelDescriptor):
raise TypeError(f"Expected ImageModelDescriptor, got {type(upscale_model).__name__}")
return upscale_model
BUFFER_FACTOR = 384.0
def upscale(timage, **kwargs):
device_name, upscale_tile, upscale_overlap = (
kwargs.get("upscaler_device", None),
kwargs.get("upscale_tile"),
kwargs.get("upscale_overlap")
)
device = get_torch_device(device_name)
model = load_upscale_model(kwargs.get("upscale_model"))
i_image = timage.movedim(-1, 1).to(device)
batch, _, height, width = i_image.shape
m_model = model_management.module_size(model.model)
m_upscale_tile = (upscale_tile * upscale_tile * 3) * i_image.element_size() * max(model.scale, 1) * BUFFER_FACTOR
m_total = m_model + m_upscale_tile + i_image.nelement() * i_image.element_size()
model_management.free_memory(m_total, device)
model.model.to(device)
progress_bar = None
try:
while True:
try:
steps = batch * utils.get_tiled_scale_steps(
width, height,
tile_x=upscale_tile,
tile_y=upscale_tile,
overlap=upscale_overlap
)
progress_bar = utils.ProgressBar(steps)
scaled = utils.tiled_scale(
i_image,
model,
tile_x=upscale_tile,
tile_y=upscale_tile,
overlap=upscale_overlap,
upscale_amount=model.scale,
pbar=progress_bar
)
break
except model_management.OOM_EXCEPTION as e:
progress_bar = None
upscale_tile //= 2
if upscale_tile < 128:
raise e
finally:
if device.type != "cpu":
model.model.to("cpu")
return torch.clamp(scaled.movedim(1, -1), min=0.0, max=1.0)
class TT_ImagePreviewUpscaleSaveNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_ImagePreviewUpscaleSaveNode",
display_name="TT Image Preview / Upscale / Save",
category=CATEGORY,
description="",
is_output_node=True,
inputs=[
io.Image.Input("image"),
io.Boolean.Input("save_image", default=True, label_on="Save image", label_off="Only preview"),
io.Boolean.Input("upscale_image", default=True, label_on="Upscale image", label_off="Keep size"),
io.String.Input("filename_prefix", default="TT"),
io.Combo.Input("filename_format", options=FILENAME_FORMATS, default="name-datetime", advanced=True),
io.String.Input("subfolder", default="", advanced=True),
io.Combo.Input("image_format", options=list(FORMAT_EXT.keys()), default="PNG", advanced=True),
io.Int.Input("image_quality", default=75, min=1, max=100, advanced=True),
io.Int.Input("compression_level", default=6, min=0, max=9, advanced=True),
io.Combo.Input("upscaler_device", options=DEVICES, advanced=True),
io.Combo.Input("upscale_model", options=get_upscale_models(), advanced=True),
io.Int.Input("upscale_tile", default=512, min=128, max=4096, step=64, advanced=True),
io.Int.Input("upscale_overlap", default=64, min=8, max=256, step=8, advanced=True),
],
outputs=[]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
timage = kwargs.get("image")
if kwargs.get("upscale_image"):
timage = upscale(timage, **kwargs)
if kwargs.get("save_image"):
kwargs["image"] = timage
save_image(**kwargs)
return io.NodeOutput(ui=ui.PreviewImage(timage, cls=cls))
class TT_GuiderImageReferenceNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_GuiderImageReferenceNode",
display_name="TT Guider Image Reference",
category=CATEGORY,
description="",
inputs=[
io.Vae.Input("vae"),
io.Guider.Input("guider"),
io.Combo.Input("megapixels", options=MEGAPIXELS),
io.Combo.Input("resize_method", options=RESIZE_METHODS, advanced=True),
io.Int.Input("dimension_step", default=1, min=1, max=256, advanced=True),
io.Combo.Input("image", options=get_image_files(), upload=io.UploadType.image)
],
outputs=[
io.Vae.Output("VAE"),
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
timage, _ = load_image(kwargs.get("image"))
vae, guider, resize_method, megapixels, dimension_step = (
kwargs.get("vae"),
kwargs.get("guider"),
kwargs.get("resize_method"),
kwargs.get("megapixels"),
kwargs.get("dimension_step")
)
if not isinstance(guider, SingleCondCFGGuider):
guider = SingleCondCFGGuider.from_cfg_guider(guider)
resized = resize_image_to_megapixels(timage, resize_method, megapixels, dimension_step)
latent = vae_encode(resized, vae)
conditioning = guider.get_conds()
if conditioning is not None:
conditioning = conditioning_set_values(conditioning, {"reference_latents": [latent["samples"]]}, append=True)
guider.set_conds(conditioning)
else:
raise ValueError("ERROR: Guider has no conditioning")
return io.NodeOutput(vae, guider, ui=ui.PreviewImage(timage, cls=cls))
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class ImageNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_ImageLoaderResizerNode,
TT_ImagePreviewSaveNode,
TT_ImagePreviewUpscaleSaveNode,
TT_GuiderImageReferenceNode,
]
async def comfy_entrypoint() -> ImageNodesExtension:
return ImageNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_ImageLoaderResizerNode",
"TT_ImagePreviewSaveNode",
"TT_ImagePreviewUpscaleSaveNode",
"TT_GuiderImageReferenceNode",
]
+284 -88
View File
@@ -1,19 +1,30 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import math
from typing import Any, override
import torch
import comfy.sample
from comfy_api.latest import IO, ComfyExtension
from .lib.common import CommonTypes
from comfy import sample, utils
from comfy.latent_formats import SDXL, Flux, Flux2, SD3
from comfy_api.latest import io, ComfyExtension
from .nodes_image import MEGAPIXELS, resize_image, rotate_image, flip_image
from .nodes_vae import vae_decode, vae_encode
CATEGORY = "TenserTensor/Latent"
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"]
MODEL_TYPES = {
"FLUX1.D": Flux,
"FLUX2.D": Flux2,
"SDXL": SDXL,
"SD3.X": SD3
}
ORIENTATIONS = ["landscape", "portrait"]
ROTATE_ANGLES = ["90°", "180°", "270°"]
SCALE_METHODS = ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]
SCALE_FACTORS = ["0.25x", "0.5x", "1x", "2x", "4x", "8x"]
class RandomNoise:
@@ -23,126 +34,308 @@ class RandomNoise:
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 comfy.sample.prepare_noise(latent_image, self.seed, batch_idx)
return sample.prepare_noise(latent_image, self.seed, batch_idx)
LATENT_DIMENSION_STEP = 64
class TT_LatentFactoryNode(IO.ComfyNode):
def calculate_dimensions(total_pixels: int, ratio_w: int, ratio_h: int) -> tuple[int, int]:
height = int(math.sqrt(total_pixels * ratio_h / ratio_w))
width = int(height * ratio_w / ratio_h)
width = round(width / LATENT_DIMENSION_STEP) * LATENT_DIMENSION_STEP
height = round(height / LATENT_DIMENSION_STEP) * LATENT_DIMENSION_STEP
return width, height
def build_latent(seed: int, batch_size: int, channels: int, latent_width: int, latent_height: int) -> torch.Tensor:
generator = torch.Generator().manual_seed(seed)
return torch.randn(batch_size, channels, latent_height, latent_width, generator=generator)
def create_empty_latent(**kwargs) -> tuple[dict[str, Any], int, int]:
total_pixels = int(float(kwargs.get("megapixels").split()[0]) * 1_000_000)
ratio_parts = kwargs.get("aspect_ratio").split(':')
ratio_w, ratio_h = (
(int(ratio_parts[0]), int(ratio_parts[1]))
if kwargs.get("orientation") == "landscape"
else (int(ratio_parts[1]), int(ratio_parts[0]))
)
width, height = calculate_dimensions(total_pixels, ratio_w, ratio_h)
downscale_ratio, channels = None, None
model = kwargs.get("model")
if model:
fmt = model.model.latent_format
downscale_ratio, channels = fmt.spacial_downscale_ratio, fmt.latent_channels
else:
model_type = kwargs.get("model_type")
fmt = MODEL_TYPES[model_type]()
downscale_ratio, channels = fmt.spacial_downscale_ratio, fmt.latent_channels
latent_width, latent_height = width // downscale_ratio, height // downscale_ratio
latent = build_latent(kwargs.get("seed"), kwargs.get("batch_size"), channels, latent_width, latent_height)
return {"samples": latent}, width, height
def build(**kwargs):
samples, width, height = create_empty_latent(**kwargs)
seed, noise_seed = kwargs.get("seed"), kwargs.get("noise_seed")
noise = RandomNoise(noise_seed)
multiplier = int(kwargs.get("clip_multiplier").replace('x', ''))
clip_width, clip_height = (width * multiplier, height * multiplier,)
return {
"latent": samples,
"noise": noise,
"seed": seed,
"noise_seed": noise_seed,
"megapixels": kwargs.get("megapixels"),
"width": width,
"height": height,
"target_width": clip_width,
"target_height": clip_height,
}
class TT_LatentFactoryNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
def define_schema(cls) -> io.Schema:
model_types = list(MODEL_TYPES.keys())
return io.Schema(
node_id="TT_LatentFactoryNode",
display_name="TT Latent Factory",
category=CATEGORY,
description="",
inputs=[
IO.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
IO.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True),
IO.Combo.Input("aspect_ratio", options=ASPECT_RATIOS),
IO.Combo.Input("megapixels", options=CommonTypes.MEGAPIXELS),
IO.Combo.Input("orientation", options=ORIENTATIONS),
IO.Combo.Input("model_type", options=MODEL_TYPES),
IO.Int.Input("batch_size", default=1, min=1, max=64, advanced=True),
IO.Combo.Input("clip_multiplier", options=CLIP_MULTIPLIERS, advanced=True),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True),
io.Combo.Input("aspect_ratio", options=ASPECT_RATIOS),
io.Combo.Input("megapixels", options=MEGAPIXELS),
io.Combo.Input("orientation", options=ORIENTATIONS),
io.Combo.Input("model_type", options=model_types),
io.Int.Input("batch_size", default=1, min=1, max=64, advanced=True),
io.Combo.Input("clip_multiplier", options=CLIP_MULTIPLIERS, advanced=True),
],
outputs=[
IO.Latent.Output(display_name="LATENT"),
IO.Noise.Output(display_name="RND_NOISE"),
IO.Int.Output(display_name="SEED"),
IO.Int.Output(display_name="NOISE_SEED"),
IO.String.Output(display_name="MEGAPIXELS"),
IO.Int.Output(display_name="WIDTH"),
IO.Int.Output(display_name="HEIGHT"),
IO.Int.Output(display_name="TARGET_WIDTH"),
IO.Int.Output(display_name="TARGET_HEIGHT"),
io.Latent.Output(display_name="LATENT"),
io.Noise.Output(display_name="RND_NOISE"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="NOISE_SEED"),
io.String.Output(display_name="MEGAPIXELS"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
io.Int.Output(display_name="TARGET_WIDTH"),
io.Int.Output(display_name="TARGET_HEIGHT"),
]
)
LATENT_DIMENSION_STEP = 64
@classmethod
def calculate_dimensions(cls, total_pixels, ratio_w, ratio_h) -> tuple[int, int]:
height = int(math.sqrt(total_pixels * ratio_h / ratio_w))
width = int(height * ratio_w / ratio_h)
width = round(width / cls.LATENT_DIMENSION_STEP) * cls.LATENT_DIMENSION_STEP
height = round(height / cls.LATENT_DIMENSION_STEP) * cls.LATENT_DIMENSION_STEP
def execute(cls, **kwargs) -> io.NodeOutput:
args = build(**kwargs)
return width, height,
return io.NodeOutput(*args.values())
class TT_LatentFactoryByModelNode(io.ComfyNode):
@classmethod
def build_latent(cls, seed, batch_size, channels, latent_width, latent_height) -> torch.Tensor:
generator = torch.Generator().manual_seed(seed)
latent = torch.randn(
batch_size,
channels,
latent_height,
latent_width,
generator=generator
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_LatentFactoryByModelNode",
display_name="TT Latent Factory (By Model)",
category=CATEGORY,
description="",
inputs=[
io.Model.Input("model"),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True),
io.Combo.Input("aspect_ratio", options=ASPECT_RATIOS),
io.Combo.Input("megapixels", options=MEGAPIXELS),
io.Combo.Input("orientation", options=ORIENTATIONS),
io.Int.Input("batch_size", default=1, min=1, max=64, advanced=True),
io.Combo.Input("clip_multiplier", options=CLIP_MULTIPLIERS, advanced=True),
],
outputs=[
io.Model.Output("MODEL"),
io.Latent.Output(display_name="LATENT"),
io.Noise.Output(display_name="RND_NOISE"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="NOISE_SEED"),
io.String.Output(display_name="MEGAPIXELS"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
io.Int.Output(display_name="TARGET_WIDTH"),
io.Int.Output(display_name="TARGET_HEIGHT"),
]
)
return latent
@classmethod
def create_empty_latent(cls, **kwargs) -> tuple[dict[str, Any], int, int]:
total_pixels = int(float(kwargs.get("megapixels").split()[0]) * 1_000_000)
ratio_parts = kwargs.get("aspect_ratio").split(':')
ratio_w, ratio_h = (
(int(ratio_parts[0]), int(ratio_parts[1]))
if kwargs.get("orientation") == "landscape"
else (int(ratio_parts[1]), int(ratio_parts[0]))
def execute(cls, **kwargs) -> io.NodeOutput:
args = build(**kwargs)
args = {"model": kwargs.get("model"), **args}
return io.NodeOutput(*args.values())
def set_latent_mask(latent, mask):
samples = latent
samples["noise_mask"] = mask.unsqueeze(0) if mask.ndim == 2 else mask
return samples
def scale_latent(**kwargs):
samples = kwargs.get("latent")["samples"]
orig_height, orig_width = samples.shape[2:]
scale_factor = float(kwargs.get("scale_factor").replace("x", ""))
final_width, final_height = round(orig_width * scale_factor), round(orig_height * scale_factor)
scaled = utils.common_upscale(
samples,
final_width,
final_height, kwargs.get("scale_method"),
"disabled"
)
return {"samples": scaled}
def rotate_latent(latent, turns):
samples = latent["samples"]
return {"samples": torch.rot90(samples, k=turns, dims=[3, 2])}
def flip_latent(latent, axis):
samples = latent["samples"]
return {"samples": torch.flip(samples, dims=[2 if axis == "x" else 3])}
class TT_LatentMultiTransformNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_LatentMultiTransformNode",
display_name="TT Latent MultiTransform",
category=CATEGORY,
description="",
inputs=[
io.Latent.Input("latent"),
io.Mask.Input("mask", optional=True),
io.Boolean.Input("scale_latent", default=True, label_on="Scale", label_off="Skip"),
io.Combo.Input("scale_factor", options=SCALE_FACTORS, default="1x"),
io.Combo.Input("scale_method", options=SCALE_METHODS, default="nearest-exact"),
io.Boolean.Input("rotate_latent", default=True, label_on="Rotate", label_off="Skip"),
io.Combo.Input("rotate_angle", options=ROTATE_ANGLES),
io.Boolean.Input("flip_latent", default=True, label_on="Flip", label_off="Skip"),
io.Combo.Input("flip_direction", options=["horizontal", "vertical"]),
],
outputs=[
io.Latent.Output("LATENT"),
]
)
width, height = cls.calculate_dimensions(total_pixels, ratio_w, ratio_h)
match kwargs.get("model_type"):
case "FLUX1.D":
scale_factor, channels = 8, 16
case "FLUX2.D":
scale_factor, channels = 16, 128
case "SDXL":
scale_factor, channels = 8, 4
latent_width, latent_height = (width // scale_factor, height // scale_factor,)
latent = cls.build_latent(kwargs.get("seed"), kwargs.get("batch_size"), channels, latent_width, latent_height)
return {"samples": latent}, width, height,
@classmethod
def execute(cls, **kwargs) -> IO.NodeOutput:
samples, width, height = cls.create_empty_latent(**kwargs)
seed, noise_seed = kwargs.get("seed"), kwargs.get("noise_seed")
noise = RandomNoise(noise_seed)
multiplier = int(kwargs.get("clip_multiplier").replace('x', ''))
clip_width, clip_height = (width * multiplier, height * multiplier,)
def execute(cls, **kwargs) -> io.NodeOutput:
latent = kwargs.get("latent").copy()
args = {
"latent": samples,
"noise": noise,
"seed": seed,
"noise_seed": noise_seed,
"megapixels": kwargs.get("megapixels"),
"width": width,
"height": height,
"target_width": clip_width,
"target_height": clip_height,
}
mask = kwargs.get("mask")
if mask is not None:
latent = set_latent_mask(latent, mask)
return IO.NodeOutput(*args.values())
if kwargs.get("scale_latent"):
latent = scale_latent(**kwargs)
if kwargs.get("rotate_latent"):
turns = int(kwargs.get("rotate_angle").replace("°", "")) // 90
latent = rotate_latent(latent, turns)
if kwargs.get("flip_latent"):
axis = "x" if kwargs.get("flip_direction") == "vertical" else "y"
latent = flip_latent(latent, axis)
return io.NodeOutput(latent)
class TT_LatentMultiTransformOnPixelSpaceNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_LatentMultiTransformOnPixelSpaceNode",
display_name="TT Latent MultiTransform (On Pixel Space)",
category=CATEGORY,
description="",
inputs=[
io.Vae.Input("vae"),
io.Latent.Input("latent"),
io.Mask.Input("mask", optional=True),
io.Boolean.Input("scale_latent", default=True, label_on="Scale", label_off="Skip"),
io.Combo.Input("scale_factor", options=SCALE_FACTORS, default="1x"),
io.Combo.Input("scale_method", options=SCALE_METHODS, default="nearest-exact"),
io.Boolean.Input("rotate_latent", default=True, label_on="Rotate", label_off="Skip"),
io.Combo.Input("rotate_angle", options=ROTATE_ANGLES),
io.Boolean.Input("flip_latent", default=True, label_on="Flip", label_off="Skip"),
io.Combo.Input("flip_direction", options=["horizontal", "vertical"]),
],
outputs=[
io.Latent.Output("LATENT"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
latent = kwargs.get("latent").copy()
vae = kwargs.get("vae")
pixels = vae_decode(latent, vae)
orig_height, orig_width = pixels.shape[1:3]
if kwargs.get("scale_latent"):
scale_factor = float(kwargs.get("scale_factor").replace("x", ""))
pixels = resize_image(
pixels,
int(orig_width * scale_factor),
int(orig_height * scale_factor),
kwargs.get("scale_method"),
)
if kwargs.get("rotate_latent"):
turns = int(kwargs.get("rotate_angle").replace("°", "")) // 90
pixels = rotate_image(pixels, turns)
if kwargs.get("flip_latent"):
axis = "x" if kwargs.get("flip_direction") == "vertical" else "y"
pixels = flip_image(pixels, axis)
latent = vae_encode(pixels, vae)
mask = kwargs.get("mask")
if mask is not None:
latent = set_latent_mask(latent, mask)
return io.NodeOutput(latent)
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class ContextExtension(ComfyExtension):
class LatentNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_LatentFactoryNode,
TT_LatentFactoryByModelNode,
TT_LatentMultiTransformNode,
TT_LatentMultiTransformOnPixelSpaceNode,
]
async def comfy_entrypoint() -> ContextExtension:
return ContextExtension()
async def comfy_entrypoint() -> LatentNodesExtension:
return LatentNodesExtension()
# ==============================================================================
@@ -151,4 +344,7 @@ async def comfy_entrypoint() -> ContextExtension:
__all__ = [
"TT_LatentFactoryNode",
"TT_LatentFactoryByModelNode",
"TT_LatentMultiTransformNode",
"TT_LatentMultiTransformOnPixelSpaceNode",
]
+809
View File
@@ -1 +1,810 @@
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# Contains slightly modified code by City96
# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# Modified by TenserTensor
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import collections
import inspect
import os
from typing import override
import torch
from torch import bfloat16, float16, float32, float8_e4m3fn, float8_e5m2, device, tensor
import folder_paths
from comfy import sd, model_management, utils, model_sampling, model_patcher, lora, float
from comfy_api.latest import io, ComfyExtension
from nodes import VAELoader, MAX_RESOLUTION
from .gguf.dequant import is_quantized, is_torch_compatible
from .gguf.loader import gguf_sd_loader, gguf_clip_loader
from .gguf.ops import GGMLOps, move_patch_to_device
CATEGORY = "TenserTensor/Loaders"
TORCH_DEVICES = ["default", "cpu"]
TORCH_DEVICE_CPU = "cpu"
DTYPES = {"bfloat16": bfloat16, "float16": float16, "float32": float32}
VIDEO_TAES = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5"]
IMAGE_TAES = ["taesd", "taesdxl", "taesd3", "taef1"]
MIN_SAMPLING_RES = 256
MAX_SAMPLING_RES = 4096
def get_checkpoint_files():
return folder_paths.get_filename_list("checkpoints")
def get_diffusion_models_files():
return folder_paths.get_filename_list("diffusion_models")
def get_gguf_diffusion_models_files():
update_folder_names_and_paths("diffusion_models_gguf", "diffusion_models", {".gguf"})
return [f for f in folder_paths.get_filename_list("diffusion_models_gguf")]
def get_text_encoder_files():
return folder_paths.get_filename_list("text_encoders")
def get_gguf_text_encoder_files():
update_folder_names_and_paths("text_encoders_gguf", "text_encoders", {'.gguf', '.pt', '.pth', '.safetensors'})
return [x for x in folder_paths.get_filename_list("text_encoders_gguf")]
def get_vae_files():
return VAELoader.vae_list(VAELoader)
def get_lora_files():
return ["None"] + folder_paths.get_filename_list("loras")
def load_checkpoint(checkpoint):
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", checkpoint)
return sd.load_checkpoint_guess_config(
ckpt_path, output_vae=False, output_clip=False,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)[0]
def load_sdxl_clip(clip_l, clip_g, clip_device):
clip_type = getattr(sd.CLIPType, "SDXL", sd.CLIPType.STABLE_DIFFUSION)
clip_l_path = folder_paths.get_full_path_or_raise("text_encoders", clip_l)
clip_g_path = folder_paths.get_full_path_or_raise("text_encoders", clip_g)
model_options = {}
if clip_device == TORCH_DEVICE_CPU:
model_options["load_device"] = model_options["offload_device"] = device(TORCH_DEVICE_CPU)
return sd.load_clip(
ckpt_paths=[clip_l_path, clip_g_path],
embedding_directory=folder_paths.get_folder_paths("embeddings"),
clip_type=clip_type,
model_options=model_options
)
def load_vae(vae_name, vae_device="default", vae_dtype="bfloat16"):
dtype = DTYPES[vae_dtype]
torch_device = model_management.get_torch_device() if vae_device == "default" else device(vae_device)
metadata = None
if vae_name == "pixel_space":
state_dict = {"pixel_space_vae": tensor(1.0)}
elif vae_name in IMAGE_TAES:
state_dict = VAELoader.load_taesd(vae_name)
else:
vae_path = (
folder_paths.get_full_path_or_raise("vae_approx", vae_name)
if os.path.splitext(vae_name)[0] in VIDEO_TAES
else folder_paths.get_full_path_or_raise("vae", vae_name)
)
state_dict, metadata = utils.load_torch_file(vae_path, return_metadata=True)
vae = sd.VAE(sd=state_dict, device=torch_device, dtype=dtype, metadata=metadata)
vae.throw_exception_if_invalid()
return vae
def load_sdxl_pipeline(cls, apply_loras, **kwargs):
model = load_checkpoint(kwargs.get("primary_ckpt"))
secondary_ckpt = kwargs.get("secondary_ckpt")
if secondary_ckpt != "None":
primary_weight = kwargs.get("primary_weight")
secondary_model = load_checkpoint(secondary_ckpt)
model = model.clone()
key_patches = secondary_model.get_key_patches("diffusion_model.")
for key in key_patches:
model.add_patches({key: key_patches[key]}, 1.0 - primary_weight, primary_weight)
clip_l, clip_g, clip_device = (
kwargs.get("clip_l"),
kwargs.get("clip_g"),
kwargs.get("clip_device"),
)
clip = load_sdxl_clip(clip_l, clip_g, clip_device)
if apply_loras:
loras = [
(kwargs.get("lora_name_1"), kwargs.get("strength_1"), 'loaded_lora_1'),
(kwargs.get("lora_name_2"), kwargs.get("strength_2"), 'loaded_lora_2'),
(kwargs.get("lora_name_3"), kwargs.get("strength_3"), 'loaded_lora_3'),
(kwargs.get("lora_name_4"), kwargs.get("strength_4"), 'loaded_lora_4'),
]
for name, strength, attr in loras:
if name != "None" and strength != 0:
cached = getattr(cls, attr)
model, clip, lora = apply_lora(cached, model, clip, name, strength)
setattr(cls, attr, lora)
vae = load_vae(kwargs.get("vae_name"), kwargs.get("vae_device"), kwargs.get("vae_dtype"))
return model, clip, vae
class TT_SdxlModelsLoaderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_SdxlModelsLoaderNode",
display_name="TT SDXL Models Loader",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("primary_ckpt", options=get_checkpoint_files()),
io.Combo.Input("secondary_ckpt", options=["None"] + get_checkpoint_files()),
io.Float.Input("primary_weight", default=1.0, min=0.0, max=1.0, step=0.01),
io.Combo.Input("clip_l", options=get_text_encoder_files()),
io.Combo.Input("clip_g", options=get_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default"),
io.Combo.Input("vae_name", options=get_vae_files()),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
model, clip, vae = load_sdxl_pipeline(cls, apply_loras=False, **kwargs)
return io.NodeOutput(model, clip, vae)
def apply_lora(loaded_lora, model, clip, lora_name, strength):
if loaded_lora is None:
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = utils.load_torch_file(lora_path, safe_load=True)
else:
lora = loaded_lora
patched_model, patched_clip = sd.load_lora_for_models(model, clip, lora, strength, strength)
return (patched_model, patched_clip, lora)
class TT_SdxlModelsLoaderAdvancedNode(io.ComfyNode):
loaded_lora_1 = None
loaded_lora_2 = None
loaded_lora_3 = None
loaded_lora_4 = None
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_SdxlModelsLoaderAdvancedNode",
display_name="TT SDXL Models Loader (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("primary_ckpt", options=get_checkpoint_files()),
io.Combo.Input("secondary_ckpt", options=["None"] + get_checkpoint_files()),
io.Float.Input("primary_weight", default=1.0, min=0.0, max=1.0, step=0.01),
io.Combo.Input("clip_l", options=get_text_encoder_files()),
io.Combo.Input("clip_g", options=get_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default"),
io.Combo.Input("lora_name_1", options=get_lora_files()),
io.Float.Input("strength_1", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_2", options=get_lora_files()),
io.Float.Input("strength_2", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_3", options=get_lora_files()),
io.Float.Input("strength_3", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_4", options=get_lora_files()),
io.Float.Input("strength_4", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("vae_name", options=get_vae_files()),
io.Combo.Input("vae_device", options=TORCH_DEVICES, default="default"),
io.Combo.Input("vae_dtype", options=list(DTYPES.keys())),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
model, clip, vae = load_sdxl_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
def load_diffusion_model(diffusion_model, dtype="default"):
unet_path = folder_paths.get_full_path_or_raise("diffusion_models", diffusion_model)
model_options = {}
match dtype:
case "fp8_e4m3fn":
model_options["dtype"] = float8_e4m3fn
case "fp8_e4m3fn_fast":
model_options["dtype"] = float8_e4m3fn
model_options["fp8_optimizations"] = True
case "fp8_e5m2":
model_options["dtype"] = float8_e5m2
case _:
pass
return sd.load_diffusion_model(unet_path, model_options=model_options)
def load_flux_clip(clip_l, t5xxl, device):
clip_type = sd.CLIPType.FLUX
clip_l_path = folder_paths.get_full_path_or_raise("text_encoders", clip_l)
t5xxl_path = folder_paths.get_full_path_or_raise("text_encoders", t5xxl)
model_options = {}
if device == TORCH_DEVICE_CPU:
model_options["load_device"] = model_options["offload_device"] = device(TORCH_DEVICE_CPU)
return sd.load_clip(
ckpt_paths=[clip_l_path, t5xxl_path],
embedding_directory=folder_paths.get_folder_paths("embeddings"),
clip_type=clip_type,
model_options=model_options
)
class ModelSamplingFluxAdvanced(model_sampling.ModelSamplingFlux, model_sampling.CONST):
pass
def patch_flux_sampling(model, base_sampling_shift, max_sampling_shift, sampling_width, sampling_height):
tmodel = model.clone()
slope = (max_sampling_shift - base_sampling_shift) / (MAX_SAMPLING_RES - MIN_SAMPLING_RES)
intercept = base_sampling_shift - slope * MIN_SAMPLING_RES
current_res = (sampling_width * sampling_height / (8 * 8 * 2 * 2))
shift = current_res * slope + intercept
model_sampling = ModelSamplingFluxAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift)
tmodel.add_object_patch("model_sampling", model_sampling)
return tmodel
def load_flux_pipeline(cls, apply_loras=False, **kwargs):
model = load_diffusion_model(kwargs.get("diffusion_model"))
if kwargs.get("apply_sampling"):
args = {
"model": model,
"base_sampling_shift": kwargs.get("base_sampling_shift"),
"max_sampling_shift": kwargs.get("max_sampling_shift"),
"sampling_width": kwargs.get("sampling_width"),
"sampling_height": kwargs.get("sampling_height"),
}
model = patch_flux_sampling(**args)
clip = load_flux_clip(
kwargs.get("clip_l"),
kwargs.get("t5xxl"),
kwargs.get("clip_device"),
)
if apply_loras:
loras = [
(kwargs.get("lora_name_1"), kwargs.get("strength_1"), 'loaded_lora_1'),
(kwargs.get("lora_name_2"), kwargs.get("strength_2"), 'loaded_lora_2'),
(kwargs.get("lora_name_3"), kwargs.get("strength_3"), 'loaded_lora_3'),
(kwargs.get("lora_name_4"), kwargs.get("strength_4"), 'loaded_lora_4'),
]
for name, strength, attr in loras:
if name != "None" and strength != 0:
cached = getattr(cls, attr)
model, clip, lora = apply_lora(cached, model, clip, name, strength)
setattr(cls, attr, lora)
vae = load_vae(kwargs.get("vae_name"), kwargs.get("vae_device"), kwargs.get("vae_dtype"))
return (model, clip, vae,)
class TT_FluxModelsLoaderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_FluxModelsLoaderNode",
display_name="TT FLUX Models Loader",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_diffusion_models_files()),
io.Combo.Input("clip_l", options=get_text_encoder_files()),
io.Combo.Input("t5xxl", options=get_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default"),
io.Combo.Input("vae_name", options=get_vae_files()),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
model, clip, vae = load_flux_pipeline(cls, apply_loras=False, **kwargs)
return io.NodeOutput(model, clip, vae)
class TT_FluxModelsLoaderAdvancedNode(io.ComfyNode):
loaded_lora_1 = None
loaded_lora_2 = None
loaded_lora_3 = None
loaded_lora_4 = None
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_FluxModelsLoaderAdvancedNode",
display_name="TT FLUX Models Loader (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_diffusion_models_files()),
io.Boolean.Input("apply_sampling", default=True, label_on="Flux Shift", label_off="No Shift"),
io.Float.Input("base_sampling_shift", default=0.5, min=0.0, max=100.0, step=0.01, advanced=True),
io.Float.Input("max_sampling_shift", default=1.15, min=0.0, max=100.0, step=0.01, advanced=True),
io.Int.Input("sampling_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Int.Input("sampling_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Combo.Input("clip_l", options=get_text_encoder_files()),
io.Combo.Input("t5xxl", options=get_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("lora_name_1", options=get_lora_files()),
io.Float.Input("strength_1", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_2", options=get_lora_files()),
io.Float.Input("strength_2", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_3", options=get_lora_files()),
io.Float.Input("strength_3", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_4", options=get_lora_files()),
io.Float.Input("strength_4", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("vae_name", options=get_vae_files()),
io.Combo.Input("vae_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("vae_dtype", options=list(DTYPES.keys()), advanced=True),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
model, clip, vae = load_flux_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
def update_folder_names_and_paths(folder, base_folder=None, exts=None):
if folder in folder_paths.folder_names_and_paths:
return
path = os.path.join(folder_paths.models_dir, folder)
os.makedirs(path, exist_ok=True)
paths = [path]
base_paths, base_exts = folder_paths.folder_names_and_paths.get(base_folder, ([], {})) if base_folder else ([], {})
_exts = exts if exts is not None else base_exts
folder_paths.folder_names_and_paths[folder] = (paths + base_paths, _exts)
WeightBackup = collections.namedtuple('WeightBackup', ['weight', 'inplace_update'])
class GGUFModelPatcher(model_patcher.ModelPatcher):
patch_on_device = False
def patch_weight_to_device(self, key, device_to=None, inplace_update=False):
if key not in self.patches:
return
weight = utils.get_attr(self.model, key)
patches = self.patches[key]
if is_quantized(weight):
out_weight = weight.to(device_to)
patches = move_patch_to_device(patches, self.load_device if self.patch_on_device else self.offload_device)
# TODO: do we ever have legitimate duplicate patches? (i.e. patch on top of patched weight)
out_weight.patches = [(patches, key)]
else:
inplace_update = self.weight_inplace_update or inplace_update
if key not in self.backup:
self.backup[key] = WeightBackup(
weight.to(device=self.offload_device, copy=inplace_update), inplace_update
)
if device_to is not None:
temp_weight = model_management.cast_to_device(weight, device_to, torch.float32, copy=True)
else:
temp_weight = weight.to(torch.float32, copy=True)
out_weight = lora.calculate_weight(patches, temp_weight, key)
out_weight = float.stochastic_rounding(out_weight, weight.dtype)
if inplace_update:
utils.copy_to_param(self.model, key, out_weight)
else:
utils.set_attr_param(self.model, key, out_weight)
def unpatch_model(self, device_to=None, unpatch_weights=True):
if unpatch_weights:
for p in self.model.parameters():
if is_torch_compatible(p):
continue
patches = getattr(p, "patches", [])
if len(patches) > 0:
p.patches = []
# TODO: Find another way to not unload after patches
return super().unpatch_model(device_to=device_to, unpatch_weights=unpatch_weights)
def get_dtype(dtype):
match dtype:
case None | "default":
return None
case "target":
return dtype
case _:
return getattr(torch, dtype)
def load_clip_data(ckpt_paths):
clip_data = []
for path in ckpt_paths:
if path.endswith(".gguf"):
state_dict = gguf_clip_loader(path)
else:
state_dict = utils.load_torch_file(path, safe_load=True)
# NOTE: Scaled FP8 would require different custom ops, but only one can be active
if "scaled_fp8" in state_dict:
raise NotImplementedError(
f"Mixing scaled FP8 with GGUF is not supported! Use regular CLIP loader or switch model(s)\n({path})")
clip_data.append(state_dict)
return clip_data
def load_flux_clip_patcher(clip_name, type="flux2"):
clip_path = folder_paths.get_full_path("text_encoders_gguf", clip_name)
clip_type = getattr(sd.CLIPType, type.upper(), sd.CLIPType.FLUX2)
clip_data = load_clip_data([clip_path])
clip = sd.load_text_encoder_state_dicts(
clip_type=clip_type,
state_dicts=clip_data,
model_options={
"custom_operations": GGMLOps,
"initial_device": model_management.text_encoder_offload_device()
},
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
clip.patcher = GGUFModelPatcher.clone(clip.patcher)
return clip
def load_sd3_clip_patcher(clip_l, clip_g, t5xxl, type="sd3"):
clip_l_path = folder_paths.get_full_path("text_encoders_gguf", clip_l)
clip_g_path = folder_paths.get_full_path("text_encoders_gguf", clip_g)
t5xxl_path = folder_paths.get_full_path("text_encoders_gguf", t5xxl)
clip_type = getattr(sd.CLIPType, type.upper(), sd.CLIPType.SD3)
clip_data = load_clip_data([clip_l_path, clip_g_path, t5xxl_path])
clip = sd.load_text_encoder_state_dicts(
clip_type=clip_type,
state_dicts=clip_data,
model_options={
"custom_operations": GGMLOps,
"initial_device": model_management.text_encoder_offload_device()
},
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
clip.patcher = GGUFModelPatcher.clone(clip.patcher)
return clip
def apply_bypass_lora_for_models(loaded_lora, model, clip, lora_name, strength):
if loaded_lora == None:
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = utils.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,)
def load_gguf_pipeline(cls, **kwargs):
ops = GGMLOps()
ops.Linear.dequant_dtype = get_dtype(kwargs.get("dequant_dtype", None))
ops.Linear.patch_dtype = get_dtype(kwargs.get("patch_dtype", None))
gguf_full_path = folder_paths.get_full_path("diffusion_models_gguf", kwargs.get("diffusion_model"))
state_dict, extra = gguf_sd_loader(gguf_full_path)
args = {}
valid_params = inspect.signature(sd.load_diffusion_model_state_dict).parameters
if "metadata" in valid_params:
args["metadata"] = extra.get("metadata", {})
model = sd.load_diffusion_model_state_dict(state_dict, model_options={"custom_operations": ops}, **args, )
if model is None:
raise RuntimeError("ERROR: Could not detect model type of: {}".format(gguf_full_path))
model = GGUFModelPatcher.clone(model)
model.patch_on_device = kwargs.get("patch_on_device", None)
apply_sampling = kwargs.get("apply_sampling")
if apply_sampling:
args = {
"model": model,
"base_sampling_shift": kwargs.get("base_sampling_shift"),
"max_sampling_shift": kwargs.get("max_sampling_shift"),
"sampling_width": kwargs.get("sampling_width"),
"sampling_height": kwargs.get("sampling_height"),
}
model = patch_flux_sampling(**args)
clip_name = kwargs.get("clip")
if clip_name:
clip = load_flux_clip_patcher(kwargs.get("clip"), kwargs.get("clip_type", "flux2"))
else:
clip_l, clip_g, t5xxl = kwargs.get("clip_l"), kwargs.get("clip_g"), kwargs.get("t5xxl")
clip = load_sd3_clip_patcher(clip_l, clip_g, t5xxl)
if kwargs.get("apply_bypass_lora"):
loras = [
(kwargs.get("lora_name_1"), kwargs.get("strength_1"), 'loaded_lora_1'),
(kwargs.get("lora_name_2"), kwargs.get("strength_2"), 'loaded_lora_2'),
(kwargs.get("lora_name_3"), kwargs.get("strength_3"), 'loaded_lora_3'),
(kwargs.get("lora_name_4"), kwargs.get("strength_4"), 'loaded_lora_4'),
]
for name, strength, attr in loras:
if name != "None" and strength != 0:
cached = getattr(cls, attr)
model, clip, lora = apply_bypass_lora_for_models(cached, model, clip, name, strength)
setattr(cls, attr, lora)
vae = load_vae(
kwargs.get("vae_name"),
kwargs.get("vae_device", "default"),
kwargs.get("vae_dtype", "bfloat16"),
)
return model, clip, vae
class TT_Flux2GgufModelsLoaderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2GgufModelsLoaderNode",
display_name="TT FLUX2 GGUF Models Loader",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_gguf_diffusion_models_files()),
io.Combo.Input("clip", options=get_gguf_text_encoder_files()),
io.Combo.Input("vae_name", options=get_vae_files()),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["apply_bypass_lora"] = False
model, clip, vae = load_gguf_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
class TT_Flux2GgufModelsLoaderAdvancedNode(io.ComfyNode):
loaded_lora_1 = None
loaded_lora_2 = None
loaded_lora_3 = None
loaded_lora_4 = None
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2GgufModelsLoaderAdvancedNode",
display_name="TT FLUX2 GGUF Models Loader (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_gguf_diffusion_models_files()),
io.Combo.Input("dequant_dtype", options=list(DTYPES.keys()), default="bfloat16", advanced=True),
io.Combo.Input("patch_dtype", options=list(DTYPES.keys()), default="bfloat16", advanced=True),
io.Boolean.Input("apply_sampling", default=True, label_on="Flux Shift", label_off="No Shift"),
io.Float.Input("base_sampling_shift", default=0.5, min=0.0, max=100.0, step=0.01, advanced=True),
io.Float.Input("max_sampling_shift", default=1.15, min=0.0, max=100.0, step=0.01, advanced=True),
io.Int.Input("sampling_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Int.Input("sampling_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Combo.Input("clip", options=get_gguf_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("lora_name_1", options=get_lora_files()),
io.Float.Input("strength_1", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_2", options=get_lora_files()),
io.Float.Input("strength_2", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_3", options=get_lora_files()),
io.Float.Input("strength_3", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_4", options=get_lora_files()),
io.Float.Input("strength_4", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("vae_name", options=get_vae_files()),
io.Combo.Input("vae_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("vae_dtype", options=list(DTYPES.keys()), advanced=True),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["apply_bypass_lora"] = False
model, clip, vae = load_gguf_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
class TT_Sd35GgufModelsLoaderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35GgufModelsLoaderNode",
display_name="TT SD3.5 GGUF Models Loader",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_gguf_diffusion_models_files()),
io.Combo.Input("clip_l", options=get_gguf_text_encoder_files()),
io.Combo.Input("clip_g", options=get_gguf_text_encoder_files()),
io.Combo.Input("t5xxl", options=get_gguf_text_encoder_files()),
io.Combo.Input("vae_name", options=get_vae_files()),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["apply_bypass_lora"] = False
model, clip, vae = load_gguf_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
class TT_Sd35GgufModelsLoaderAdvancedNode(io.ComfyNode):
loaded_lora_1 = None
loaded_lora_2 = None
loaded_lora_3 = None
loaded_lora_4 = None
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35GgufModelsLoaderAdvancedNode",
display_name="TT SD3.5 GGUF Models Loader (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Combo.Input("diffusion_model", options=get_gguf_diffusion_models_files()),
io.Combo.Input("dequant_dtype", options=list(DTYPES.keys()), default="bfloat16", advanced=True),
io.Combo.Input("patch_dtype", options=list(DTYPES.keys()), default="bfloat16", advanced=True),
io.Combo.Input("clip_l", options=get_gguf_text_encoder_files()),
io.Combo.Input("clip_g", options=get_gguf_text_encoder_files()),
io.Combo.Input("t5xxl", options=get_gguf_text_encoder_files()),
io.Combo.Input("clip_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("lora_name_1", options=get_lora_files()),
io.Float.Input("strength_1", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_2", options=get_lora_files()),
io.Float.Input("strength_2", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_3", options=get_lora_files()),
io.Float.Input("strength_3", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("lora_name_4", options=get_lora_files()),
io.Float.Input("strength_4", default=1.0, min=-10.0, max=10.0, step=0.1),
io.Combo.Input("vae_name", options=get_vae_files()),
io.Combo.Input("vae_device", options=TORCH_DEVICES, default="default", advanced=True),
io.Combo.Input("vae_dtype", options=list(DTYPES.keys()), advanced=True),
],
outputs=[
io.Model.Output("MODEL"),
io.Clip.Output("CLIP"),
io.Vae.Output("VAE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["apply_bypass_lora"] = True
model, clip, vae = load_gguf_pipeline(cls, apply_loras=True, **kwargs)
return io.NodeOutput(model, clip, vae)
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class LoaderNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_SdxlModelsLoaderNode,
TT_SdxlModelsLoaderAdvancedNode,
TT_FluxModelsLoaderNode,
TT_FluxModelsLoaderAdvancedNode,
TT_Flux2GgufModelsLoaderNode,
TT_Flux2GgufModelsLoaderAdvancedNode,
TT_Sd35GgufModelsLoaderNode,
# TT_Sd35GgufModelsLoaderAdvancedNode,
]
async def comfy_entrypoint() -> LoaderNodesExtension:
return LoaderNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_SdxlModelsLoaderNode",
"TT_SdxlModelsLoaderAdvancedNode",
"TT_FluxModelsLoaderNode",
"TT_FluxModelsLoaderAdvancedNode",
"TT_Flux2GgufModelsLoaderNode",
"TT_Flux2GgufModelsLoaderAdvancedNode",
"TT_Sd35GgufModelsLoaderNode",
# "TT_Sd35GgufModelsLoaderAdvancedNode",
]
+30 -1
View File
@@ -1 +1,30 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
from comfy_api.latest import ComfyExtension, IO
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class PostproductionNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
# TT_Node,
]
async def comfy_entrypoint() -> PostproductionNodesExtension:
return PostproductionNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
# "TT_Node",
]
+176 -1
View File
@@ -1 +1,176 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
import latent_preview
from comfy import sample, model_management
from comfy_api.latest import ComfyExtension, io
from .nodes_latent import SCALE_FACTORS, SCALE_METHODS, scale_latent
from .nodes_text_encoder import SingleCondCFGGuider
CATEGORY = "TenserTensor/Sampling"
def execute_guided_sampling(**kwargs):
guider, latent, sigmas, random_noise, sampler, preview_latent = (
kwargs.get("guider"),
kwargs.get("latent"),
kwargs.get("sigmas"),
kwargs.get("random_noise"),
kwargs.get("sampler"),
kwargs.get("preview_latent"),
)
if not isinstance(guider, SingleCondCFGGuider):
guider = SingleCondCFGGuider.from_cfg_guider(guider)
model_patcher = guider.model_patcher
latent_dict = latent.copy()
latent_tensor = sample.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 = {}
if preview_latent:
latent_preview.set_preview_method("latent2rgb")
callback = latent_preview.prepare_callback(model_patcher, sigmas.shape[-1] - 1, interim_buffer)
else:
callback = None
# disable_progress_bar = not utils.PROGRESS_BAR_ENABLED
samples = guider.sample(
random_noise.generate_noise(latent),
latent_tensor,
sampler,
sigmas,
denoise_mask=noise_mask,
callback=callback,
disable_pbar=False,
seed=random_noise.seed
).to(model_management.intermediate_device())
sampled_latent = latent_dict.copy()
sampled_latent.pop("downscale_ratio_spacial", None)
sampled_latent["samples"] = samples
return sampled_latent
class TT_GuidedKSamplerNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_GuidedKSamplerNode",
display_name="TT Guided KSampler",
category=CATEGORY,
description="",
inputs=[
io.Latent.Input("latent"),
io.Guider.Input("guider"),
io.Sigmas.Input("sigmas"),
io.Sampler.Input("sampler"),
io.Noise.Input("random_noise"),
],
outputs=[
io.Latent.Output("SAMPLES")
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
samples = execute_guided_sampling(**kwargs)
return io.NodeOutput(samples)
class TT_GuidedUpscaleKSamplerNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_GuidedUpscaleKSamplerNode",
display_name="TT Guided Upscale KSampler",
category=CATEGORY,
description="",
inputs=[
io.Latent.Input("latent"),
io.Guider.Input("guider"),
io.Sigmas.Input("sigmas"),
io.Sampler.Input("sampler"),
io.Noise.Input("random_noise"),
io.Boolean.Input("scale_latent", default=False, label_on="Scale", label_off="Skip"),
io.Combo.Input("scale_factor", options=SCALE_FACTORS, default="1x", advanced=True),
io.Combo.Input("scale_method", options=SCALE_METHODS, default="bicubic", advanced=True),
],
outputs=[
io.Latent.Output("SAMPLES")
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
samples = execute_guided_sampling(**kwargs)
if kwargs.get("scale_latent"):
samples = scale_latent(**kwargs)
return io.NodeOutput(samples)
class TT_GuidedKSamplerWithPreviewNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_GuidedKSamplerWithPreviewNode",
display_name="TT Guided KSampler (With Preview)",
category=CATEGORY,
description="",
inputs=[
io.Latent.Input("latent"),
io.Guider.Input("guider"),
io.Sigmas.Input("sigmas"),
io.Sampler.Input("sampler"),
io.Noise.Input("random_noise"),
io.Boolean.Input("preview_latent", default=True, label_on="Show Peview", label_off="Only Sample"),
],
outputs=[
io.Latent.Output("SAMPLES")
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
samples = execute_guided_sampling(**kwargs)
return io.NodeOutput(samples)
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class SamplingNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_GuidedKSamplerNode,
TT_GuidedUpscaleKSamplerNode,
TT_GuidedKSamplerWithPreviewNode,
]
async def comfy_entrypoint() -> SamplingNodesExtension:
return SamplingNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_GuidedKSamplerNode",
"TT_GuidedUpscaleKSamplerNode",
"TT_GuidedKSamplerWithPreviewNode",
]
+448
View File
@@ -0,0 +1,448 @@
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
from comfy import samplers
from comfy_api.latest import io, ComfyExtension
from nodes import MAX_RESOLUTION
from .nodes_context import Context
CATEGORY = "TenserTensor/Text Encoder"
class SingleCondCFGGuider(samplers.CFGGuider):
@classmethod
def from_cfg_guider(cls, guider):
obj = cls.__new__(cls)
obj.__dict__.update(guider.__dict__)
return obj
@override
def set_conds(self, positive, negative=None):
dict = {"positive": positive}
if negative is not None:
dict["negative"] = negative
self.inner_set_conds(dict)
def get_conds(self, key="positive"):
return [[c.get("cross_attn", None), c] for c in self.original_conds[key]]
def encode_prompts_sdxl(**kwargs):
clip = kwargs.get("clip")
if clip is None:
raise ValueError("ERROR: CLIP is required for text encoder")
positive_tokens = clip.tokenize(kwargs.get("clip_g_positive"))
positive_tokens["l"] = clip.tokenize(kwargs.get("clip_l_positive"))["l"]
negative_tokens = clip.tokenize(kwargs.get("clip_g_negative"))
negative_tokens["l"] = clip.tokenize(kwargs.get("clip_l_negative"))["l"]
cond_dict = {
"aesthetic_score": kwargs.get("ascore_positive"),
"width": kwargs.get("width"),
"height": kwargs.get("height"),
"target_width": kwargs.get("target_width"),
"target_height": kwargs.get("target_height"),
}
positive = clip.encode_from_tokens_scheduled(positive_tokens, add_dict=cond_dict)
cond_dict["aesthetic_score"] = kwargs.get("ascore_negative")
negative = clip.encode_from_tokens_scheduled(negative_tokens, add_dict=cond_dict)
return positive, negative
class TT_SdxlClipTextEncoderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_SdxlClipTextEncoderNode",
display_name="TT SDXL CLIP Text Encoder",
category=CATEGORY,
description="",
inputs=[
io.Clip.Input("clip"),
io.String.Input("clip_l_positive", multiline=True, placeholder="CLIP_L Positive", dynamic_prompts=True),
io.String.Input("clip_g_positive", multiline=True, placeholder="CLIP_G Positive", dynamic_prompts=True),
io.String.Input("clip_l_negative", multiline=True, placeholder="CLIP_L Negative", dynamic_prompts=True),
io.String.Input("clip_g_negative", multiline=True, placeholder="CLIP_G Negative", dynamic_prompts=True),
io.Float.Input("ascore_positive", default=9.0, min=0.0, max=100.0, step=0.1),
io.Float.Input("ascore_negative", default=6.0, min=0.0, max=100.0, step=0.1),
io.Int.Input("width", default=512, min=64, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=512, min=64, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_width", default=512, min=64, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_height", default=512, min=64, max=MAX_RESOLUTION, step=8),
],
outputs=[
io.Conditioning.Output("POSITIVE"),
io.Conditioning.Output("NEGATIVE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
positive, negative = encode_prompts_sdxl(**kwargs)
return io.NodeOutput(positive, negative)
class TT_SdxlClipTextEncoderContextNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_SdxlClipTextEncoderContextNode",
display_name="TT SDXL CLIP Text Encoder (Context)",
category=CATEGORY,
description="",
inputs=[
Context.Input("context")
],
outputs=[
Context.Output("CONTEXT"),
io.Conditioning.Output("POSITIVE"),
io.Conditioning.Output("NEGATIVE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
context = kwargs.get("context")
args = {}
for key in (
"clip", "clip_l_positive", "clip_g_positive", "clip_l_negative", "clip_g_negative",
"ascore_positive", "ascore_negative", "width", "height", "target_width", "target_height",
):
args[key] = context.get_attr(key)
positive, negative = encode_prompts_sdxl(**args)
context.set_attr("positive", positive)
context.set_attr("negative", negative)
return io.NodeOutput(context, positive, negative)
def encode_prompts_flux(**kwargs):
clip = kwargs.get("clip")
if clip is None:
raise ValueError("ERROR: CLIP is required for text encoder")
positive_tokens = clip.tokenize(kwargs.get("clip_l_positive"))
positive_tokens["t5xxl"] = clip.tokenize(kwargs.get("t5xxl_positive"))["t5xxl"]
negative_tokens = clip.tokenize(kwargs.get("clip_l_negative"))
negative_tokens["t5xxl"] = clip.tokenize(kwargs.get("t5xxl_negative"))["t5xxl"]
guidance = kwargs.get("guidance")
positive = clip.encode_from_tokens_scheduled(positive_tokens, add_dict={"guidance": guidance, })
negative = clip.encode_from_tokens_scheduled(negative_tokens, add_dict={"guidance": guidance, })
return positive, negative
class TT_Flux1ClipTextEncoderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux1ClipTextEncoderNode",
display_name="TT FLUX1 CLIP Text Encoder",
category=CATEGORY,
description="",
inputs=[
io.Clip.Input("clip"),
io.String.Input("clip_l_positive", multiline=True, placeholder="CLIP_L Positive", dynamic_prompts=True),
io.String.Input("t5xxl_positive", multiline=True, placeholder="T5XXL Positive", dynamic_prompts=True),
io.String.Input("clip_l_negative", multiline=True, placeholder="CLIP_L Negative", dynamic_prompts=True),
io.String.Input("t5xxl_negative", multiline=True, placeholder="T5XXL Negative", dynamic_prompts=True),
io.Float.Input("guidance", default=9.0, min=0.0, max=100.0, step=0.1),
],
outputs=[
io.Conditioning.Output("POSITIVE"),
io.Conditioning.Output("NEGATIVE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
positive, negative = encode_prompts_flux(**kwargs)
return io.NodeOutput(positive, negative)
class TT_Flux1ClipTextEncoderContextNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux1ClipTextEncoderContextNode",
display_name="TT FLUX1 CLIP Text Encoder (Context)",
category=CATEGORY,
description="",
inputs=[
Context.Input("context")
],
outputs=[
Context.Output("CONTEXT"),
io.Conditioning.Output("POSITIVE"),
io.Conditioning.Output("NEGATIVE"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
context = kwargs.get("context")
args = {}
for key in ("clip", "clip_l_positive", "t5xxl_positive", "clip_l_negative", "t5xxl_negative", "guidance",):
args[key] = context.get_attr(key)
positive, negative = encode_prompts_flux(**args)
context.set_attr("positive", positive)
context.set_attr("negative", negative)
return io.NodeOutput(context, positive, negative)
def encode_prompts_flux2(**kwargs):
clip = kwargs.get("clip")
model = kwargs.get("model")
if model is None:
raise ValueError("ERROR: MODEL is required for text encoder")
if clip is None:
raise ValueError("ERROR: CLIP is required for text encoder")
prompt, lora_triggers, guidance = (
kwargs.get("prompt"),
kwargs.get("lora_triggers"),
kwargs.get("guidance"),
)
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 = SingleCondCFGGuider(model)
guider.set_conds(conditioning)
guider.set_cfg(kwargs.get("cfg"))
return guider
class TT_Flux2TextEncoderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2TextEncoderNode",
display_name="TT FLUX2 Text Encoder",
category=CATEGORY,
description="",
inputs=[
io.Model.Input("model"),
io.Clip.Input("clip"),
io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1),
io.String.Input("prompt", multiline=True, placeholder="Prompt", dynamic_prompts=True),
io.String.Input("lora_triggers", multiline=True, placeholder="LoRA Triggers", dynamic_prompts=True),
io.Float.Input("guidance", default=3.5, min=1.0, max=10.0, step=0.1)
],
outputs=[
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
guider = encode_prompts_flux2(**kwargs)
return io.NodeOutput(guider)
class TT_Flux2TextEncoderContextNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2TextEncoderContextNode",
display_name="TT FLUX2 Text Encoder (Context)",
category=CATEGORY,
description="",
inputs=[
Context.Input("context")
],
outputs=[
Context.Output("CONTEXT"),
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
context = kwargs.get("context")
args = {
"model": context.get_attr("model"),
"clip": context.get_attr("clip"),
"prompt": context.get_attr("prompt"),
"lora_triggers": context.get_attr("lora_triggers"),
"guidance": context.get_attr("guidance"),
}
guider = encode_prompts_flux2(**args)
context.set_attr("guider", guider)
return io.NodeOutput(context, guider)
def encode_prompts_sd35(**kwargs):
model = kwargs.get("model")
if model is None:
raise ValueError("ERROR: MODEL is required for text encoder")
clip = kwargs.get("clip")
if clip is None:
raise ValueError("ERROR: CLIP is required for text encoder")
lora_triggers, t5xxl_positive = kwargs.get("lora_triggers"), kwargs.get("t5xxl_positive")
if lora_triggers:
t5xxl_positive = f"{t5xxl_positive}, {lora_triggers}"
positive_tokens = clip.tokenize(kwargs.get("clip_g_positive"))
positive_tokens["l"] = clip.tokenize(kwargs.get("clip_l_positive"))["l"]
positive_tokens["t5xxl"] = clip.tokenize(t5xxl_positive)["t5xxl"]
negative_tokens = clip.tokenize(kwargs.get("clip_g_negative"))
negative_tokens["l"] = clip.tokenize(kwargs.get("clip_l_negative"))["l"]
negative_tokens["t5xxl"] = clip.tokenize(kwargs.get("t5xxl_negative"))["t5xxl"]
add_dict = {
"width": kwargs.get("width"),
"height": kwargs.get("height"),
"target_width": kwargs.get("target_width"),
"target_height": kwargs.get("target_height"),
}
positive = clip.encode_from_tokens_scheduled(positive_tokens, add_dict=add_dict)
negative = clip.encode_from_tokens_scheduled(negative_tokens, add_dict=add_dict)
guider = SingleCondCFGGuider(model)
guider.set_conds(positive, negative)
guider.set_cfg(kwargs.get("cfg"))
# print(f"POSITIVE: {positive}")
# print(f"NEGATIVE: {negative}")
# print(f"MODEL: {model}")
# print(f"GUIDER: {guider}")
return guider
class TT_Sd35TextEncoderNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35TextEncoderNode",
display_name="TT SD3.5 Text Encoder",
category=CATEGORY,
description="",
inputs=[
io.Model.Input("model"),
io.Clip.Input("clip"),
io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1),
io.String.Input("clip_l_positive", multiline=True, placeholder="CLIP_L Positive", dynamic_prompts=True),
io.String.Input("clip_g_positive", multiline=True, placeholder="CLIP_G Positive", dynamic_prompts=True),
io.String.Input("t5xxl_positive", multiline=True, placeholder="T5XXL Positive", dynamic_prompts=True),
io.String.Input("clip_l_negative", multiline=True, placeholder="CLIP_L Negative", dynamic_prompts=True),
io.String.Input("clip_g_negative", multiline=True, placeholder="CLIP_G Negative", dynamic_prompts=True),
io.String.Input("t5xxl_negative", multiline=True, placeholder="T5XXL Negative", dynamic_prompts=True),
io.String.Input("lora_triggers", multiline=True, placeholder="LoRA Triggers", dynamic_prompts=True),
io.Int.Input("width", default=512, min=0, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=512, min=0, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_width", default=512, min=0, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_height", default=512, min=0, max=MAX_RESOLUTION, step=8),
],
outputs=[
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
guider = encode_prompts_sd35(**kwargs)
return io.NodeOutput(guider)
class TT_Sd35TextEncoderContextNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35TextEncoderContextNode",
display_name="TT SD3.5 Text Encoder (Context)",
category=CATEGORY,
description="",
inputs=[
Context.Input("context")
],
outputs=[
Context.Output("CONTEXT"),
io.Guider.Output("GUIDER"),
]
)
@classmethod
def execute(cls, context) -> io.NodeOutput:
args = {}
for key in (
"model", "clip", "cfg",
"clip_l_positive", "clip_g_positive", "t5xxl_positive",
"clip_l_negative", "clip_g_negative", "t5xxl_negative",
"lora_triggers",
"width", "height", "target_width", "target_height",
):
args[key] = context.get_attr(key)
guider = encode_prompts_sd35(**args)
context.set_attr("guider", guider)
return io.NodeOutput(context, guider)
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class TextEncodeNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_SdxlClipTextEncoderNode,
TT_SdxlClipTextEncoderContextNode,
TT_Flux1ClipTextEncoderNode,
TT_Flux1ClipTextEncoderContextNode,
TT_Flux2TextEncoderNode,
TT_Flux2TextEncoderContextNode,
TT_Sd35TextEncoderNode,
TT_Sd35TextEncoderContextNode,
]
async def comfy_entrypoint() -> TextEncodeNodesExtension:
return TextEncodeNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
"TT_SdxlClipTextEncoderNode",
"TT_SdxlClipTextEncoderContextNode",
"TT_Flux1ClipTextEncoderNode",
"TT_Flux1ClipTextEncoderContextNode",
"TT_Flux2TextEncoderNode",
"TT_Flux2TextEncoderContextNode",
"TT_Sd35TextEncoderNode",
"TT_Sd35TextEncoderContextNode",
]
+59 -1
View File
@@ -1 +1,59 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
from typing import override
import torch
from comfy_api.latest import IO, ComfyExtension
TILE_SIZE, OVERLAP = 512, 64
def vae_encode(image, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP):
samples = vae.encode_tiled(
image,
tile_x=tile_width,
tile_y=tile_height,
overlap=overlap
)
return {"samples": samples}
def vae_decode(latent, vae, tile_width=TILE_SIZE, tile_height=TILE_SIZE, overlap=OVERLAP):
samples = latent["samples"]
compression = vae.spacial_compression_decode()
images = vae.decode_tiled(
samples,
tile_x=tile_width // compression,
tile_y=tile_height // compression,
overlap=overlap // compression
)
return images
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class VaeNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
# TT_Node,
]
async def comfy_entrypoint() -> VaeNodesExtension:
return VaeNodesExtension()
# ==============================================================================
# Re-exports for backward compatibility
# ==============================================================================
__all__ = [
# "TT_Node",
]
+208 -59
View File
@@ -1,4 +1,4 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
# (c) TenserTensor <tenser.tensor@proton.me> || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
import dataclasses
import math
@@ -8,9 +8,11 @@ from typing import override
import torch
from comfy.samplers import KSampler, sampler_object, Sampler
from comfy_api.latest import IO, ComfyExtension
from comfy_api.latest import io, ComfyExtension
from nodes import MAX_RESOLUTION
CATEGORY = "TenserTensor/Workflow"
@dataclass
class TTWorkflowSettings():
@@ -64,16 +66,17 @@ class TTWorkflowSettings():
return val if val is not None else default
@IO.comfytype(io_type="TT_WORKFLOW_CONFIG")
@io.comfytype(io_type="TT_WORKFLOW_CONFIG")
class WorkflowSettings:
Type = TTWorkflowSettings
class Input(IO.Input):
class Input(io.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(IO.Output):
class Output(io.Output):
def __init__(self, **kwargs):
print()
super().__init__(**kwargs)
@@ -115,7 +118,7 @@ def build_sampler_sigmas(sampler_name, steps, width, height):
return sampler, sigmas
class TT_Flux2WorkflowSettingsNode(IO.ComfyNode):
class TT_Flux2WorkflowSettingsNode(io.ComfyNode):
"""
Use this node as the single source of truth for FLUX2 workflow parameters. The sampler and sigma schedule
are computed automatically from the selected sampler, step count, and resolution. Connect `WORKFLOW_CONFIG`
@@ -123,36 +126,36 @@ class TT_Flux2WorkflowSettingsNode(IO.ComfyNode):
"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2WorkflowSettingsNode",
display_name="TT FLUX2 Workflow Settings",
category="TenserTensor/Workflow",
category=CATEGORY,
description="",
inputs=[
IO.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
IO.Int.Input("steps", default=30, min=1, max=10_000),
IO.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1),
IO.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1),
IO.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
IO.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
IO.Combo.Input("sampler_name", options=KSampler.SAMPLERS)
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=30, min=1, max=10_000),
io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1),
io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1),
io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Combo.Input("sampler_name", options=KSampler.SAMPLERS)
],
outputs=[
WorkflowSettings.Output(display_name="WORKFLOW_CONFIG"),
IO.Sampler.Output(display_name="SAMPLER"),
IO.Sigmas.Output(display_name="SIGMAS"),
IO.Int.Output(display_name="SEED"),
IO.Int.Output(display_name="STEPS"),
IO.Float.Output(display_name="CFG"),
IO.Float.Output(display_name="GUIDANCE"),
IO.Int.Output(display_name="WIDTH"),
IO.Int.Output(display_name="HEIGHT"),
io.Sampler.Output(display_name="SAMPLER"),
io.Sigmas.Output(display_name="SIGMAS"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="STEPS"),
io.Float.Output(display_name="CFG"),
io.Float.Output(display_name="GUIDANCE"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
]
)
@classmethod
def execute(cls, **kwargs) -> IO.NodeOutput:
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas(
kwargs.get("sampler_name"),
kwargs.get("steps"),
@@ -173,10 +176,10 @@ class TT_Flux2WorkflowSettingsNode(IO.ComfyNode):
"height": kwargs.get("height"),
}
return IO.NodeOutput(*args.values())
return io.NodeOutput(*args.values())
class TT_Flux2WorkflowSettingsAdvancedNode(IO.ComfyNode):
class TT_Flux2WorkflowSettingsAdvancedNode(io.ComfyNode):
"""
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
@@ -185,45 +188,42 @@ class TT_Flux2WorkflowSettingsAdvancedNode(IO.ComfyNode):
"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Flux2WorkflowSettingsAdvancedNode",
display_name="TT FLUX2 Workflow Settings (Advanced)",
category="TenserTensor/Workflow",
category=CATEGORY,
description="",
inputs=[
IO.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
IO.Int.Input("steps", default=30, min=1, max=10_000),
IO.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1),
IO.Combo.Input("sampler_name", options=KSampler.SAMPLERS),
IO.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
IO.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
IO.String.Input("prompt", multiline=True, dynamic_prompts=True),
IO.String.Input("lora_triggers", multiline=True, dynamic_prompts=True),
IO.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=30, min=1, max=10_000),
io.Float.Input("cfg", default=3.0, min=0.0, max=100.0, step=0.1),
io.Combo.Input("sampler_name", options=KSampler.SAMPLERS),
io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True),
io.Float.Input("guidance", default=3.0, min=1.0, max=10.0, step=0.1),
],
outputs=[
WorkflowSettings.Output(display_name="WORKFLOW_CONFIG"),
IO.Sampler.Output(display_name="SAMPLER"),
IO.Sigmas.Output(display_name="SIGMAS"),
IO.Int.Output(display_name="SEED"),
IO.Int.Output(display_name="STEPS"),
IO.Float.Output(display_name="CFG"),
IO.Int.Output(display_name="WIDTH"),
IO.Int.Output(display_name="HEIGHT"),
IO.String.Output(display_name="PROMPT"),
IO.String.Output(display_name="LORA_TRIGGERS"),
IO.Float.Output(display_name="GUIDANCE"),
io.Sampler.Output(display_name="SAMPLER"),
io.Sigmas.Output(display_name="SIGMAS"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="STEPS"),
io.Float.Output(display_name="CFG"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
io.String.Output(display_name="PROMPT"),
io.String.Output(display_name="LORA_TRIGGERS"),
io.Float.Output(display_name="GUIDANCE"),
]
)
@classmethod
def execute(cls, **kwargs) -> IO.NodeOutput:
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["sampler"], kwargs["sigmas"] = build_sampler_sigmas(
kwargs.get("sampler_name"),
kwargs.get("steps"),
kwargs.get("width"),
kwargs.get("height"),
kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("width"), kwargs.get("height"),
)
workflow_config = TTWorkflowSettings.create(**kwargs)
@@ -241,24 +241,171 @@ class TT_Flux2WorkflowSettingsAdvancedNode(IO.ComfyNode):
"guidance": kwargs.get("guidance"),
}
return IO.NodeOutput(*args.values())
return io.NodeOutput(*args.values())
def sd3_shift_sigma(sigma, schedule_shift):
return schedule_shift * sigma / (1 + (schedule_shift - 1) * sigma)
def build_sd3_sampler_sigmas(sampler_name, steps, schedule_shift=3.0):
sampler = sampler_object(sampler_name)
timesteps = torch.linspace(1, 0, steps + 1)
sigmas = sd3_shift_sigma(timesteps, schedule_shift)
return sampler, sigmas
class TT_Sd35WorkflowSettingsNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35WorkflowSettingsNode",
display_name="TT SD3.5 Workflow Settings",
category=CATEGORY,
description="",
inputs=[
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=30, min=1, max=10_000),
io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1),
io.Combo.Input("sampler_name", options=KSampler.SAMPLERS),
io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True),
io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
],
outputs=[
WorkflowSettings.Output(display_name="WORKFLOW_CONFIG"),
io.Sampler.Output(display_name="SAMPLER"),
io.Sigmas.Output(display_name="SIGMAS"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="STEPS"),
io.Float.Output(display_name="CFG"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
io.Int.Output(display_name="TARGET_WIDTH"),
io.Int.Output(display_name="TARGET_HEIGHT"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas(
kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"),
)
workflow_config = TTWorkflowSettings.create(**kwargs)
args = {
"workflow_config": workflow_config,
"sampler": kwargs["sampler"],
"sigmas": kwargs["sigmas"],
"seed": kwargs.get("seed"),
"steps": kwargs.get("steps"),
"cfg": kwargs.get("cfg"),
"width": kwargs.get("width"),
"height": kwargs.get("height"),
"target_width": kwargs.get("target_width"),
"target_height": kwargs.get("target_height"),
}
return io.NodeOutput(*args.values())
class TT_Sd35WorkflowSettingsAdvancedNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TT_Sd35WorkflowSettingsAdvancedNode",
display_name="TT SD3.5 Workflow Settings (Advanced)",
category=CATEGORY,
description="",
inputs=[
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=30, min=1, max=10_000),
io.Float.Input("cfg", default=5.0, min=0.0, max=100.0, step=0.1),
io.Combo.Input("sampler_name", options=KSampler.SAMPLERS),
io.Float.Input("schedule_shift", default=3.0, min=0.0, max=20.0, step=0.1, advanced=True),
io.String.Input("clip_l_positive", multiline=True, dynamic_prompts=True),
io.String.Input("clip_g_positive", multiline=True, dynamic_prompts=True),
io.String.Input("t5xxl_positive", multiline=True, dynamic_prompts=True),
io.String.Input("clip_l_negative", multiline=True, dynamic_prompts=True),
io.String.Input("clip_g_negative", multiline=True, dynamic_prompts=True),
io.String.Input("t5xxl_negative", multiline=True, dynamic_prompts=True),
io.String.Input("lora_triggers", multiline=True, dynamic_prompts=True, advanced=True),
io.Int.Input("width", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("height", default=1024, min=16, max=MAX_RESOLUTION, step=8),
io.Int.Input("target_width", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
io.Int.Input("target_height", default=1024, min=16, max=MAX_RESOLUTION, step=8, advanced=True),
],
outputs=[
WorkflowSettings.Output(display_name="WORKFLOW_CONFIG"),
io.Sampler.Output(display_name="SAMPLER"),
io.Sigmas.Output(display_name="SIGMAS"),
io.Int.Output(display_name="SEED"),
io.Int.Output(display_name="STEPS"),
io.Float.Output(display_name="CFG"),
io.Int.Output(display_name="WIDTH"),
io.Int.Output(display_name="HEIGHT"),
io.Int.Output(display_name="TARGET_WIDTH"),
io.Int.Output(display_name="TARGET_HEIGHT"),
io.String.Output(display_name="CLIP_L_POSITIVE"),
io.String.Output(display_name="CLIP_G_POSITIVE"),
io.String.Output(display_name="T5XXL_POSITIVE"),
io.String.Output(display_name="CLIP_L_NEGATIVE"),
io.String.Output(display_name="CLIP_G_NEGATIVE"),
io.String.Output(display_name="T5XXL_NEGATIVE"),
io.String.Output(display_name="LORA_TRIGGERS"),
]
)
@classmethod
def execute(cls, **kwargs) -> io.NodeOutput:
kwargs["sampler"], kwargs["sigmas"] = build_sd3_sampler_sigmas(
kwargs.get("sampler_name"), kwargs.get("steps"), kwargs.get("schedule_shift"),
)
workflow_config = TTWorkflowSettings.create(**kwargs)
args = {
"workflow_config": workflow_config,
"sampler": kwargs["sampler"],
"sigmas": kwargs["sigmas"],
"seed": kwargs.get("seed"),
"steps": kwargs.get("steps"),
"cfg": kwargs.get("cfg"),
"width": kwargs.get("width"),
"height": kwargs.get("height"),
"target_width": kwargs.get("target_width"),
"target_height": kwargs.get("target_height"),
"clip_l_positive": kwargs.get("clip_l_positive"),
"clip_g_positive": kwargs.get("clip_g_positive"),
"t5xxl_positive": kwargs.get("t5xxl_positive"),
"clip_l_negative": kwargs.get("clip_l_negative"),
"clip_g_negative": kwargs.get("clip_g_negative"),
"t5xxl_negative": kwargs.get("t5xxl_negative"),
"lora_triggers": kwargs.get("lora_triggers"),
}
return io.NodeOutput(*args.values())
# ==============================================================================
# V3 entrypoint — registers context nodes with ComfyUI
# ==============================================================================
class ContextExtension(ComfyExtension):
class WorkflowNodesExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
TT_Flux2WorkflowSettingsNode,
TT_Flux2WorkflowSettingsAdvancedNode,
TT_Sd35WorkflowSettingsNode,
TT_Sd35WorkflowSettingsAdvancedNode,
]
async def comfy_entrypoint() -> ContextExtension:
return ContextExtension()
async def comfy_entrypoint() -> WorkflowNodesExtension:
return WorkflowNodesExtension()
# ==============================================================================
@@ -268,4 +415,6 @@ async def comfy_entrypoint() -> ContextExtension:
__all__ = [
"TT_Flux2WorkflowSettingsNode",
"TT_Flux2WorkflowSettingsAdvancedNode",
"TT_Sd35WorkflowSettingsNode",
"TT_Sd35WorkflowSettingsAdvancedNode",
]
+15 -4
View File
@@ -1,10 +1,21 @@
# pyproject.toml
[project]
name = "ComfyUI-TenserTensor"
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.5.11"
name = """ComfyUI-TenserTensor"
description = "A collection of custom nodes for ComfyUI that streamline common workflows.
— All-in-one model loaders: checkpoint, LoRA, CLIP, VAE
— GGUF quantized model support: FLUX, FLUX2, SDXL, SD3.5
— Workflow settings nodes for FLUX, SDXL, and SD3.5
— Latent factory with aspect ratio, megapixel, and automatic model format detection
— Context nodes for passing pipeline state between nodes
— CLIP text encoders for FLUX, SDXL, and SD3.5
— KSampler variants with guided sampling and step-by-step latent preview
— VAE encoders and decoders: tiled and context-based
— Image tools: load, resize, upscale, preview, save, ControlNet reference
— Post-production: brightness, contrast, LUT color grading, film grain, upscaler"""
version = "1.6.20"
license = { file = "LICENSE" }
dependencies = ["gguf"]
dependencies = ["gguf", "kornia"]
[project.urls]
Repository = "https://github.com/tenser-tensor/ComfyUI-TenserTensor"
+1
View File
@@ -1 +1,2 @@
gguf>=0.17.0
kornia
-1
View File
@@ -1 +0,0 @@
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
+28
View File
@@ -0,0 +1,28 @@
## TT Canny Edge Detector *TT_CannyEdgeDetectorNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Applies Canny edge detection to a reference image and returns the result as a ControlNet-ready image.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|---------------------------------------------|
| `low_threshold` | FLOAT | Yes | Lower hysteresis threshold, default 0.4 |
| `high_threshold` | FLOAT | Yes | Upper hysteresis threshold, default 0.8 |
| `reference_image` | IMAGE | Yes | Source image, uploaded directly in the node |
### Outputs
| Parameter | Type | Description |
|-----------|-------|-------------------------------|
| `IMAGE` | IMAGE | Edge map ready for ControlNet |
### Usage
Connect `IMAGE` output to a ControlNet apply node. The source image is previewed in the node after processing. Increase `high_threshold` for fewer,
stronger edges; decrease `low_threshold` to retain more fine detail.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -1,4 +1,4 @@
## TT Base Context Passthrough *TT_BaseContextPassthroughNode*
## TT Context Passthrough *TT_ContextPassthroughNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
@@ -0,0 +1,29 @@
## TT FLUX1 CLIP Text Encoder (Context) *TT_Flux1ClipTextEncoderContextNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Context-based variant of the FLUX1 CLIP text encoder. Reads all required parameters from a Context object and writes the resulting conditioning back
into it.
### Inputs
| Parameter | Type | Required | Description |
|-----------|------------|----------|-----------------------------------------------------------|
| `context` | TT_CONTEXT | Yes | Context object carrying CLIP, prompts, and guidance scale |
### Outputs
| Parameter | Type | Description |
|------------|--------------|-----------------------------------------------------------------------|
| `CONTEXT` | TT_CONTEXT | Updated Context with positive and negative conditioning stored inside |
| `POSITIVE` | CONDITIONING | Positive conditioning |
| `NEGATIVE` | CONDITIONING | Negative conditioning |
### Usage
Drop-in replacement for [TT FLUX1 CLIP Text Encoder](TT_Flux1ClipTextEncoderNode.md) in Context-based workflows. The Context must have `clip`,
`clip_l_positive`, `t5xxl_positive`, `clip_l_negative`, `t5xxl_negative`, and `guidance` set before reaching this node.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+32
View File
@@ -0,0 +1,32 @@
## TT FLUX1 CLIP Text Encoder *TT_Flux1ClipTextEncoderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Encodes positive and negative text prompts for FLUX1 models using dual text encoders (CLIP-L and T5-XXL) with guidance scale conditioning.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|--------|----------|-------------------------------------------|
| `clip` | CLIP | Yes | Dual text encoder model (CLIP-L + T5-XXL) |
| `clip_l_positive` | STRING | Yes | CLIP-L positive prompt |
| `t5xxl_positive` | STRING | Yes | T5-XXL positive prompt |
| `clip_l_negative` | STRING | Yes | CLIP-L negative prompt |
| `t5xxl_negative` | STRING | Yes | T5-XXL negative prompt |
| `guidance` | FLOAT | Yes | Guidance scale, default 9.0 |
### Outputs
| Parameter | Type | Description |
|------------|--------------|-----------------------|
| `POSITIVE` | CONDITIONING | Positive conditioning |
| `NEGATIVE` | CONDITIONING | Negative conditioning |
### Usage
Connect `POSITIVE` and `NEGATIVE` outputs to a sampler. CLIP-L handles short, structured descriptions; T5-XXL understands longer, more complex
prompts — you can use the same text in both or tailor each separately.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,51 @@
## TT FLUX2 GGUF Models Loader (Advanced) *TT_Flux2GgufModelsLoaderAdvancedNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Extended GGUF pipeline loader with dequantization control, sampling shift, up to 4 LoRA slots, and advanced device/dtype configuration.
### Inputs
| Parameter | Type | Required | Description |
|-----------------------|---------|----------|--------------------------------------------------------------------------------------|
| `diffusion_model` | COMBO | Yes | GGUF-quantized diffusion model file |
| `dequant_dtype` | COMBO | Yes | Data type for dequantization (default: "bfloat16") *(advanced)* |
| `patch_dtype` | COMBO | Yes | Data type for model patches (default: "bfloat16") *(advanced)* |
| `apply_sampling` | BOOLEAN | Yes | Enable resolution-based sampling shift ("Flux Shift" / "No Shift", default: enabled) |
| `base_sampling_shift` | FLOAT | Yes | Base shift value for sampling schedule (0.0–100.0, default: 0.5) *(advanced)* |
| `max_sampling_shift` | FLOAT | Yes | Maximum shift value for sampling schedule (0.0–100.0, default: 1.15) *(advanced)* |
| `sampling_width` | INT | Yes | Reference width for shift calculation (16–MAX, step: 8, default: 1024) *(advanced)* |
| `sampling_height` | INT | Yes | Reference height for shift calculation (16–MAX, step: 8, default: 1024) *(advanced)* |
| `clip` | COMBO | Yes | GGUF-quantized text encoder file |
| `clip_device` | COMBO | Yes | Device for CLIP inference (default: "default") *(advanced)* |
| `lora_name_1` | COMBO | Yes | LoRA file for slot 1 |
| `strength_1` | FLOAT | Yes | Strength for LoRA slot 1 (-10.0–10.0, default: 1.0) |
| `lora_name_2` | COMBO | Yes | LoRA file for slot 2 |
| `strength_2` | FLOAT | Yes | Strength for LoRA slot 2 (-10.0–10.0, default: 1.0) |
| `lora_name_3` | COMBO | Yes | LoRA file for slot 3 |
| `strength_3` | FLOAT | Yes | Strength for LoRA slot 3 (-10.0–10.0, default: 1.0) |
| `lora_name_4` | COMBO | Yes | LoRA file for slot 4 |
| `strength_4` | FLOAT | Yes | Strength for LoRA slot 4 (-10.0–10.0, default: 1.0) |
| `vae_name` | COMBO | Yes | VAE model file for image encoding/decoding |
| `vae_device` | COMBO | Yes | Device for VAE inference (default: "default") *(advanced)* |
| `vae_dtype` | COMBO | Yes | Data type for VAE computation *(advanced)* |
### Outputs
| Parameter | Type | Description |
|-----------|-------|-----------------------------|
| `MODEL` | MODEL | Loaded GGUF diffusion model |
| `CLIP` | CLIP | Loaded GGUF text encoder |
| `VAE` | VAE | Loaded VAE |
### Usage
Advanced GGUF pipeline loader with full control over quantization and loading parameters. Use `dequant_dtype`
and `patch_dtype` to balance precision and VRAM usage during dequantization. Supports up to 4 simultaneous
LoRAs applied at load time. Enable "Flux Shift" to apply resolution-aware sampling shift — adjust
`base_sampling_shift`, `max_sampling_shift`, and the reference resolution to tune the shift curve.
Use advanced device options to offload components as needed.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+31
View File
@@ -0,0 +1,31 @@
## TT FLUX2 GGUF Models Loader *TT_Flux2GgufModelsLoaderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Loads a complete GGUF-quantized pipeline (diffusion model, text encoder, and VAE) in a single node.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|---------------------------------------------|
| `diffusion_model` | COMBO | Yes | GGUF-quantized diffusion model file |
| `clip` | COMBO | Yes | GGUF-quantized text encoder file |
| `vae_name` | COMBO | Yes | VAE model file for image encoding/decoding |
### Outputs
| Parameter | Type | Description |
|-----------|-------|-------------------------------------|
| `MODEL` | MODEL | Loaded GGUF diffusion model |
| `CLIP` | CLIP | Loaded GGUF text encoder |
| `VAE` | VAE | Loaded VAE |
### Usage
Convenience node for loading a GGUF-quantized pipeline in one step. Designed for use with quantized model
files that reduce VRAM requirements. Select the GGUF diffusion model, GGUF text encoder, and VAE from
their respective dropdowns. LoRA loading is supported at this stage; bypass LoRA is not applied.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,27 @@
## TT FLUX2 Text Encoder (Context) *TT_Flux2TextEncoderContextNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Context-based variant of the FLUX2 text encoder. Reads all required parameters from a Context object and writes the resulting Guider back into it.
### Inputs
| Parameter | Type | Required | Description |
|-----------|------------|----------|--------------------------------------------------------------------------------|
| `context` | TT_CONTEXT | Yes | Context object carrying model, CLIP, prompt, LoRA triggers, and guidance scale |
### Outputs
| Parameter | Type | Description |
|-----------|------------|---------------------------------------------|
| `CONTEXT` | TT_CONTEXT | Updated Context with Guider stored inside |
| `GUIDER` | GUIDER | Configured Guider with encoded conditioning |
### Usage
Drop-in replacement for [TT FLUX2 Text Encoder](TT_Flux2TextEncoderNode.md) in Context-based workflows. The Context must have `model`, `clip`,
`prompt`, `lora_triggers`, and `guidance` set before reaching this node.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+31
View File
@@ -0,0 +1,31 @@
## TT FLUX2 Text Encoder *TT_Flux2TextEncoderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Encodes text prompts for FLUX2 models and returns a ready-to-use Guider object. Combines prompt and LoRA trigger words into a single conditioning
step.
### Inputs
| Parameter | Type | Required | Description |
|-----------------|--------|----------|--------------------------------------------|
| `model` | MODEL | Yes | Diffusion model |
| `clip` | CLIP | Yes | CLIP model for text encoding |
| `prompt` | STRING | Yes | Main generation prompt |
| `lora_triggers` | STRING | No | LoRA trigger words, appended to the prompt |
| `guidance` | FLOAT | Yes | Guidance scale (CFG), default 3.5 |
### Outputs
| Parameter | Type | Description |
|-----------|--------|---------------------------------------------|
| `GUIDER` | GUIDER | Configured Guider with encoded conditioning |
### Usage
Connect to a sampler node that accepts a Guider input. The `lora_triggers` field is a convenience input — enter trigger words for any loaded LoRA
models here instead of adding them manually to the main prompt.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,49 @@
## TT FLUX Models Loader (Advanced) *TT_FluxModelsLoaderAdvancedNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Extended FLUX pipeline loader with sampling shift control, up to 4 LoRA slots, and advanced device/dtype configuration.
### Inputs
| Parameter | Type | Required | Description |
|---------------------|---------|----------|--------------------------------------------------------------------------------------------------|
| `diffusion_model` | COMBO | Yes | FLUX diffusion model checkpoint file |
| `apply_sampling` | BOOLEAN | Yes | Enable resolution-based sampling shift ("Flux Shift" / "No Shift", default: enabled) |
| `base_sampling_shift` | FLOAT | Yes | Base shift value for sampling schedule (0.0–100.0, default: 0.5) *(advanced)* |
| `max_sampling_shift` | FLOAT | Yes | Maximum shift value for sampling schedule (0.0–100.0, default: 1.15) *(advanced)* |
| `sampling_width` | INT | Yes | Reference width for shift calculation (16–MAX, step: 8, default: 1024) *(advanced)* |
| `sampling_height` | INT | Yes | Reference height for shift calculation (16–MAX, step: 8, default: 1024) *(advanced)* |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `t5xxl` | COMBO | Yes | T5-XXL text encoder file |
| `clip_device` | COMBO | Yes | Device for CLIP/T5 inference (default: "default") *(advanced)* |
| `lora_name_1` | COMBO | Yes | LoRA file for slot 1 |
| `strength_1` | FLOAT | Yes | Strength for LoRA slot 1 (-10.0–10.0, default: 1.0) |
| `lora_name_2` | COMBO | Yes | LoRA file for slot 2 |
| `strength_2` | FLOAT | Yes | Strength for LoRA slot 2 (-10.0–10.0, default: 1.0) |
| `lora_name_3` | COMBO | Yes | LoRA file for slot 3 |
| `strength_3` | FLOAT | Yes | Strength for LoRA slot 3 (-10.0–10.0, default: 1.0) |
| `lora_name_4` | COMBO | Yes | LoRA file for slot 4 |
| `strength_4` | FLOAT | Yes | Strength for LoRA slot 4 (-10.0–10.0, default: 1.0) |
| `vae_name` | COMBO | Yes | VAE model file for image encoding/decoding |
| `vae_device` | COMBO | Yes | Device for VAE inference (default: "default") *(advanced)* |
| `vae_dtype` | COMBO | Yes | Data type for VAE computation *(advanced)* |
### Outputs
| Parameter | Type | Description |
|-----------|-------|----------------------------------|
| `MODEL` | MODEL | Loaded FLUX diffusion model |
| `CLIP` | CLIP | Loaded CLIP+T5 text encoder |
| `VAE` | VAE | Loaded VAE |
### Usage
Advanced version of the FLUX pipeline loader with full control over all loading parameters. Supports up to
4 simultaneous LoRAs applied at load time. Enable "Flux Shift" to apply resolution-aware sampling shift —
adjust `base_sampling_shift`, `max_sampling_shift`, and the reference resolution to tune the shift curve.
Use advanced device/dtype options to offload components to specific devices or reduce memory usage.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+33
View File
@@ -0,0 +1,33 @@
## TT FLUX Models Loader *TT_FluxModelsLoaderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Loads all components of the FLUX pipeline (diffusion model, text encoders, and VAE) in a single node.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|------------------------------------------------|
| `diffusion_model` | COMBO | Yes | FLUX diffusion model checkpoint file |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `t5xxl` | COMBO | Yes | T5-XXL text encoder file |
| `clip_device` | COMBO | Yes | Device for CLIP inference (default: "default") |
| `vae_name` | COMBO | Yes | VAE model file for image encoding/decoding |
### Outputs
| Parameter | Type | Description |
|-----------|-------|-----------------------------|
| `MODEL` | MODEL | Loaded FLUX diffusion model |
| `CLIP` | CLIP | Loaded CLIP+T5 text encoder |
| `VAE` | VAE | Loaded VAE |
### Usage
Convenience node for loading the complete FLUX pipeline in one step. Select the diffusion model,
both text encoders (CLIP-L and T5-XXL), and VAE from their respective dropdowns. Use `clip_device`
to offload text encoders to a specific device if needed. LoRA loading is not applied at this stage.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+29
View File
@@ -0,0 +1,29 @@
## TT Guided KSampler *TT_GuidedKSamplerNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Runs guided diffusion sampling using a pre-configured Guider object. Accepts all sampling components as separate inputs for maximum flexibility.
### Inputs
| Parameter | Type | Required | Description |
|----------------|---------|----------|-------------------------------------|
| `latent` | LATENT | Yes | Input latent tensor |
| `guider` | GUIDER | Yes | Configured Guider with conditioning |
| `sigmas` | SIGMAS | Yes | Noise schedule |
| `sampler` | SAMPLER | Yes | Sampler algorithm |
| `random_noise` | NOISE | Yes | Noise generator with seed |
### Outputs
| Parameter | Type | Description |
|-----------|--------|-----------------------|
| `SAMPLES` | LATENT | Sampled latent tensor |
### Usage
Connect `GUIDER`, `SIGMAS`, and `SAMPLER` outputs from a workflow settings node. `random_noise` comes from a latent factory node.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,32 @@
## TT Guided KSampler (With Preview) *TT_GuidedKSamplerWithPreviewNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Runs guided diffusion sampling with optional step-by-step latent preview in the ComfyUI interface. Identical to TT Guided KSampler with an added
preview toggle.
### Inputs
| Parameter | Type | Required | Description |
|------------------|---------|----------|--------------------------------------------|
| `latent` | LATENT | Yes | Input latent tensor |
| `guider` | GUIDER | Yes | Configured Guider with conditioning |
| `sigmas` | SIGMAS | Yes | Noise schedule |
| `sampler` | SAMPLER | Yes | Sampler algorithm |
| `random_noise` | NOISE | Yes | Noise generator with seed |
| `preview_latent` | BOOLEAN | Yes | Show intermediate previews during sampling |
### Outputs
| Parameter | Type | Description |
|-----------|--------|-----------------------|
| `SAMPLES` | LATENT | Sampled latent tensor |
### Usage
Enable `preview_latent` to display latent previews at each sampling step in the node interface. Note: ComfyUI launches with previews disabled by
default — this node overrides that setting automatically.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+33
View File
@@ -0,0 +1,33 @@
## TT Guided Upscale KSampler *TT_GuidedUpscaleKSamplerNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Runs guided diffusion sampling with optional latent upscaling after generation. Extends the standard Guided KSampler with a built-in upscale step.
### Inputs
| Parameter | Type | Required | Description |
|----------------|---------|----------|----------------------------------------------------|
| `latent` | LATENT | Yes | Input latent tensor |
| `guider` | GUIDER | Yes | Configured Guider with conditioning |
| `sigmas` | SIGMAS | Yes | Noise schedule |
| `sampler` | SAMPLER | Yes | Sampler algorithm |
| `random_noise` | NOISE | Yes | Noise generator with seed |
| `scale_latent` | BOOLEAN | Yes | Enable latent upscaling after sampling |
| `scale_factor` | COMBO | No | Upscale multiplier, default `1x` (advanced) |
| `scale_method` | COMBO | No | Interpolation method, default `bicubic` (advanced) |
### Outputs
| Parameter | Type | Description |
|-----------|--------|-----------------------|
| `SAMPLES` | LATENT | Sampled latent tensor |
### Usage
Toggle `scale_latent` to enable upscaling after sampling. Scale factor and method are in advanced inputs. Use for hi-res fix workflows without a
separate upscale node.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+31
View File
@@ -0,0 +1,31 @@
## TT Guider Image Reference *TT_GuiderImageReferenceNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Encodes a reference image into latent space and injects it into a guider's conditioning as reference latents. Useful for image-guided sampling workflows.
### Inputs
| Parameter | Type | Required | Description |
|---|---|---|---|
| `vae` | VAE | Yes | VAE model for encoding the reference image |
| `guider` | GUIDER | Yes | Guider to inject reference latents into |
| `megapixels` | COMBO | Yes | Target resolution for resizing before encoding |
| `resize_method` | COMBO | No | Interpolation method for resizing (advanced) |
| `dimension_step` | INT | No | Align output dimensions to this step size, 1-256 (advanced) |
| `image` | COMBO | Yes | Reference image file to load, with upload support |
### Outputs
| Parameter | Type | Description |
|---|---|---|
| `VAE` | VAE | Passthrough of the input VAE |
| `GUIDER` | GUIDER | Guider with reference latents injected into conditioning |
### Usage
Load a reference image, resize it to the target resolution, encode it with the VAE, and inject the resulting latents into the guider's conditioning. Connect the output `GUIDER` to a sampler to use image-guided sampling.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+32
View File
@@ -0,0 +1,32 @@
## TT Image Loader / Resizer *TT_ImageLoaderResizerNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Loads an image from the input directory with optional resizing to a target megapixel count and alpha mask extraction.
### Inputs
| Parameter | Type | Required | Description |
|------------------|---------|----------|-----------------------------------------------------------------------|
| `resize_image` | BOOLEAN | Yes | Scale image to target megapixels (Scale) or keep original size (Keep) |
| `resize_method` | COMBO | No | Interpolation method for resizing (advanced) |
| `dimension_step` | INT | No | Align output dimensions to this step size, 1–256 (advanced) |
| `megapixels` | COMBO | No | Target resolution in megapixels (advanced) |
| `create_mask` | BOOLEAN | Yes | Extract alpha channel as mask (Mask) or return empty mask (Empty) |
| `image` | COMBO | Yes | Image file to load, with upload support |
### Outputs
| Parameter | Type | Description |
|-----------|-------|----------------------------------------------------|
| `IMAGE` | IMAGE | Loaded (and optionally resized) image tensor |
| `MASK` | MASK | Alpha channel mask, or empty mask if not extracted |
### Usage
Load an image from the input directory and optionally resize it to a target megapixel count. When resizing is enabled, both the image and mask are
scaled together. Use `dimension_step` to snap output dimensions to a specific grid — useful for models that require dimensions divisible by 8 or 64.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+31
View File
@@ -0,0 +1,31 @@
## TT Image Preview / Save *TT_ImagePreviewSaveNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Previews an image in the node graph and optionally saves it to the output directory.
### Inputs
| Parameter | Type | Required | Description |
|---------------------|---------|----------|----------------------------------------------------------------|
| `image` | IMAGE | Yes | Image to preview and optionally save |
| `save_image` | BOOLEAN | Yes | Save image to disk (Save image) or preview only (Only preview) |
| `filename_prefix` | STRING | Yes | Prefix for the output filename (default: TT) |
| `filename_format` | COMBO | No | Filename format pattern (advanced) |
| `subfolder` | STRING | No | Subfolder within the output directory (advanced) |
| `image_format` | COMBO | No | Output file format: PNG, JPEG, WEBP (advanced) |
| `image_quality` | INT | No | JPEG quality, 1-100 (advanced) |
| `compression_level` | INT | No | PNG compression level, 0-9 (advanced) |
### Outputs
This node has no outputs. It is a terminal output node.
### Usage
Place at the end of a workflow to preview the result and optionally save it to disk. When saving is disabled, the image is still visible in the node
graph as a preview. Use `subfolder` to organize outputs into subdirectories, and `filename_format` to control how files are named.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,37 @@
## TT Image Preview / Upscale / Save *TT_ImagePreviewUpscaleSaveNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Previews an image in the node graph with optional upscaling via a dedicated upscale model and optional saving to the output directory.
### Inputs
| Parameter | Type | Required | Description |
|---------------------|---------|----------|-------------------------------------------------------------------------------|
| `image` | IMAGE | Yes | Image to preview, upscale, and optionally save |
| `save_image` | BOOLEAN | Yes | Save image to disk (Save image) or preview only (Only preview) |
| `upscale_image` | BOOLEAN | Yes | Upscale image before saving (Upscale image) or keep original size (Keep size) |
| `filename_prefix` | STRING | Yes | Prefix for the output filename (default: TT) |
| `filename_format` | COMBO | No | Filename format pattern (advanced) |
| `subfolder` | STRING | No | Subfolder within the output directory (advanced) |
| `image_format` | COMBO | No | Output file format: PNG, JPEG, WEBP (advanced) |
| `image_quality` | INT | No | JPEG quality, 1-100 (advanced) |
| `compression_level` | INT | No | PNG compression level, 0-9 (advanced) |
| `upscaler_device` | COMBO | No | Device to run the upscale model on (advanced) |
| `upscale_model` | COMBO | No | Upscale model to use (advanced) |
| `upscale_tile` | INT | No | Tile size for tiled upscaling, 128-4096 (advanced) |
| `upscale_overlap` | INT | No | Overlap between tiles in pixels, 8-256 (advanced) |
### Outputs
This node has no outputs. It is a terminal output node.
### Usage
Place at the end of a workflow to upscale, preview, and optionally save the result. When upscaling is enabled, the image is processed through the
selected upscale model before saving. Tiled upscaling is used to handle large images with limited VRAM — reduce `upscale_tile` if you run into
out-of-memory errors.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+43
View File
@@ -0,0 +1,43 @@
## TT Latent Factory (By Model) *TT_LatentFactoryByModelNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Creates an empty latent tensor with dimensions and channel count automatically derived from the connected model. Supports any model architecture
without manual configuration.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|---------------------------------------------------------------|
| `model` | MODEL | Yes | Diffusion model — latent format is read from it automatically |
| `seed` | INT | Yes | Generation seed |
| `noise_seed` | INT | Yes | Noise seed, randomized after each generation |
| `aspect_ratio` | COMBO | Yes | Aspect ratio of the output image |
| `megapixels` | COMBO | Yes | Target resolution in megapixels |
| `orientation` | COMBO | Yes | Landscape or portrait |
| `batch_size` | INT | No | Number of latents per batch, default 1 (advanced) |
| `clip_multiplier` | COMBO | No | Multiplier for CLIP target dimensions, default 1x (advanced) |
### Outputs
| Parameter | Type | Description |
|-----------------|--------|-----------------------------------------------|
| `MODEL` | MODEL | Passthrough model for chaining nodes |
| `LATENT` | LATENT | Empty latent tensor |
| `RND_NOISE` | NOISE | Noise generator configured with noise seed |
| `SEED` | INT | Generation seed |
| `NOISE_SEED` | INT | Noise seed |
| `MEGAPIXELS` | STRING | Selected megapixel value |
| `WIDTH` | INT | Calculated image width in pixels |
| `HEIGHT` | INT | Calculated image height in pixels |
| `TARGET_WIDTH` | INT | CLIP target width (width × clip_multiplier) |
| `TARGET_HEIGHT` | INT | CLIP target height (height × clip_multiplier) |
### Usage
Connect any diffusion model — the node reads latent channel count and spatial downscale ratio directly from it. Use `MODEL` passthrough output to
continue the node chain without an extra connection.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+35
View File
@@ -0,0 +1,35 @@
## TT Latent MultiTransform *TT_LatentMultiTransformNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Applies multiple geometric transformations to a latent in a single node: scaling, rotation, and flipping. Each transformation can be enabled or
disabled independently.
### Inputs
| Parameter | Type | Required | Description |
|------------------|---------|----------|--------------------------------------------------------|
| `latent` | LATENT | Yes | Input latent to transform |
| `mask` | MASK | No | Optional noise mask to attach to the latent |
| `scale_latent` | BOOLEAN | Yes | Scale latent (Scale) or keep original size (Keep size) |
| `scale_factor` | COMBO | No | Scale multiplier (advanced) |
| `scale_method` | COMBO | No | Interpolation method for scaling (advanced) |
| `rotate_latent` | BOOLEAN | Yes | Rotate latent (Rotate) or keep orientation (Keep) |
| `rotate_angle` | COMBO | Yes | Rotation angle in degrees |
| `flip_latent` | BOOLEAN | Yes | Flip latent (Flip) or keep orientation (Keep) |
| `flip_direction` | COMBO | Yes | Flip axis: horizontal or vertical |
### Outputs
| Parameter | Type | Description |
|-----------|--------|--------------------|
| `LATENT` | LATENT | Transformed latent |
### Usage
Apply scale, rotation, and flip to a latent before or after sampling. Transformations are applied in order: scale → rotate → flip. Use the boolean
toggles to enable only the transformations you need.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,36 @@
## TT Latent MultiTransform On Pixel Space *TT_LatentMultiTransformOnPixelSpaceNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Applies multiple geometric transformations to a latent by first decoding it to pixel space, transforming, and re-encoding. Produces better quality
results than direct latent manipulation.
### Inputs
| Parameter | Type | Required | Description |
|------------------|---------|----------|----------------------------------------------------|
| `vae` | VAE | Yes | VAE model for decoding and re-encoding the latent |
| `latent` | LATENT | Yes | Input latent to transform |
| `mask` | MASK | No | Optional noise mask to attach to the output latent |
| `scale_latent` | BOOLEAN | Yes | Scale image (Scale) or skip (Skip) |
| `scale_factor` | COMBO | Yes | Scale multiplier |
| `scale_method` | COMBO | Yes | Interpolation method for scaling |
| `rotate_latent` | BOOLEAN | Yes | Rotate image (Rotate) or skip (Skip) |
| `rotate_angle` | COMBO | Yes | Rotation angle in degrees, clockwise |
| `flip_latent` | BOOLEAN | Yes | Flip image (Flip) or skip (Skip) |
| `flip_direction` | COMBO | Yes | Flip axis: horizontal or vertical |
### Outputs
| Parameter | Type | Description |
|-----------|--------|-----------------------------------|
| `LATENT` | LATENT | Transformed and re-encoded latent |
### Usage
Decode the latent to pixel space, apply scale, rotation, and flip transformations, then re-encode back to latent space. Transformations are applied in
order: scale → rotate → flip. Requires a VAE — use the same VAE that was used to encode the original latent.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,40 @@
## TT SD3.5 GGUF Models Loader (Advanced) *TT_Sd35GgufModelsLoaderAdvancedNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Extended GGUF loader for SD3.5 with built-in LoRA support and fine-grained quantization and VAE configuration.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|---------------------------------------------------------|
| `diffusion_model` | COMBO | Yes | GGUF diffusion model file |
| `dequant_dtype` | COMBO | No | Dequantization dtype, default `bfloat16` (advanced) |
| `patch_dtype` | COMBO | No | Patch dtype, default `bfloat16` (advanced) |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `clip_g` | COMBO | Yes | CLIP-G text encoder file |
| `t5xxl` | COMBO | Yes | T5-XXL text encoder file |
| `clip_device` | COMBO | No | Device for CLIP inference, default `default` (advanced) |
| `lora_name_1-4` | COMBO | No | LoRA files, up to four |
| `strength_1-4` | FLOAT | No | LoRA strength, default 1.0, range -10.0 to 10.0 |
| `vae_name` | COMBO | Yes | VAE file |
| `vae_device` | COMBO | No | Device for VAE inference, default `default` (advanced) |
| `vae_dtype` | COMBO | No | VAE data type (advanced) |
### Outputs
| Parameter | Type | Description |
|-----------|-------|------------------------|
| `MODEL` | MODEL | Loaded diffusion model |
| `CLIP` | CLIP | Loaded CLIP encoders |
| `VAE` | VAE | Loaded VAE |
### Usage
Place GGUF diffusion model files in `models/diffusion_models_gguf` or `models/diffusion_models`. Place text encoder files in
`models/text_encoders_gguf`, `models/text_encoders` or `models/clip`. Unused LoRA slots are ignored. Negative LoRA strength values invert the effect.
Adjust `dequant_dtype` and `patch_dtype` if you experience quality or memory issues with the default `bfloat16`.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+32
View File
@@ -0,0 +1,32 @@
## TT SD3.5 GGUF Models Loader *TT_Sd35GgufModelsLoaderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Loads a quantized SD3.5 pipeline from GGUF files. Supports separate GGUF files for the diffusion model and all three text encoders.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|-------|----------|---------------------------|
| `diffusion_model` | COMBO | Yes | GGUF diffusion model file |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `clip_g` | COMBO | Yes | CLIP-G text encoder file |
| `t5xxl` | COMBO | Yes | T5-XXL text encoder file |
| `vae_name` | COMBO | Yes | VAE file |
### Outputs
| Parameter | Type | Description |
|-----------|-------|------------------------|
| `MODEL` | MODEL | Loaded diffusion model |
| `CLIP` | CLIP | Loaded CLIP encoders |
| `VAE` | VAE | Loaded VAE |
### Usage
Place GGUF diffusion model files in `models/diffusion_models_gguf` or `models/diffusion_models`. Place text encoder files in
`models/text_encoders_gguf`, `models/text_encoders` or `models/clip`. VAE is loaded from `models/vae` as a standard safetensors file.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,30 @@
## TT SDXL CLIP Text Encoder (Context) *TT_SdxlClipTextEncoderContextNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Context-based variant of the SDXL CLIP text encoder. Reads all required parameters from a Context object and writes the resulting conditioning back
into it.
### Inputs
| Parameter | Type | Required | Description |
|-----------|------------|----------|------------------------------------------------------------------------------------|
| `context` | TT_CONTEXT | Yes | Context object carrying CLIP, prompts, aesthetic scores, and resolution parameters |
### Outputs
| Parameter | Type | Description |
|------------|--------------|-----------------------------------------------------------------------|
| `CONTEXT` | TT_CONTEXT | Updated Context with positive and negative conditioning stored inside |
| `POSITIVE` | CONDITIONING | Positive conditioning |
| `NEGATIVE` | CONDITIONING | Negative conditioning |
### Usage
Drop-in replacement for [TT SDXL CLIP Text Encoder](TT_SdxlClipTextEncoderNode.md) in Context-based workflows. The Context must have `clip`,
`clip_l_positive`, `clip_g_positive`, `clip_l_negative`, `clip_g_negative`, `ascore_positive`, `ascore_negative`, `width`, `height`, `target_width`,
and `target_height` set before reaching this node.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+39
View File
@@ -0,0 +1,39 @@
## TT SDXL CLIP Text Encoder *TT_SdxlClipTextEncoderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Encodes positive and negative text prompts for SDXL models using dual CLIP encoders (CLIP-L and CLIP-G) with aesthetic score and resolution
conditioning.
### Inputs
| Parameter | Type | Required | Description |
|-------------------|--------|----------|--------------------------------------------------------|
| `clip` | CLIP | Yes | Dual CLIP model (CLIP-L + CLIP-G) |
| `clip_l_positive` | STRING | Yes | CLIP-L positive prompt |
| `clip_g_positive` | STRING | Yes | CLIP-G positive prompt |
| `clip_l_negative` | STRING | Yes | CLIP-L negative prompt |
| `clip_g_negative` | STRING | Yes | CLIP-G negative prompt |
| `ascore_positive` | FLOAT | Yes | Aesthetic score for positive conditioning, default 9.0 |
| `ascore_negative` | FLOAT | Yes | Aesthetic score for negative conditioning, default 6.0 |
| `width` | INT | Yes | Source image width |
| `height` | INT | Yes | Source image height |
| `target_width` | INT | Yes | Target output width |
| `target_height` | INT | Yes | Target output height |
### Outputs
| Parameter | Type | Description |
|------------|--------------|-----------------------|
| `POSITIVE` | CONDITIONING | Positive conditioning |
| `NEGATIVE` | CONDITIONING | Negative conditioning |
### Usage
Connect `POSITIVE` and `NEGATIVE` outputs to a sampler. Set `width`/`height` to match your source image dimensions and `target_width`/`target_height`
to your desired output resolution. Higher `ascore_positive` (8–10) steers generation toward higher aesthetic quality; lower `ascore_negative` (4–6)
pushes away from low-quality outputs.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
@@ -0,0 +1,39 @@
## TT SDXL Models Loader (Advanced) *TT_SdxlModelsLoaderAdvancedNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Extended SDXL loader with built-in LoRA support and VAE device and dtype configuration. Loads up to four LoRAs in a single node alongside the main
pipeline.
### Inputs
| Parameter | Type | Required | Description |
|------------------|-------|----------|--------------------------------------------------|
| `primary_ckpt` | COMBO | Yes | Primary checkpoint file |
| `secondary_ckpt` | COMBO | No | Secondary checkpoint for merging, `None` to skip |
| `primary_weight` | FLOAT | No | Merge weight for primary checkpoint, default 1.0 |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `clip_g` | COMBO | Yes | CLIP-G text encoder file |
| `clip_device` | COMBO | No | Device for CLIP inference, default `default` |
| `lora_name_1-4` | COMBO | No | LoRA files, up to four |
| `strength_1-4` | FLOAT | No | LoRA strength, default 1.0, range -10.0 to 10.0 |
| `vae_name` | COMBO | Yes | VAE file |
| `vae_device` | COMBO | No | Device for VAE inference, default `default` |
| `vae_dtype` | COMBO | No | VAE data type |
### Outputs
| Parameter | Type | Description |
|-----------|-------|------------------------|
| `MODEL` | MODEL | Loaded diffusion model |
| `CLIP` | CLIP | Loaded CLIP encoders |
| `VAE` | VAE | Loaded VAE |
### Usage
Identical to TT SDXL Models Loader with added LoRA and VAE configuration. Unused LoRA slots are ignored. Negative LoRA strength values invert the
effect.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)
+34
View File
@@ -0,0 +1,34 @@
## TT SDXL Models Loader *TT_SdxlModelsLoaderNode*
*This description was generated with AI assistance. If you spot any errors, please create an issue on GitHub.*
Loads SDXL model, CLIP encoders, and VAE in a single node. Supports optional model merging with a secondary checkpoint.
### Inputs
| Parameter | Type | Required | Description |
|------------------|-------|----------|--------------------------------------------------|
| `primary_ckpt` | COMBO | Yes | Primary checkpoint file |
| `secondary_ckpt` | COMBO | No | Secondary checkpoint for merging, `None` to skip |
| `primary_weight` | FLOAT | No | Merge weight for primary checkpoint, default 1.0 |
| `clip_l` | COMBO | Yes | CLIP-L text encoder file |
| `clip_g` | COMBO | Yes | CLIP-G text encoder file |
| `clip_device` | COMBO | No | Device for CLIP inference, default `default` |
| `vae_name` | COMBO | Yes | VAE file |
### Outputs
| Parameter | Type | Description |
|-----------|-------|------------------------|
| `MODEL` | MODEL | Loaded diffusion model |
| `CLIP` | CLIP | Loaded CLIP encoders |
| `VAE` | VAE | Loaded VAE |
### Usage
Select a primary checkpoint and separate CLIP-L, CLIP-G encoder files. To merge two checkpoints, select a secondary checkpoint and adjust
`primary_weight` — at 1.0 only the primary is used, at 0.5 both are weighted equally.
---
[View on GitHub](https://github.com/tenser-tensor/ComfyUI-TenserTensor)