diff --git a/examples/documentation/display_any.md b/examples/documentation/display_any.md new file mode 100644 index 0000000..14441bd --- /dev/null +++ b/examples/documentation/display_any.md @@ -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. \ No newline at end of file diff --git a/kikotools/__init__.py b/kikotools/__init__.py index 2d4213e..367bac7 100644 --- a/kikotools/__init__.py +++ b/kikotools/__init__.py @@ -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"] diff --git a/kikotools/tools/display_any/__init__.py b/kikotools/tools/display_any/__init__.py new file mode 100644 index 0000000..be3f30a --- /dev/null +++ b/kikotools/tools/display_any/__init__.py @@ -0,0 +1,5 @@ +"""DisplayAny tool for ComfyUI.""" + +from .node import DisplayAnyNode + +__all__ = ["DisplayAnyNode"] diff --git a/kikotools/tools/display_any/logic.py b/kikotools/tools/display_any/logic.py new file mode 100644 index 0000000..f816643 --- /dev/null +++ b/kikotools/tools/display_any/logic.py @@ -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 diff --git a/kikotools/tools/display_any/node.py b/kikotools/tools/display_any/node.py new file mode 100644 index 0000000..a821a04 --- /dev/null +++ b/kikotools/tools/display_any/node.py @@ -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,), + } diff --git a/tests/unit/tools/test_display_any.py b/tests/unit/tools/test_display_any.py new file mode 100644 index 0000000..5ca7187 --- /dev/null +++ b/tests/unit/tools/test_display_any.py @@ -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