Add files via upload

This commit is contained in:
Merserk
2025-12-26 21:56:34 +02:00
committed by GitHub
parent f35226a847
commit 2e4d0ee50a
8 changed files with 316 additions and 17 deletions
+39 -11
View File
@@ -155,7 +155,17 @@ A utility node that passes an image through unchanged but extracts its width and
---
### 7. ✖️ Multiplication (Dual & Latent)
### 7. 📏 Image Latent Resolution Extractor
**Get pixel dimensions from a Latent.**
Standard Stable Diffusion latents are compressed (1/8th scale). This node reads the latent dimensions and multiplies them by 8 to give you the effective pixel width and height.
> **Use Case:**
> Useful when you have a latent and need to know its target pixel size for downstream resizing or conditionings, without decoding it first.
---
### 8. ✖️ Multiplication (Dual & Latent)
**Mathematical scaling for custom upscaling.**
Takes two integer inputs (like Width and Height) and an optional Latent, and multiplies them by a float factor.
@@ -176,7 +186,7 @@ Takes two integer inputs (like Width and Height) and an optional Latent, and mul
---
### 8. 🧹 VRAM/RAM Cleaner
### 9. 🧹 VRAM/RAM Cleaner
**Manage your resources mid-workflow.**
A "Pass-through" node that can connect to **any** input (Model, VAE, Image, etc.). It passes the data through unchanged but triggers a garbage collection and VRAM flush event when executed.
@@ -188,7 +198,7 @@ A "Pass-through" node that can connect to **any** input (Model, VAE, Image, etc.
---
### 9. 🎮 Flow Control (Sidecar Bypass)
### 10. 🎮 Flow Control (Sidecar Bypass)
**Toggle nodes On/Off remotely.**
A control panel that connects to up to 4 other nodes. This acts as a "Sidecar" — it does not sit in the flow of data, but rather controls the logic of the connected nodes.
@@ -201,7 +211,7 @@ A control panel that connects to up to 4 other nodes. This acts as a "Sidecar"
---
### 10. 🔀 Any Passthrough
### 11. 🔀 Any Passthrough
**Universal rerouting tools.**
Use these to clean up "spaghetti" wires or create fallback logic. These nodes accept **any** connection type (Model, Image, String, Latent, etc.).
@@ -211,21 +221,34 @@ Use these to clean up "spaghetti" wires or create fallback logic. These nodes ac
---
### 11. ⏱️ Add Delay
### 12. ⏱️ Add Delay
**Pause execution for debugging.**
A simple node that connects to any data type, waits for a specified number of seconds, and then passes the data through unchanged. Useful for timing API calls or debugging complex flows.
---
### 12. 📺 Show Text
### 13. 🐞 Debug Data (Any Input)
**Inspect your data connections.**
Not sure what shape a tensor is? Want to check the exact resolution of a latent? Connect **ANY** output to this node. It analyzes the data and displays a summary directly on the node.
> **Features:**
> * **Images:** Displays Resolution, Aspect Ratio, Batch Size, and Channels.
> * **Latents:** Displays Latent Shape and equivalent Pixel Resolution (8x).
> * **General:** Displays Type, Tensor Shape, Dtype, and Device.
> * **On-Graph Display:** Shows the data in a read-only "Matrix green" text box directly on the node itself.
---
### 14. 📺 Show Text
**Display text directly on the graph.**
Connect any string output (like the `Prompt Queue` or `Camera Angle Control`) to this node. It will display the text content inside a widget box on the node itself. Useful for verifying what is actually being sent to your CLIP encoder.
---
### 13. 🎨 CLIP Text Encode (Prompt Enrichment)
### 15. 🎨 CLIP Text Encode (Prompt Enrichment)
**Standard conditioning with built-in style power.**
Why copy-paste "masterpiece, best quality, 4k..." every time? This node acts exactly like the standard CLIP Text Encode but includes a dropdown with **20 curated style presets**.
@@ -246,7 +269,7 @@ Why copy-paste "masterpiece, best quality, 4k..." every time? This node acts exa
---
### 14. 🖱️ Visual Marquee (Interactive)
### 16. 🖱️ Visual Marquee (Interactive)
**Pause, Select, and Refine.**
A Human-in-the-Loop tool that pauses your workflow and opens a popup editor over your generated image. You can draw a crop box to select a specific area, then resume the workflow to process just that area.
@@ -269,7 +292,7 @@ A Human-in-the-Loop tool that pauses your workflow and opens a popup editor over
---
### 15. 🧩 Tile Compositor (Merge)
### 17. 🧩 Tile Compositor (Merge)
**Seamlessly paste refined details back.**
The partner node to **Visual Marquee**. After you have processed/upscaled/inpainted your cropped area, this node puts it back into the original full-size image at the exact correct coordinates.
@@ -280,7 +303,7 @@ The partner node to **Visual Marquee**. After you have processed/upscaled/inpain
---
### 16. 💎 Detail Enhancer
### 18. 💎 Detail Enhancer
**Inject texture and crispness.**
A specialized node that manipulates the noise schedule (Sigmas) to force the model to generate more high-frequency detail. It works by "slowing down" the sampling process during the critical detail-formation phase and artificially inflating noise values.
@@ -302,7 +325,7 @@ A specialized node that manipulates the noise schedule (Sigmas) to force the mod
---
### 17. ☁️ LoRA Online
### 19. ☁️ LoRA Online
**Use LoRAs directly from URLs.**
Skip the manual download process. Paste a link from Civitai, Tensor.art, or HuggingFace, and this node will download the LoRA, load it into your model, and optionally delete it after generation to save disk space.
@@ -359,6 +382,11 @@ Skip the manual download process. Paste a link from Civitai, Tensor.art, or Hugg
4. Feed the Result, the Original Image, and the `tile_data` into **Tile Compositor**.
5. Result: The original image with a high-res, refined detail pasted back in seamlessly.
### The "Debugger"
1. Not sure if your latent is the right size?
2. Connect your Latent to the **Debug Data** node.
3. It instantly shows: `Latent Size: 128x128` and `Pixel Res: 1024x1024` right on the graph.
<br>
## 🤝 Contributing
+15 -4
View File
@@ -30,6 +30,11 @@ try:
except ImportError:
EXTRACT_CLASSES, EXTRACT_DISPLAY = {}, {}
try:
from .image_latent_resolution_extractor_node import NODE_CLASS_MAPPINGS as LAT_EXT_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as LAT_EXT_DISPLAY
except ImportError:
LAT_EXT_CLASSES, LAT_EXT_DISPLAY = {}, {}
try:
from .multiplication_node import NODE_CLASS_MAPPINGS as MULT_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as MULT_DISPLAY
except ImportError:
@@ -50,6 +55,12 @@ try:
except ImportError:
ANYPT_CLASSES, ANYPT_DISPLAY = {}, {}
# New Debug Node
try:
from .output_any_debug_data_node import NODE_CLASS_MAPPINGS as DEBUG_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as DEBUG_DISPLAY
except ImportError:
DEBUG_CLASSES, DEBUG_DISPLAY = {}, {}
try:
from .delay_node import NODE_CLASS_MAPPINGS as DELAY_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as DELAY_DISPLAY
except ImportError:
@@ -65,25 +76,21 @@ try:
except ImportError:
ENRICH_CLASSES, ENRICH_DISPLAY = {}, {}
# Tile Compositor (and Manager)
try:
from .tile_manager_nodes import NODE_CLASS_MAPPINGS as TILE_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as TILE_DISPLAY
except ImportError:
TILE_CLASSES, TILE_DISPLAY = {}, {}
# Visual Marquee (Popup/JS based)
try:
from .visual_marquee_node import NODE_CLASS_MAPPINGS as VIS_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as VIS_DISPLAY
except ImportError:
VIS_CLASSES, VIS_DISPLAY = {}, {}
# Detail Enhancer
try:
from .detail_enhance_node import NODE_CLASS_MAPPINGS as DETAIL_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as DETAIL_DISPLAY
except ImportError:
DETAIL_CLASSES, DETAIL_DISPLAY = {}, {}
# LoRA Online Loader
try:
from .lora_online_node import NODE_CLASS_MAPPINGS as LORA_ONLINE_CLASSES, NODE_DISPLAY_NAME_MAPPINGS as LORA_ONLINE_DISPLAY
except ImportError:
@@ -97,10 +104,12 @@ NODE_CLASS_MAPPINGS = {
**RES_CLASSES,
**FIT_CLASSES,
**EXTRACT_CLASSES,
**LAT_EXT_CLASSES,
**MULT_CLASSES,
**CLEAN_CLASSES,
**FLOW_CLASSES,
**ANYPT_CLASSES,
**DEBUG_CLASSES,
**DELAY_CLASSES,
**DT_CLASSES,
**ENRICH_CLASSES,
@@ -117,10 +126,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
**RES_DISPLAY,
**FIT_DISPLAY,
**EXTRACT_DISPLAY,
**LAT_EXT_DISPLAY,
**MULT_DISPLAY,
**CLEAN_DISPLAY,
**FLOW_DISPLAY,
**ANYPT_DISPLAY,
**DEBUG_DISPLAY,
**DELAY_DISPLAY,
**DT_DISPLAY,
**ENRICH_DISPLAY,
+1 -1
View File
@@ -225,4 +225,4 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"CameraAngleControl": "Camera Angle Control",
}
}
+58
View File
@@ -0,0 +1,58 @@
import torch
class ImageLatentResolutionExtractorNode:
"""
A utility node that takes a LATENT (samples) as input and extracts its
corresponding pixel resolution (Width and Height).
Standard Stable Diffusion latents are 1/8th the size of the pixel image.
This node calculates: Pixel Size = Latent Size * 8.
- Input: samples (Latent)
- Outputs: Latent (Passthrough), Width (Int), Height (Int)
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"samples": ("LATENT",),
},
}
RETURN_TYPES = ("LATENT", "INT", "INT")
RETURN_NAMES = ("latent", "width", "height")
FUNCTION = "extract_resolution"
CATEGORY = "flow-assistor"
def extract_resolution(self, samples):
# samples is a dictionary: {'samples': Tensor[Batch, Channels, Height, Width]}
latent_tensor = samples["samples"]
# Get dimensions from the tensor
# Shape is [Batch, Channels, H, W]
l_height = latent_tensor.shape[2]
l_width = latent_tensor.shape[3]
# Convert to pixel dimensions (Standard SD factor is 8)
# We assume square pixels
pixel_width = int(l_width * 8)
pixel_height = int(l_height * 8)
return (
samples, # Pass the latent through unchanged
pixel_width,
pixel_height
)
# Node Registration
NODE_CLASS_MAPPINGS = {
"ImageLatentResolutionExtractorNode": ImageLatentResolutionExtractorNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageLatentResolutionExtractorNode": "Image Latent Resolution Extractor",
}
+2
View File
@@ -5,11 +5,13 @@
"ResolutionSelectNode": "Resolution Selector (Groups)",
"ImageResolutionFitNode": "Image Resolution Fit",
"ImageResolutionExtractorNode": "Image Resolution Extractor",
"ImageLatentResolutionExtractorNode": "Image Latent Resolution Extractor",
"MultiplicationNode": "Multiplication (Dual & Latent)",
"VRAMRAMCleanerNode": "VRAM/RAM Cleaner",
"BypassControl": "Flow Control (Sidecar Bypass)",
"AnyPassthrough6to1": "Any Passthrough (6 → 1)",
"AnyPassthrough1to6": "Any Passthrough (1 → 6)",
"OutputAnyDebugDataNode": "Debug Data (Any Input)",
"AddDelay": "Add Delay",
"DisplayText": "Show Text",
"CLIPTextEncodePromptEnrichment": "CLIP Text Encode (Prompt Enrichment)",
+124
View File
@@ -0,0 +1,124 @@
import torch
# Special AnyType to allow any connection
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
class OutputAnyDebugDataNode:
"""
A debug node that accepts ANY input and analyzes it.
- If IMAGE: Shows resolution (WxH), Aspect Ratio, and Batch Size.
- If LATENT: Shows Latent Dimensions and equivalent Pixel Resolution (8x).
- If other: Shows Type and raw string representation.
The result is displayed as text on the node itself (via JS) and output as a string.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any_input": (any_type,),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("debug_text",)
FUNCTION = "analyze_data"
CATEGORY = "flow-assistor/debug"
OUTPUT_NODE = True
def analyze_data(self, any_input, unique_id=None, extra_pnginfo=None):
text_output = "None"
try:
# 1. Handle Torch Tensors (Images, Masks, Raw Latents)
if isinstance(any_input, torch.Tensor):
shape = any_input.shape
# Basic info
text_output = f"Type: Tensor\nShape: {list(shape)}\nDtype: {any_input.dtype}\nDevice: {any_input.device}\n"
# Heuristic for IMAGE: [Batch, Height, Width, Channels]
if any_input.ndim == 4:
# Standard Image: Channels at end (usually 3 or 4)
if shape[3] <= 4 and shape[1] > 8 and shape[2] > 8:
b, h, w, c = shape
ar = w / h if h > 0 else 0
text_output += f"\n--- IMAGE DETECTED ---\n"
text_output += f"Resolution: {w}x{h}\n"
text_output += f"Aspect Ratio: {ar:.2f}\n"
text_output += f"Batch Size: {b}\n"
text_output += f"Channels: {c}"
# Raw Latent / NCHW: Channels at dim 1
elif shape[1] <= 4 and shape[2] > 8 and shape[3] > 8:
b, c, h, w = shape
text_output += f"\n--- RAW LATENT/NCHW ---\n"
text_output += f"Latent Size: {w}x{h}\n"
text_output += f"Pixel Res (~8x): {w*8}x{h*8}\n"
text_output += f"Batch Size: {b}"
# Heuristic for MASK: [Batch, Height, Width]
elif any_input.ndim == 3:
b, h, w = shape
text_output += f"\n--- MASK DETECTED ---\n"
text_output += f"Resolution: {w}x{h}\n"
text_output += f"Batch Size: {b}"
# 2. Handle Latent Dictionary (Standard ComfyUI Latent Wrapper)
elif isinstance(any_input, dict) and "samples" in any_input:
samples = any_input["samples"]
if isinstance(samples, torch.Tensor):
# Shape: [Batch, 4, Height, Width]
shape = samples.shape
b, c, h, w = shape
ar = w / h if h > 0 else 0
text_output = f"Type: LATENT (Dict)\nShape: {list(shape)}\n"
text_output += f"\n--- RESOLUTION INFO ---\n"
text_output += f"Latent Size: {w}x{h}\n"
text_output += f"Pixel Res (8x): {w*8}x{h*8}\n"
text_output += f"Aspect Ratio: {ar:.2f}\n"
text_output += f"Batch Size: {b}"
else:
text_output = f"Type: LATENT (Dict)\nError: 'samples' is not a tensor."
# 3. Handle Lists
elif isinstance(any_input, list):
text_output = f"Type: List\nLength: {len(any_input)}\n"
if len(any_input) > 0:
text_output += f"Sample[0]: {str(any_input[0])[:50]}..."
# 4. Handle Basic Types
elif isinstance(any_input, (int, float, bool, str)):
text_output = f"Type: {type(any_input).__name__}\nValue: {any_input}"
# 5. Generic Fallback
else:
raw_str = str(any_input)
text_output = f"Type: {type(any_input)}\n"
text_output += f"Value: {raw_str[:200]}..." if len(raw_str) > 0 else "Empty"
except Exception as e:
text_output = f"Error analyzing data: {str(e)}"
# Return: {"ui": {KEY: [LIST]}, "result": (TUPLE)}
return {"ui": {"text": [text_output]}, "result": (text_output,)}
NODE_CLASS_MAPPINGS = {
"OutputAnyDebugDataNode": OutputAnyDebugDataNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"OutputAnyDebugDataNode": "Debug Data (Any Input)",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-flow-assistor"
description = "Essential utility nodes for ComfyUI (Prompt Queue, Camera Control, Resolution Fit, etc.)"
version = "1.0.5"
version = "1.0.6"
license = { file = "LICENSE.txt" }
dependencies = []
tags = ["utils", "prompt", "camera", "resolution", "workflow"]
@@ -0,0 +1,76 @@
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
// Renders the debug text returned by OutputAnyDebugDataNode directly on the node.
app.registerExtension({
name: "FlowAssistor.DebugData",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "OutputAnyDebugDataNode") return;
// Helper to Create or Update the Text Widget
function populate(text) {
// 1. Try to find the existing widget to avoid flicker
const widgetName = "debug_text_msg";
let w = this.widgets?.find((w) => w.name === widgetName);
// 2. If it doesn't exist, create it
if (!w) {
// ComfyWidgets["STRING"] adds the widget to this.widgets automatically
const widgetResult = ComfyWidgets["STRING"](
this,
widgetName,
["STRING", { multiline: true }],
app
);
w = widgetResult.widget;
// Styling to look like a debug console
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.9;
w.inputEl.style.backgroundColor = "#222";
w.inputEl.style.color = "#0f0"; // Matrix green text
w.inputEl.style.fontFamily = "monospace";
w.inputEl.style.fontSize = "11px";
w.inputEl.style.padding = "4px";
}
// 3. Update the value
w.value = text ?? "Waiting for data...";
// 4. Force Resize to fit content
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) sz[0] = this.size[0];
if (sz[1] < this.size[1]) sz[1] = this.size[1];
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// Hook: When the node executes (during prompt run)
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
// "message.text" comes from the python return {"ui": {"text": ...}}
// ComfyUI usually sends lists for UI elements
const text = message?.text?.[0] ?? message?.text ?? "No Data";
populate.call(this, text);
};
// Hook: When the node loads from a workflow (restore saved state)
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
// Restore the last seen value if it exists in the save file
if (this.widgets_values && this.widgets_values.length) {
// The read-only widget usually ends up being the last one in saved values
// or we just check index 0 if it's the only widget
const savedText = this.widgets_values[0];
if (typeof savedText === "string") {
populate.call(this, savedText);
}
}
};
},
});