Expose the Scene Analyzer as a ComfyUI node (MASTER_SPEC §20) plus the node registration plumbing and a shared output adapter. - nodes/scene_analyzer_node.py: SceneAnalyzerNode, a thin adapter. Inputs are Natural Language, Model Name, Temperature, Timeout, and an optional System Prompt override; it builds a Config, constructs OllamaBackend.from_config, and delegates to analyze(). Outputs (scene, warnings, errors): the SCENE model and the warnings/errors formatted for display. No compiler logic in the node. - nodes/adapters.py: format_messages() renders diagnostics as CODE: description lines. - Registration: nodes/__init__.py aggregates NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS; the root __init__.py re-exports them. ComfyUI compatibility: the node package uses relative imports internally so our `nodes` package is never resolved as ComfyUI's own top-level `nodes` module, and the entry point puts the repo root on sys.path so `compiler`/`schemas` resolve when ComfyUI loads the package by file path. Tests (no live Ollama): INPUT_TYPES/RETURN metadata, delegation to analyze (monkeypatched) with warnings/errors surfaced, empty-message strings, system prompt override passthrough, and ComfyUI 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
c777bebf00
commit
0de9b43d7a
+19
-7
@@ -2,15 +2,27 @@
|
||||
|
||||
ComfyUI loads this module by path and reads ``NODE_CLASS_MAPPINGS`` /
|
||||
``NODE_DISPLAY_NAME_MAPPINGS`` to register the compiler's nodes. The mappings are
|
||||
populated in Phase 5 when the nodes are implemented; until then they are empty so
|
||||
that a fresh install imports cleanly without registering anything.
|
||||
aggregated in the ``nodes`` package; the compiler logic lives entirely in the
|
||||
``compiler`` package, which never imports ComfyUI.
|
||||
|
||||
Compiler logic never lives here — it belongs in the ``compiler/`` package. This
|
||||
file is an interface for ComfyUI only.
|
||||
The ``compiler`` and ``schemas`` packages are imported by their top-level names,
|
||||
so this entry point puts the repository root on ``sys.path`` when ComfyUI loads it
|
||||
by file path. The ``nodes`` package is only ever imported relative to this
|
||||
package (never as a top-level ``nodes``), so it does not collide with ComfyUI's
|
||||
own core ``nodes`` module.
|
||||
"""
|
||||
|
||||
# Populated by nodes/ in Phase 5 (issues #19–#26).
|
||||
NODE_CLASS_MAPPINGS: dict[str, type] = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {}
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
try:
|
||||
# ComfyUI loads this file inside a package, so the relative import resolves.
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
except ImportError:
|
||||
# Imported standalone (e.g. by the test runner): fall back to the top-level
|
||||
# package that is on sys.path.
|
||||
from nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
+20
-1
@@ -1 +1,20 @@
|
||||
"""nodes package."""
|
||||
"""ComfyUI node registrations for Scene Compiler.
|
||||
|
||||
Each node lives in its own module and is aggregated here into the
|
||||
``NODE_CLASS_MAPPINGS`` / ``NODE_DISPLAY_NAME_MAPPINGS`` that ComfyUI reads.
|
||||
Nodes are thin interfaces; all compiler logic lives in the ``compiler`` package.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .scene_analyzer_node import SceneAnalyzerNode
|
||||
|
||||
NODE_CLASS_MAPPINGS: dict[str, type] = {
|
||||
"SceneCompilerAnalyzer": SceneAnalyzerNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
|
||||
"SceneCompilerAnalyzer": "Scene Analyzer",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Shared helpers for adapting compiler results to ComfyUI node outputs.
|
||||
|
||||
Nodes are thin interfaces; these helpers only format data for display and never
|
||||
contain compiler logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from compiler.common.result import Message
|
||||
|
||||
|
||||
def format_messages(messages: Iterable[Message]) -> str:
|
||||
"""Render diagnostic messages as one ``CODE: description`` line each.
|
||||
|
||||
Returns an empty string when there are no messages.
|
||||
"""
|
||||
return "\n".join(f"{message.code}: {message.description}" for message in messages)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""ComfyUI node for the Scene Analyzer (MASTER_SPEC §20).
|
||||
|
||||
A thin interface: it adapts ComfyUI inputs to the Analyzer module and surfaces the
|
||||
CompilerResult's data, warnings, and errors. It contains no compiler logic and
|
||||
never imports ComfyUI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from compiler.analyzer.backend import OllamaBackend
|
||||
from compiler.analyzer.scene_analyzer import analyze
|
||||
from compiler.common.config import Config
|
||||
|
||||
from .adapters import format_messages
|
||||
|
||||
|
||||
class SceneAnalyzerNode:
|
||||
"""Runs the Scene Analyzer and emits Scene plus warnings/errors."""
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("SCENE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("scene", "warnings", "errors")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"natural_language": ("STRING", {"multiline": True, "default": ""}),
|
||||
"model_name": ("STRING", {"default": "llama3"}),
|
||||
"temperature": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"timeout": ("INT", {"default": 60, "min": 1, "max": 3600}),
|
||||
},
|
||||
"optional": {
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
def run(
|
||||
self,
|
||||
natural_language: str,
|
||||
model_name: str,
|
||||
temperature: float,
|
||||
timeout: int,
|
||||
system_prompt: str = "",
|
||||
) -> tuple[Any, str, str]:
|
||||
analyzer: dict[str, Any] = {
|
||||
"model": model_name,
|
||||
"temperature": temperature,
|
||||
"timeout": timeout,
|
||||
}
|
||||
if system_prompt:
|
||||
analyzer["system_prompt"] = system_prompt
|
||||
|
||||
config = Config.from_json({"analyzer": analyzer})
|
||||
backend = OllamaBackend.from_config(config)
|
||||
result = analyze(natural_language, backend, config)
|
||||
return (result.data, format_messages(result.warnings), format_messages(result.errors))
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Tests for the Scene Analyzer ComfyUI node (issue #19). No live Ollama."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import nodes
|
||||
import nodes.scene_analyzer_node as node_module
|
||||
from compiler.common.message_codes import message
|
||||
from compiler.common.result import CompilerResult
|
||||
from nodes.scene_analyzer_node import SceneAnalyzerNode
|
||||
|
||||
|
||||
def test_input_types_structure() -> None:
|
||||
inputs = SceneAnalyzerNode.INPUT_TYPES()
|
||||
assert set(inputs["required"]) == {
|
||||
"natural_language",
|
||||
"model_name",
|
||||
"temperature",
|
||||
"timeout",
|
||||
}
|
||||
assert "system_prompt" in inputs["optional"]
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert SceneAnalyzerNode.RETURN_TYPES == ("SCENE", "STRING", "STRING")
|
||||
assert SceneAnalyzerNode.RETURN_NAMES == ("scene", "warnings", "errors")
|
||||
assert SceneAnalyzerNode.FUNCTION == "run"
|
||||
assert SceneAnalyzerNode.CATEGORY == "Scene Compiler"
|
||||
|
||||
|
||||
def test_run_delegates_to_analyzer_and_surfaces_messages(monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_analyze(description, backend, config):
|
||||
captured["description"] = description
|
||||
captured["model"] = config.analyzer.model
|
||||
return (
|
||||
CompilerResult(data="SCENE_MODEL")
|
||||
.add_warning(message("SC0001", "unknown concept 'x'"))
|
||||
.add_error(message("SC0011", "bad response"))
|
||||
)
|
||||
|
||||
monkeypatch.setattr(node_module, "analyze", fake_analyze)
|
||||
|
||||
scene, warnings, errors = SceneAnalyzerNode().run(
|
||||
natural_language="A girl.",
|
||||
model_name="mistral",
|
||||
temperature=0.0,
|
||||
timeout=60,
|
||||
)
|
||||
assert scene == "SCENE_MODEL"
|
||||
assert captured["description"] == "A girl."
|
||||
assert captured["model"] == "mistral"
|
||||
assert "SC0001: unknown concept 'x'" in warnings
|
||||
assert "SC0011: bad response" in errors
|
||||
|
||||
|
||||
def test_no_messages_yield_empty_strings(monkeypatch) -> None:
|
||||
monkeypatch.setattr(node_module, "analyze", lambda d, b, c: CompilerResult(data="S"))
|
||||
scene, warnings, errors = SceneAnalyzerNode().run("x", "llama3", 0.0, 60)
|
||||
assert (warnings, errors) == ("", "")
|
||||
|
||||
|
||||
def test_system_prompt_override_is_passed(monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_analyze(description, backend, config):
|
||||
captured["system_prompt"] = config.analyzer.system_prompt
|
||||
return CompilerResult(data="S")
|
||||
|
||||
monkeypatch.setattr(node_module, "analyze", fake_analyze)
|
||||
SceneAnalyzerNode().run("x", "llama3", 0.0, 60, system_prompt="CUSTOM")
|
||||
assert captured["system_prompt"] == "CUSTOM"
|
||||
|
||||
|
||||
def test_registered_for_comfyui() -> None:
|
||||
assert nodes.NODE_CLASS_MAPPINGS["SceneCompilerAnalyzer"] is SceneAnalyzerNode
|
||||
assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SceneCompilerAnalyzer"] == "Scene Analyzer"
|
||||
Reference in New Issue
Block a user