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 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
kunail0804
Claude Opus 4.8
parent
b8a865367c
commit
cf1091a444
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,)
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user