From cf1091a444768edd1eb6b09436e641eaf0678059 Mon Sep 17 00:00:00 2001 From: Crespin Alexandre <114501859+kunail0804@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:20:44 +0200 Subject: [PATCH] feat(nodes): add the Debug Viewer ComfyUI node (#24) (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add nodes/debug_viewer_node.py: DebugViewerNode, a read-only inspector (MASTER_SPEC §20), plus render_debug_report in nodes/adapters.py. - Optional inputs for each intermediate state: scene (SCENE), resolved_tags (RESOLVED_TAGS), category_map (CATEGORY_MAP), warnings and errors (STRING). - Renders whichever are connected into a single `report` STRING via each value's to_json(); absent inputs are skipped. Terminal viewer (OUTPUT_NODE); read-only, never modifies the data passing through. No compiler logic in the node. - Registered as SceneCompilerDebugViewer ("Debug Viewer"). Tests: metadata/inputs, empty report, rendering each state (with empty sections omitted), valid serialization, read-only guarantee, and registration. Co-authored-by: kunail0804 Co-authored-by: Claude Opus 4.8 --- nodes/__init__.py | 3 ++ nodes/adapters.py | 30 +++++++++++++ nodes/debug_viewer_node.py | 51 +++++++++++++++++++++ tests/test_debug_viewer_node.py | 78 +++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 nodes/debug_viewer_node.py create mode 100644 tests/test_debug_viewer_node.py diff --git a/nodes/__init__.py b/nodes/__init__.py index 9f78a53..e236ffc 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -8,6 +8,7 @@ Nodes are thin interfaces; all compiler logic lives in the ``compiler`` package. from __future__ import annotations from .category_splitter_node import CategorySplitterNode +from .debug_viewer_node import DebugViewerNode from .prompt_builder_node import PromptBuilderNode from .resolver_node import ResolverNode from .scene_analyzer_node import SceneAnalyzerNode @@ -19,6 +20,7 @@ NODE_CLASS_MAPPINGS: dict[str, type] = { "SceneCompilerResolver": ResolverNode, "SceneCompilerCategorySplitter": CategorySplitterNode, "SceneCompilerPromptBuilder": PromptBuilderNode, + "SceneCompilerDebugViewer": DebugViewerNode, } NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = { @@ -27,6 +29,7 @@ NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = { "SceneCompilerResolver": "Resolver", "SceneCompilerCategorySplitter": "Category Splitter", "SceneCompilerPromptBuilder": "Prompt Builder", + "SceneCompilerDebugViewer": "Debug Viewer", } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/nodes/adapters.py b/nodes/adapters.py index 1f470b6..e1f36cb 100644 --- a/nodes/adapters.py +++ b/nodes/adapters.py @@ -6,7 +6,9 @@ contain compiler logic. from __future__ import annotations +import json from collections.abc import Iterable +from typing import Any from compiler.common.result import Message @@ -17,3 +19,31 @@ def format_messages(messages: Iterable[Message]) -> str: Returns an empty string when there are no messages. """ return "\n".join(f"{message.code}: {message.description}" for message in messages) + + +def render_debug_report( + *, + scene: Any = None, + resolved_tags: Any = None, + category_map: Any = None, + warnings: str = "", + errors: str = "", +) -> str: + """Render whichever intermediate states are provided into one report string. + + Read-only: inputs are serialized via their ``to_json`` methods and never + modified. Absent (None/empty) inputs are skipped. + """ + sections: list[str] = [] + if scene is not None: + sections.append("== Scene JSON ==\n" + json.dumps(scene.to_json(), indent=2)) + if resolved_tags is not None: + payload = [tag.to_json() for tag in resolved_tags] + sections.append("== Resolved Tags ==\n" + json.dumps(payload, indent=2)) + if category_map is not None: + sections.append("== Categories ==\n" + json.dumps(category_map.to_json(), indent=2)) + if warnings: + sections.append("== Warnings ==\n" + warnings) + if errors: + sections.append("== Errors ==\n" + errors) + return "\n\n".join(sections) diff --git a/nodes/debug_viewer_node.py b/nodes/debug_viewer_node.py new file mode 100644 index 0000000..7a6e3bf --- /dev/null +++ b/nodes/debug_viewer_node.py @@ -0,0 +1,51 @@ +"""ComfyUI Debug Viewer node (MASTER_SPEC §20). + +A read-only inspector that renders any connected intermediate state — Scene JSON, +Resolved Tags, Categories, Warnings, Errors — into a single report string. It +never alters the data passing through and contains no compiler logic. +""" + +from __future__ import annotations + +from typing import Any + +from .adapters import render_debug_report + + +class DebugViewerNode: + """Renders connected intermediate states for inspection (read-only).""" + + CATEGORY = "Scene Compiler" + FUNCTION = "run" + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("report",) + OUTPUT_NODE = True + + @classmethod + def INPUT_TYPES(cls) -> dict[str, Any]: + return { + "optional": { + "scene": ("SCENE",), + "resolved_tags": ("RESOLVED_TAGS",), + "category_map": ("CATEGORY_MAP",), + "warnings": ("STRING", {"default": "", "forceInput": True}), + "errors": ("STRING", {"default": "", "forceInput": True}), + } + } + + def run( + self, + scene: Any = None, + resolved_tags: Any = None, + category_map: Any = None, + warnings: str = "", + errors: str = "", + ) -> tuple[str]: + report = render_debug_report( + scene=scene, + resolved_tags=resolved_tags, + category_map=category_map, + warnings=warnings, + errors=errors, + ) + return (report,) diff --git a/tests/test_debug_viewer_node.py b/tests/test_debug_viewer_node.py new file mode 100644 index 0000000..647b1ec --- /dev/null +++ b/tests/test_debug_viewer_node.py @@ -0,0 +1,78 @@ +"""Tests for the Debug Viewer ComfyUI node (issue #24).""" + +from __future__ import annotations + +import json +from types import MappingProxyType + +import nodes +from nodes.debug_viewer_node import DebugViewerNode +from schemas.models import CategoryMap, ResolvedTag, Scene + + +def sample_scene() -> Scene: + return Scene.from_json( + { + "characters": [], + "interactions": [], + "objects": ["chair"], + "environment": [], + "camera": [], + "lighting": [], + "metadata": {"schema_version": "1.0"}, + } + ) + + +def rtag(name: str, category: str) -> ResolvedTag: + return ResolvedTag(tag=name, category=category, source_concept=name, knowledge_base_entry=name) + + +def test_node_metadata() -> None: + assert DebugViewerNode.RETURN_NAMES == ("report",) + assert DebugViewerNode.OUTPUT_NODE is True + optional = DebugViewerNode.INPUT_TYPES()["optional"] + assert set(optional) == {"scene", "resolved_tags", "category_map", "warnings", "errors"} + + +def test_empty_report_when_nothing_connected() -> None: + (report,) = DebugViewerNode().run() + assert report == "" + + +def test_renders_each_state() -> None: + scene = sample_scene() + tags = (rtag("chair", "objects"),) + category_map = CategoryMap(categories=MappingProxyType({"objects": tags})) + (report,) = DebugViewerNode().run( + scene=scene, + resolved_tags=tags, + category_map=category_map, + warnings="SC0001: unknown", + errors="", + ) + assert "== Scene JSON ==" in report + assert "== Resolved Tags ==" in report + assert "== Categories ==" in report + assert "== Warnings ==" in report + assert "== Errors ==" not in report # empty errors omitted + assert "chair" in report + + +def test_report_is_valid_serialization() -> None: + scene = sample_scene() + (report,) = DebugViewerNode().run(scene=scene) + payload = report.split("== Scene JSON ==\n", 1)[1] + assert json.loads(payload)["objects"] == ["chair"] + + +def test_is_read_only() -> None: + scene = sample_scene() + before = scene.to_json() + DebugViewerNode().run(scene=scene) + assert scene.to_json() == before # unchanged + + +def test_registered_for_comfyui() -> None: + assert nodes.NODE_CLASS_MAPPINGS["SceneCompilerDebugViewer"] is DebugViewerNode + assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SceneCompilerDebugViewer"] == "Debug Viewer"