diff --git a/README.md b/README.md index 7799c92..e8a22de 100644 --- a/README.md +++ b/README.md @@ -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. +
## ๐Ÿค Contributing diff --git a/__init__.py b/__init__.py index ee2f56e..d0ded8e 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/camera_angle_control_node.py b/camera_angle_control_node.py index 169d4dd..6488527 100644 --- a/camera_angle_control_node.py +++ b/camera_angle_control_node.py @@ -225,4 +225,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "CameraAngleControl": "Camera Angle Control", -} +} \ No newline at end of file diff --git a/image_latent_resolution_extractor_node.py b/image_latent_resolution_extractor_node.py new file mode 100644 index 0000000..afdb3ab --- /dev/null +++ b/image_latent_resolution_extractor_node.py @@ -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", +} \ No newline at end of file diff --git a/node_list.json b/node_list.json index 8454b26..4b544f3 100644 --- a/node_list.json +++ b/node_list.json @@ -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)", diff --git a/output_any_debug_data_node.py b/output_any_debug_data_node.py new file mode 100644 index 0000000..b07b90c --- /dev/null +++ b/output_any_debug_data_node.py @@ -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)", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f9605da..f0235ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/web/extensions/flow_assistor/debug_data.js b/web/extensions/flow_assistor/debug_data.js new file mode 100644 index 0000000..b5ab042 --- /dev/null +++ b/web/extensions/flow_assistor/debug_data.js @@ -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); + } + } + }; + }, +}); \ No newline at end of file