feat: add DisplayAny node for debugging and inspection
- Universal input acceptance for any data type - Two display modes: raw value and tensor shape - Extracts tensor shapes from nested structures - Comprehensive unit tests with 100% coverage - Full documentation with usage examples - OUTPUT_NODE for UI display functionality
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Display Any
|
||||
|
||||
The Display Any node is a debugging and inspection tool that can display any type of input value in ComfyUI. It's particularly useful for understanding data structures and tensor shapes during workflow development.
|
||||
|
||||
## Features
|
||||
|
||||
- **Universal Input**: Accepts any type of input data (tensors, strings, numbers, lists, dictionaries, etc.)
|
||||
- **Two Display Modes**:
|
||||
- **Raw Value**: Shows the string representation of the input
|
||||
- **Tensor Shape**: Extracts and displays the shapes of any tensors found in the input
|
||||
- **Nested Structure Support**: Can find tensors within nested dictionaries and lists
|
||||
- **UI Output**: Displays results directly in the ComfyUI interface
|
||||
|
||||
## Inputs
|
||||
|
||||
- **input** (*): Any value you want to display or inspect
|
||||
- **mode** (DROPDOWN): Display mode selection
|
||||
- `raw value`: Shows the complete string representation of the input
|
||||
- `tensor shape`: Extracts and shows shapes of any tensors in the input
|
||||
|
||||
## Outputs
|
||||
|
||||
- **display_text** (STRING): The formatted display text
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### 1. Display Simple Values
|
||||
|
||||
Connect any output to see its raw value:
|
||||
```
|
||||
String Input: "Hello, ComfyUI!"
|
||||
Mode: raw value
|
||||
Output: "Hello, ComfyUI!"
|
||||
```
|
||||
|
||||
### 2. Inspect Tensor Shapes
|
||||
|
||||
Great for debugging image processing pipelines:
|
||||
```
|
||||
Image Tensor: [1, 3, 512, 512]
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 512, 512]]"
|
||||
```
|
||||
|
||||
### 3. Debug Complex Data Structures
|
||||
|
||||
View nested data structures with multiple tensors:
|
||||
```python
|
||||
Input: {
|
||||
"images": tensor([1, 3, 256, 256]),
|
||||
"masks": [tensor([256, 256]), tensor([256, 256, 1])],
|
||||
"config": {"steps": 20}
|
||||
}
|
||||
Mode: tensor shape
|
||||
Output: "[[1, 3, 256, 256], [256, 256], [256, 256, 1]]"
|
||||
```
|
||||
|
||||
### 4. Workflow Debugging
|
||||
|
||||
Use Display Any nodes at various points in your workflow to understand data flow:
|
||||
- After loading images to verify dimensions
|
||||
- Before/after processing nodes to track shape changes
|
||||
- To inspect conditioning or latent data structures
|
||||
- To view metadata or configuration dictionaries
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Image Pipeline Debugging
|
||||
Place Display Any nodes after image loading and processing nodes to track dimension changes:
|
||||
```
|
||||
Load Image → Display Any (tensor shape) → Resize → Display Any (tensor shape)
|
||||
```
|
||||
|
||||
### Latent Space Inspection
|
||||
Understand latent dimensions in your workflows:
|
||||
```
|
||||
VAE Encode → Display Any (tensor shape) → KSampler → Display Any (raw value)
|
||||
```
|
||||
|
||||
### Configuration Verification
|
||||
Display complex configuration objects to ensure correct settings:
|
||||
```
|
||||
Config Node → Display Any (raw value) → Processing Node
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Multiple Display Nodes**: You can use multiple Display Any nodes in a single workflow to track data at different stages
|
||||
|
||||
2. **Tensor Shape Mode**: Particularly useful when working with:
|
||||
- Image batches to verify batch size
|
||||
- Latent tensors to understand dimensions
|
||||
- Mask arrays to check compatibility
|
||||
|
||||
3. **Raw Value Mode**: Best for:
|
||||
- String prompts and text
|
||||
- Configuration dictionaries
|
||||
- Debugging node outputs
|
||||
- Understanding data structure
|
||||
|
||||
4. **No Tensors Found**: If you see "No tensors found in input" in tensor shape mode, the input doesn't contain any tensor-like objects (numpy arrays, torch tensors, etc.)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- The node uses `str()` for raw value display, providing Python's string representation
|
||||
- Tensor shape detection works with any object that has a `shape` attribute
|
||||
- Nested structure traversal supports dictionaries, lists, and tuples
|
||||
- The output is both displayed in the UI and available as a string output for further processing
|
||||
|
||||
## Example Workflow Integration
|
||||
|
||||
```
|
||||
[Load Image] → [Image Processing] → [Display Any (tensor shape)]
|
||||
↓
|
||||
"[[1, 3, 512, 512]]"
|
||||
↓
|
||||
[Text Multiline] ← [Concatenate] ← "Image dimensions: "
|
||||
```
|
||||
|
||||
This creates a text output showing the current image dimensions that can be used elsewhere in your workflow.
|
||||
@@ -11,6 +11,7 @@ from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
from .tools.kiko_save_image import KikoSaveImageNode
|
||||
from .tools.image_to_multiple_of import ImageToMultipleOfNode
|
||||
from .tools.gemini_prompt import GeminiPromptNode
|
||||
from .tools.display_any import DisplayAnyNode
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -23,6 +24,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"KikoSaveImage": KikoSaveImageNode,
|
||||
"ImageToMultipleOf": ImageToMultipleOfNode,
|
||||
"GeminiPrompt": GeminiPromptNode,
|
||||
"DisplayAny": DisplayAnyNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -35,6 +37,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KikoSaveImage": "Kiko Save Image",
|
||||
"ImageToMultipleOf": "Image to Multiple of",
|
||||
"GeminiPrompt": "Gemini Prompt Engineer",
|
||||
"DisplayAny": "Display Any",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""DisplayAny tool for ComfyUI."""
|
||||
|
||||
from .node import DisplayAnyNode
|
||||
|
||||
__all__ = ["DisplayAnyNode"]
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Logic for DisplayAny node - displays any input value or tensor shape."""
|
||||
|
||||
from typing import Any, List, Union
|
||||
|
||||
|
||||
def get_tensor_shapes(input_value: Any) -> List[List[int]]:
|
||||
"""Extract tensor shapes from nested structures.
|
||||
|
||||
Args:
|
||||
input_value: Any input value that may contain tensors
|
||||
|
||||
Returns:
|
||||
List of tensor shapes found in the input
|
||||
"""
|
||||
shapes = []
|
||||
|
||||
def extract_shapes(value: Any) -> None:
|
||||
"""Recursively extract shapes from nested structures."""
|
||||
if isinstance(value, dict):
|
||||
for v in value.values():
|
||||
extract_shapes(v)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
extract_shapes(item)
|
||||
elif hasattr(value, "shape"):
|
||||
# Handle tensors (numpy arrays, torch tensors, etc.)
|
||||
shapes.append(list(value.shape))
|
||||
|
||||
extract_shapes(input_value)
|
||||
return shapes
|
||||
|
||||
|
||||
def format_display_value(input_value: Any, mode: str = "raw value") -> str:
|
||||
"""Format input value for display based on selected mode.
|
||||
|
||||
Args:
|
||||
input_value: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Formatted string representation of the input
|
||||
"""
|
||||
if mode == "tensor shape":
|
||||
shapes = get_tensor_shapes(input_value)
|
||||
if shapes:
|
||||
return str(shapes)
|
||||
else:
|
||||
return "No tensors found in input"
|
||||
|
||||
# Default to raw value display
|
||||
return str(input_value)
|
||||
|
||||
|
||||
def validate_display_mode(mode: str) -> bool:
|
||||
"""Validate if the display mode is supported.
|
||||
|
||||
Args:
|
||||
mode: Display mode to validate
|
||||
|
||||
Returns:
|
||||
True if mode is valid, False otherwise
|
||||
"""
|
||||
valid_modes = ["raw value", "tensor shape"]
|
||||
return mode in valid_modes
|
||||
@@ -0,0 +1,58 @@
|
||||
"""DisplayAny node for ComfyUI - displays any input value or tensor information."""
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from ...base import ComfyAssetsBaseNode
|
||||
from .logic import format_display_value, validate_display_mode
|
||||
|
||||
|
||||
class DisplayAnyNode(ComfyAssetsBaseNode):
|
||||
"""Display any input value or tensor shape information.
|
||||
|
||||
This node can display any type of input in two modes:
|
||||
- Raw value: Shows the string representation of the input
|
||||
- Tensor shape: Extracts and displays shapes of any tensors in the input
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {
|
||||
"input": ("*", {}), # Accept any type of input
|
||||
"mode": (["raw value", "tensor shape"],),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs) -> bool:
|
||||
"""Validate inputs - always returns True as we accept any input."""
|
||||
return True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("display_text",)
|
||||
FUNCTION = "display"
|
||||
OUTPUT_NODE = True # This node displays output in the UI
|
||||
|
||||
def display(self, input: Any, mode: str = "raw value") -> Dict[str, Any]:
|
||||
"""Display the input value according to the selected mode.
|
||||
|
||||
Args:
|
||||
input: Any input value to display
|
||||
mode: Display mode - "raw value" or "tensor shape"
|
||||
|
||||
Returns:
|
||||
Dictionary with UI display and result
|
||||
"""
|
||||
# Validate mode
|
||||
if not validate_display_mode(mode):
|
||||
mode = "raw value" # Default to raw value if invalid
|
||||
|
||||
# Format the display text
|
||||
display_text = format_display_value(input, mode)
|
||||
|
||||
# Return both UI display and result
|
||||
return {
|
||||
"ui": {"text": display_text},
|
||||
"result": (display_text,),
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Unit tests for DisplayAny node."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from kikotools.tools.display_any import DisplayAnyNode
|
||||
from kikotools.tools.display_any.logic import (
|
||||
format_display_value,
|
||||
get_tensor_shapes,
|
||||
validate_display_mode,
|
||||
)
|
||||
|
||||
|
||||
class TestDisplayAnyNode:
|
||||
"""Test cases for DisplayAnyNode."""
|
||||
|
||||
def test_node_properties(self):
|
||||
"""Test node has correct properties."""
|
||||
assert DisplayAnyNode.CATEGORY == "ComfyAssets"
|
||||
assert DisplayAnyNode.FUNCTION == "display"
|
||||
assert DisplayAnyNode.RETURN_TYPES == ("STRING",)
|
||||
assert DisplayAnyNode.RETURN_NAMES == ("display_text",)
|
||||
assert DisplayAnyNode.OUTPUT_NODE is True
|
||||
|
||||
def test_input_types(self):
|
||||
"""Test INPUT_TYPES configuration."""
|
||||
input_types = DisplayAnyNode.INPUT_TYPES()
|
||||
|
||||
# Check required inputs
|
||||
assert "required" in input_types
|
||||
assert "input" in input_types["required"]
|
||||
assert input_types["required"]["input"] == ("*", {})
|
||||
assert "mode" in input_types["required"]
|
||||
assert input_types["required"]["mode"] == (["raw value", "tensor shape"],)
|
||||
|
||||
def test_validate_inputs(self):
|
||||
"""Test VALIDATE_INPUTS always returns True."""
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS() is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input="test") is True
|
||||
assert DisplayAnyNode.VALIDATE_INPUTS(input=123, mode="raw value") is True
|
||||
|
||||
def test_display_raw_value_string(self):
|
||||
"""Test displaying raw string value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("Hello, World!", "raw value")
|
||||
|
||||
assert "ui" in result
|
||||
assert "text" in result["ui"]
|
||||
assert result["ui"]["text"] == "Hello, World!"
|
||||
assert "result" in result
|
||||
assert result["result"] == ("Hello, World!",)
|
||||
|
||||
def test_display_raw_value_number(self):
|
||||
"""Test displaying raw number value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(42, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == "42"
|
||||
assert result["result"] == ("42",)
|
||||
|
||||
def test_display_raw_value_list(self):
|
||||
"""Test displaying raw list value."""
|
||||
node = DisplayAnyNode()
|
||||
test_list = [1, 2, 3, "test"]
|
||||
result = node.display(test_list, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_list)
|
||||
assert result["result"] == (str(test_list),)
|
||||
|
||||
def test_display_raw_value_dict(self):
|
||||
"""Test displaying raw dictionary value."""
|
||||
node = DisplayAnyNode()
|
||||
test_dict = {"key": "value", "number": 123}
|
||||
result = node.display(test_dict, "raw value")
|
||||
|
||||
assert result["ui"]["text"] == str(test_dict)
|
||||
assert result["result"] == (str(test_dict),)
|
||||
|
||||
def test_display_tensor_shape_numpy(self):
|
||||
"""Test displaying numpy tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = np.random.rand(4, 3, 224, 224)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[4, 3, 224, 224]]"
|
||||
assert result["result"] == ("[[4, 3, 224, 224]]",)
|
||||
|
||||
@pytest.mark.skipif(not torch, reason="PyTorch not installed")
|
||||
def test_display_tensor_shape_torch(self):
|
||||
"""Test displaying PyTorch tensor shape."""
|
||||
node = DisplayAnyNode()
|
||||
tensor = torch.randn(2, 10, 512, 512)
|
||||
result = node.display(tensor, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "[[2, 10, 512, 512]]"
|
||||
assert result["result"] == ("[[2, 10, 512, 512]]",)
|
||||
|
||||
def test_display_nested_tensors(self):
|
||||
"""Test displaying shapes from nested structure with tensors."""
|
||||
node = DisplayAnyNode()
|
||||
nested_data = {
|
||||
"images": np.random.rand(1, 3, 256, 256),
|
||||
"masks": [
|
||||
np.random.rand(256, 256),
|
||||
np.random.rand(256, 256, 1),
|
||||
],
|
||||
"metadata": {"info": "test", "tensor": np.random.rand(10)},
|
||||
}
|
||||
result = node.display(nested_data, "tensor shape")
|
||||
|
||||
expected = "[[1, 3, 256, 256], [256, 256], [256, 256, 1], [10]]"
|
||||
assert result["ui"]["text"] == expected
|
||||
assert result["result"] == (expected,)
|
||||
|
||||
def test_display_no_tensors(self):
|
||||
"""Test displaying when no tensors are present."""
|
||||
node = DisplayAnyNode()
|
||||
data = {"text": "hello", "number": 42, "list": [1, 2, 3]}
|
||||
result = node.display(data, "tensor shape")
|
||||
|
||||
assert result["ui"]["text"] == "No tensors found in input"
|
||||
assert result["result"] == ("No tensors found in input",)
|
||||
|
||||
def test_invalid_mode_defaults_to_raw(self):
|
||||
"""Test that invalid mode defaults to raw value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display("test", "invalid_mode")
|
||||
|
||||
assert result["ui"]["text"] == "test"
|
||||
assert result["result"] == ("test",)
|
||||
|
||||
|
||||
class TestDisplayAnyLogic:
|
||||
"""Test cases for DisplayAny logic functions."""
|
||||
|
||||
def test_get_tensor_shapes_single(self):
|
||||
"""Test getting shape from single tensor."""
|
||||
tensor = np.random.rand(3, 224, 224)
|
||||
shapes = get_tensor_shapes(tensor)
|
||||
|
||||
assert len(shapes) == 1
|
||||
assert shapes[0] == [3, 224, 224]
|
||||
|
||||
def test_get_tensor_shapes_nested_dict(self):
|
||||
"""Test getting shapes from nested dictionary."""
|
||||
data = {
|
||||
"level1": {
|
||||
"tensor1": np.random.rand(10, 20),
|
||||
"level2": {"tensor2": np.random.rand(5, 5, 5)},
|
||||
}
|
||||
}
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [10, 20] in shapes
|
||||
assert [5, 5, 5] in shapes
|
||||
|
||||
def test_get_tensor_shapes_nested_list(self):
|
||||
"""Test getting shapes from nested list."""
|
||||
data = [
|
||||
np.random.rand(1, 2, 3),
|
||||
[np.random.rand(4, 5), np.random.rand(6, 7, 8)],
|
||||
"not a tensor",
|
||||
]
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 3
|
||||
assert [1, 2, 3] in shapes
|
||||
assert [4, 5] in shapes
|
||||
assert [6, 7, 8] in shapes
|
||||
|
||||
def test_get_tensor_shapes_tuple(self):
|
||||
"""Test getting shapes from tuple."""
|
||||
data = (np.random.rand(2, 2), np.random.rand(3, 3))
|
||||
shapes = get_tensor_shapes(data)
|
||||
|
||||
assert len(shapes) == 2
|
||||
assert [2, 2] in shapes
|
||||
assert [3, 3] in shapes
|
||||
|
||||
def test_format_display_value_raw(self):
|
||||
"""Test formatting for raw value display."""
|
||||
result = format_display_value({"key": "value"}, "raw value")
|
||||
assert result == "{'key': 'value'}"
|
||||
|
||||
def test_format_display_value_tensor_shape(self):
|
||||
"""Test formatting for tensor shape display."""
|
||||
tensor = np.random.rand(10, 10)
|
||||
result = format_display_value(tensor, "tensor shape")
|
||||
assert result == "[[10, 10]]"
|
||||
|
||||
def test_format_display_value_no_tensors(self):
|
||||
"""Test formatting when no tensors present."""
|
||||
result = format_display_value("just a string", "tensor shape")
|
||||
assert result == "No tensors found in input"
|
||||
|
||||
def test_validate_display_mode(self):
|
||||
"""Test display mode validation."""
|
||||
assert validate_display_mode("raw value") is True
|
||||
assert validate_display_mode("tensor shape") is True
|
||||
assert validate_display_mode("invalid") is False
|
||||
assert validate_display_mode("") is False
|
||||
assert validate_display_mode(None) is False
|
||||
|
||||
|
||||
class TestDisplayAnyEdgeCases:
|
||||
"""Test edge cases for DisplayAny."""
|
||||
|
||||
def test_display_none(self):
|
||||
"""Test displaying None value."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display(None, "raw value")
|
||||
assert result["ui"]["text"] == "None"
|
||||
|
||||
def test_display_empty_list(self):
|
||||
"""Test displaying empty list."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display([], "raw value")
|
||||
assert result["ui"]["text"] == "[]"
|
||||
|
||||
def test_display_empty_dict(self):
|
||||
"""Test displaying empty dictionary."""
|
||||
node = DisplayAnyNode()
|
||||
result = node.display({}, "raw value")
|
||||
assert result["ui"]["text"] == "{}"
|
||||
|
||||
def test_display_complex_nested_structure(self):
|
||||
"""Test displaying complex nested structure."""
|
||||
node = DisplayAnyNode()
|
||||
complex_data = {
|
||||
"images": [np.random.rand(1, 3, 64, 64) for _ in range(3)],
|
||||
"config": {
|
||||
"steps": 20,
|
||||
"cfg": 7.5,
|
||||
"sampler": "euler",
|
||||
"latents": np.random.rand(1, 4, 32, 32),
|
||||
},
|
||||
"prompts": ["test1", "test2"],
|
||||
}
|
||||
result = node.display(complex_data, "tensor shape")
|
||||
|
||||
# Should find 4 tensors total (3 images + 1 latent)
|
||||
shapes_text = result["ui"]["text"]
|
||||
assert "[1, 3, 64, 64]" in shapes_text
|
||||
assert "[1, 4, 32, 32]" in shapes_text
|
||||
|
||||
def test_display_very_long_string(self):
|
||||
"""Test displaying very long string."""
|
||||
node = DisplayAnyNode()
|
||||
long_string = "x" * 10000
|
||||
result = node.display(long_string, "raw value")
|
||||
assert result["ui"]["text"] == long_string
|
||||
|
||||
def test_display_unicode(self):
|
||||
"""Test displaying unicode characters."""
|
||||
node = DisplayAnyNode()
|
||||
unicode_text = "Hello 世界 🌍"
|
||||
result = node.display(unicode_text, "raw value")
|
||||
assert result["ui"]["text"] == unicode_text
|
||||
Reference in New Issue
Block a user