Merge pull request #4 from tenser-tensor/feature/image-nodes
feature/image-nodes
This commit is contained in:
+33
-3
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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)",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 +1,2 @@
|
||||
gguf>=0.17.0
|
||||
kornia
|
||||
@@ -1 +0,0 @@
|
||||
# (c) TenserTensor || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user