feat(config)!: trim Configuration inputs and rewrite tooltips (#150)

Remove settings the user should not have to touch, and make every tooltip
plain-language:

- knowledge_base path: removed; the Knowledge Base ships in the package, so the
  path is fixed rather than a prompted input.
- prompt_target: removed (reserved; to be replaced in V3).
- prompt_separator: removed; always ',' this version. Both keep their compiler
  defaults, just no longer exposed as node inputs.
- analyzer_system_prompt (Configuration) and system_prompt (Scene Analyzer):
  removed. A user-authored system prompt is not UX-friendly; the built-in analyzer
  prompt is always used.
- Rewrite all node tooltips in plain language reflecting the current nodes.

Example workflow and node tests updated; docs/nodes.md refreshed.

BREAKING CHANGE: the Configuration node's knowledge_base, prompt_target,
prompt_separator, and analyzer_system_prompt inputs and the Scene Analyzer's
system_prompt input are removed.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Crespin Alexandre
2026-07-16 17:37:28 +02:00
committed by GitHub
co-authored by kunail0804 Claude Opus 4.8
parent bc0edb1827
commit f2027a89f8
9 changed files with 97 additions and 199 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ default `knowledge_base/` works regardless of ComfyUI's working directory.
| Node | Inputs | Outputs |
|---|---|---|
| **Scene Analyzer** | `natural_language` (STRING), `config` (COMPILER_CONFIG, optional — supplies model/temperature/retries/timeout), `system_prompt` (STRING, optional override) | `scene` (SCENE), `warnings` (STRING), `errors` (STRING) |
| **Scene Analyzer** | `natural_language` (STRING), `config` (COMPILER_CONFIG, optional — supplies model/temperature/retries/timeout) | `scene` (SCENE), `warnings` (STRING), `errors` (STRING) |
| **Scene Validator** | `scene` (SCENE), `config` (COMPILER_CONFIG, optional) | `scene` (SCENE), `warnings` (STRING), `errors` (STRING) |
| **Resolver** | `scene` (SCENE), `knowledge_base` (KNOWLEDGE_BASE), `config` (COMPILER_CONFIG, optional) | `resolved_tags` (RESOLVED_TAGS), `warnings` (STRING), `errors` (STRING) |
| **Category Splitter** | `resolved_tags` (RESOLVED_TAGS) | `category_map` (CATEGORY_MAP), `warnings` (STRING), `errors` (STRING) |
@@ -31,7 +31,7 @@ prompt strings are produced by the **Prompt Builder**.
| Node | Inputs | Outputs |
|---|---|---|
| **Configuration** | one input per configuration option (analyzer, resolver, validator, prompt builder, semantic, debug), plus the Knowledge Base `knowledge_base` path, `resolver_knowledge_base_version`, and `knowledge_base_reload` | `config` (COMPILER_CONFIG), `knowledge_base` (KNOWLEDGE_BASE), `warnings` (STRING), `errors` (STRING) |
| **Configuration** | the settings a user needs to touch: analyzer (model, temperature, retries, timeout), resolver (strict mode, aliases, expansion, depth, `include_nsfw`, optional `resolver_knowledge_base_version`), validator (`allow_unknown_fields`), `prompt_remove_duplicate_tags`, the opt-in semantic-fallback settings, debug, and `knowledge_base_reload`. The Knowledge Base path is fixed (not user-facing); `prompt_target`/`prompt_separator` are not exposed (fixed defaults). | `config` (COMPILER_CONFIG), `knowledge_base` (KNOWLEDGE_BASE), `warnings` (STRING), `errors` (STRING) |
| **Debug Viewer** | `scene` (SCENE, optional), `resolved_tags` (RESOLVED_TAGS, optional), `category_map` (CATEGORY_MAP, optional), `warnings` (STRING, optional), `errors` (STRING, optional) | `report` (STRING) |
- The **Configuration** node is the single place for compiler settings. Its `config`
@@ -19,8 +19,8 @@
],
"properties": { "Node name for S&R": "SceneCompilerConfiguration" },
"widgets_values": [
"llama3", 0.0, 3, 300, "knowledge_base/", true, true, true, 8, false, false,
"easy_illustrious", ",", true, false, "basic", "", "", false, 0.5, "char_ngram", 0
"llama3", 0.0, 3, 300, true, true, true, 8, false, false, true, false, "basic",
"", false, 0.5, "char_ngram", 0
]
},
{
@@ -42,8 +42,7 @@
],
"properties": { "Node name for S&R": "SceneCompilerAnalyzer" },
"widgets_values": [
"A blonde girl wearing a dress smiles in a classroom at sunset.",
""
"A blonde girl wearing a dress smiles in a classroom at sunset."
]
},
{
+48 -82
View File
@@ -1,10 +1,9 @@
"""ComfyUI node for the compiler Configuration (MASTER_SPEC §20, §23).
A thin interface: it collects the configuration options as node inputs and emits a
single COMPILER_CONFIG that the other nodes consume, so behaviour can change without
editing the workflow. It also loads the Knowledge Base from the configured path and
emits it for the Resolver, so the Knowledge Base directory is entered in exactly one
place (there is no separate Knowledge Base Loader node). It never imports ComfyUI.
A thin interface: it collects the settings a user actually needs to touch and emits
a single COMPILER_CONFIG the other nodes consume, so behaviour can change without
editing the workflow. It also loads the Knowledge Base and emits it for the Resolver,
so the pipeline needs no separate loader node. It never imports ComfyUI.
"""
from __future__ import annotations
@@ -18,9 +17,13 @@ from .kb_loading import load_cached_knowledge_base
_DEBUG_LEVELS = ["none", "basic", "verbose", "developer"]
# The Knowledge Base ships inside the package, so its path is fixed rather than a
# user-facing setting.
_KNOWLEDGE_BASE_PATH = "knowledge_base/"
class ConfigurationNode:
"""Centralizes compiler configuration and loads the Knowledge Base for the pipeline."""
"""Centralizes compiler settings and loads the Knowledge Base for the pipeline."""
CATEGORY = "Scene Compiler"
FUNCTION = "run"
@@ -35,7 +38,7 @@ class ConfigurationNode:
"STRING",
{
"default": "llama3",
"tooltip": "Ollama model the Scene Analyzer uses to read the description.",
"tooltip": "Which local Ollama model reads your description (e.g. llama3).",
},
),
"analyzer_temperature": (
@@ -45,7 +48,7 @@ class ConfigurationNode:
"min": 0.0,
"max": 2.0,
"step": 0.1,
"tooltip": "Analyzer sampling temperature. 0 = most repeatable.",
"tooltip": "How freely the model interprets. 0 = most consistent.",
},
),
"analyzer_max_retries": (
@@ -54,7 +57,7 @@ class ConfigurationNode:
"default": 3,
"min": 0,
"max": 10,
"tooltip": "How many times to re-ask when the model returns bad JSON.",
"tooltip": "How many times to retry if the model returns a bad answer.",
},
),
"analyzer_timeout": (
@@ -64,33 +67,31 @@ class ConfigurationNode:
"min": 1,
"max": 3600,
"tooltip": (
"Seconds to wait for the model. 300 s is generous so a model that "
"cold-loads into VRAM on the first call does not time out."
),
},
),
"knowledge_base": (
"STRING",
{
"default": "knowledge_base/",
"tooltip": (
"Knowledge Base directory. Relative paths resolve against the node "
"package. This is the authoritative path when this Configuration is "
"wired into the Knowledge Base Loader."
"How long to wait for the model, in seconds. The default is generous "
"so the first run (while the model loads) doesn't time out."
),
},
),
"resolver_strict_mode": (
"BOOLEAN",
{"default": True, "tooltip": "Report unknown concepts instead of guessing."},
{
"default": True,
"tooltip": "Report concepts not in the Knowledge Base instead of guessing.",
},
),
"resolver_allow_aliases": (
"BOOLEAN",
{"default": True, "tooltip": "Resolve aliases to their canonical entry."},
{
"default": True,
"tooltip": "Map synonyms to the same tag (e.g. 'girl' = 'female').",
},
),
"resolver_expansion_enabled": (
"BOOLEAN",
{"default": True, "tooltip": "Auto-add tags from an entry's expansion list."},
{
"default": True,
"tooltip": "Automatically add related tags (e.g. an outfit's parts).",
},
),
"resolver_max_expansion_depth": (
"INT",
@@ -98,69 +99,47 @@ class ConfigurationNode:
"default": 8,
"min": 1,
"max": 32,
"tooltip": "How deep expansion may recurse before stopping.",
"tooltip": "How far related tags are followed. Higher adds more tags.",
},
),
"resolver_include_nsfw": (
"BOOLEAN",
{
"default": False,
"tooltip": "Include explicit-rated Knowledge Base entries. Off = SFW only.",
"tooltip": "Allow explicit (NSFW) tags. Off keeps results safe-for-work.",
},
),
"validator_allow_unknown_fields": (
"BOOLEAN",
{
"default": False,
"tooltip": "Keep unrecognized Scene JSON fields instead of stripping them.",
},
),
"prompt_target": (
"STRING",
{
"default": "easy_illustrious",
"tooltip": "Reserved prompt-format label. Currently informational.",
},
),
"prompt_separator": (
"STRING",
{
"default": ",",
"tooltip": "String used to join the resolved tags into the final prompt.",
"tooltip": "Keep unexpected fields in the scene data. Usually leave off.",
},
),
"prompt_remove_duplicate_tags": (
"BOOLEAN",
{
"default": True,
"tooltip": "Drop duplicate tags after expansion (warns SC0007).",
"tooltip": "Remove repeated tags from the final prompt. Recommended on.",
},
),
"debug_enabled": (
"BOOLEAN",
{"default": False, "tooltip": "Enable extra diagnostic logging."},
{"default": False, "tooltip": "Turn on extra logging to troubleshoot."},
),
"debug_level": (
_DEBUG_LEVELS,
{"default": "basic", "tooltip": "Verbosity of debug logging when enabled."},
{"default": "basic", "tooltip": "How much detail the logs include."},
),
},
# Appended at the END so saved workflows keep their positional widget
# values (ComfyUI stores widget values positionally).
"optional": {
"analyzer_system_prompt": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "Advanced: override the analyzer system prompt.",
},
),
"resolver_knowledge_base_version": (
"STRING",
{
"default": "",
"tooltip": "Pin a KB dataset version. Empty = unpinned.",
"tooltip": "Advanced: pin a Knowledge Base version. Empty = default.",
},
),
"semantic_enabled": (
@@ -168,8 +147,8 @@ class ConfigurationNode:
{
"default": False,
"tooltip": (
"Opt-in nearest-neighbour fallback for concepts that miss "
"deterministic lookup. Off by default; deterministic lookup wins."
"Find the closest known tag when a concept isn't recognized. Off by "
"default; it never invents tags."
),
},
),
@@ -180,14 +159,14 @@ class ConfigurationNode:
"min": 0.0,
"max": 1.0,
"step": 0.05,
"tooltip": "Minimum similarity to accept a semantic-fallback match.",
"tooltip": "How close a match must be for the closest-tag search.",
},
),
"semantic_backend": (
"STRING",
{
"default": "char_ngram",
"tooltip": "Embedding backend for the semantic fallback (offline).",
"tooltip": "Advanced: which method the closest-tag search uses.",
},
),
"knowledge_base_reload": (
@@ -195,7 +174,7 @@ class ConfigurationNode:
{
"default": 0,
"min": 0,
"tooltip": "Bump to force a fresh read of the Knowledge Base from disk.",
"tooltip": "Bump this if you edited the Knowledge Base, to reload it.",
},
),
},
@@ -207,38 +186,29 @@ class ConfigurationNode:
analyzer_temperature: float,
analyzer_max_retries: int,
analyzer_timeout: int,
knowledge_base: str,
resolver_strict_mode: bool,
resolver_allow_aliases: bool,
resolver_expansion_enabled: bool,
resolver_max_expansion_depth: int,
resolver_include_nsfw: bool,
validator_allow_unknown_fields: bool,
prompt_target: str,
prompt_separator: str,
prompt_remove_duplicate_tags: bool,
debug_enabled: bool,
debug_level: str,
analyzer_system_prompt: str = "",
resolver_knowledge_base_version: str = "",
semantic_enabled: bool = False,
semantic_min_similarity: float = 0.5,
semantic_backend: str = "char_ngram",
knowledge_base_reload: int = 0,
) -> tuple[Any, Any, str, str]:
analyzer: dict[str, Any] = {
"model": analyzer_model,
"temperature": analyzer_temperature,
"max_retries": analyzer_max_retries,
"timeout": analyzer_timeout,
}
if analyzer_system_prompt:
analyzer["system_prompt"] = analyzer_system_prompt
document = {
"analyzer": analyzer,
"analyzer": {
"model": analyzer_model,
"temperature": analyzer_temperature,
"max_retries": analyzer_max_retries,
"timeout": analyzer_timeout,
},
"resolver": {
"knowledge_base": knowledge_base,
"strict_mode": resolver_strict_mode,
"allow_aliases": resolver_allow_aliases,
"expansion_enabled": resolver_expansion_enabled,
@@ -256,11 +226,7 @@ class ConfigurationNode:
"min_similarity": semantic_min_similarity,
"backend": semantic_backend,
},
"prompt_builder": {
"target": prompt_target,
"separator": prompt_separator,
"remove_duplicate_tags": prompt_remove_duplicate_tags,
},
"prompt_builder": {"remove_duplicate_tags": prompt_remove_duplicate_tags},
"debug": {"enabled": debug_enabled, "level": debug_level},
}
@@ -269,10 +235,10 @@ class ConfigurationNode:
except ConfigError as error:
return (None, None, "", format_messages([error.message]))
# Load the Knowledge Base from the configured path/version and emit it for
# the Resolver, so the Knowledge Base directory is entered in one place.
# Load the shipped Knowledge Base and emit it for the Resolver, so the
# Knowledge Base is loaded in one place with no separate loader node.
kb, warnings, errors, _raw = load_cached_knowledge_base(
knowledge_base,
_KNOWLEDGE_BASE_PATH,
resolver_knowledge_base_version,
knowledge_base_reload,
)
+11 -3
View File
@@ -28,15 +28,23 @@ class DebugViewerNode:
"optional": {
"scene": (
"SCENE",
{"tooltip": "Scene JSON to inspect (Analyzer/Validator output)."},
{"tooltip": "Any scene data to inspect (from the Analyzer or Validator)."},
),
"warnings": (
"STRING",
{"default": "", "forceInput": True, "tooltip": "A stage's warnings output."},
{
"default": "",
"forceInput": True,
"tooltip": "A stage's warnings output to view.",
},
),
"errors": (
"STRING",
{"default": "", "forceInput": True, "tooltip": "A stage's errors output."},
{
"default": "",
"forceInput": True,
"tooltip": "A stage's errors output to view.",
},
),
}
}
+3 -8
View File
@@ -30,21 +30,16 @@ class ResolverNode:
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"scene": ("SCENE", {"tooltip": "Validated Scene JSON from the Scene Validator."}),
"scene": ("SCENE", {"tooltip": "The checked scene data from the Scene Validator."}),
"knowledge_base": (
"KNOWLEDGE_BASE",
{"tooltip": "Loaded Knowledge Base from the Knowledge Base Loader."},
{"tooltip": "The Knowledge Base from the Configuration node."},
),
},
"optional": {
"config": (
"COMPILER_CONFIG",
{
"tooltip": (
"Optional Configuration node. Supplies resolver options, the prompt "
"separator, duplicate handling and the opt-in semantic fallback."
)
},
{"tooltip": "Optional: connect a Configuration node to set resolver options."},
)
},
}
+11 -42
View File
@@ -4,12 +4,11 @@ A thin interface: it adapts ComfyUI inputs to the Analyzer module and surfaces t
CompilerResult's data, warnings, and errors. It contains no compiler logic and
never imports ComfyUI.
Analyzer settings (model, temperature, retries, timeout, system prompt) live on
the **Configuration** node and reach this node through the optional ``config``
input, so a setting like the model name is entered in exactly one place. Without a
Configuration node the built-in defaults apply (llama3, temperature 0), with a
generous 300 s timeout so a local Ollama model that cold-loads into VRAM on the
first call does not time out.
Analyzer settings (model, temperature, retries, timeout) live on the **Configuration**
node and reach this node through its optional ``config`` input, so they are entered in
one place. Without a Configuration node the built-in defaults apply (llama3,
temperature 0) with a generous 300 s timeout, because a local Ollama model cold-loads
into VRAM on the first call and a shorter timeout often fails on first use.
"""
from __future__ import annotations
@@ -44,7 +43,7 @@ class SceneAnalyzerNode:
{
"multiline": True,
"default": "",
"tooltip": "The scene description to analyze, in plain language.",
"tooltip": "Describe your scene in plain language.",
},
),
},
@@ -53,23 +52,11 @@ class SceneAnalyzerNode:
"COMPILER_CONFIG",
{
"tooltip": (
"Optional Configuration node. Supplies the analyzer model, "
"temperature, retries and timeout. If left unconnected, the "
"defaults apply (llama3, temperature 0, 300 s timeout)."
"Optional: connect a Configuration node to set the model and other "
"options. Without it, sensible defaults are used."
)
},
),
"system_prompt": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": (
"Advanced: override the analyzer system prompt. Leave empty to "
"use the built-in prompt (or the one from the Configuration node)."
),
},
),
},
}
@@ -77,9 +64,10 @@ class SceneAnalyzerNode:
self,
natural_language: str,
config: Config | None = None,
system_prompt: str = "",
) -> tuple[Any, str, str, str]:
config = self._resolve_config(config, system_prompt)
if config is None:
# No Configuration node: use defaults with a cold-load-friendly timeout.
config = Config.from_json({"analyzer": {"timeout": _DEFAULT_TIMEOUT}})
backend = OllamaBackend.from_config(config)
result = analyze(natural_language, backend, config)
return (
@@ -91,22 +79,3 @@ class SceneAnalyzerNode:
# re-serialization of the parsed Scene.
result.metadata.get("raw_response", ""),
)
@staticmethod
def _resolve_config(config: Config | None, system_prompt: str) -> Config:
"""Pick the effective config, applying the optional system-prompt override.
No Configuration node: build a default config with a cold-load-friendly
timeout. A non-empty ``system_prompt`` widget always wins over whatever the
Configuration node carries.
"""
if config is None:
analyzer: dict[str, Any] = {"timeout": _DEFAULT_TIMEOUT}
if system_prompt:
analyzer["system_prompt"] = system_prompt
return Config.from_json({"analyzer": analyzer})
if system_prompt:
document = config.to_json()
document.setdefault("analyzer", {})["system_prompt"] = system_prompt
return Config.from_json(document)
return config
+2 -2
View File
@@ -27,12 +27,12 @@ class SceneValidatorNode:
def INPUT_TYPES(cls) -> dict[str, Any]:
return {
"required": {
"scene": ("SCENE", {"tooltip": "Raw Scene JSON from the Scene Analyzer."}),
"scene": ("SCENE", {"tooltip": "The scene data from the Scene Analyzer."}),
},
"optional": {
"config": (
"COMPILER_CONFIG",
{"tooltip": "Optional Configuration node; supplies the unknown-field policy."},
{"tooltip": "Optional: connect a Configuration node."},
)
},
}
+14 -26
View File
@@ -14,15 +14,12 @@ def default_inputs() -> dict:
"analyzer_temperature": 0.0,
"analyzer_max_retries": 3,
"analyzer_timeout": 60,
"knowledge_base": "knowledge_base/",
"resolver_strict_mode": True,
"resolver_allow_aliases": True,
"resolver_expansion_enabled": True,
"resolver_max_expansion_depth": 8,
"resolver_include_nsfw": False,
"validator_allow_unknown_fields": False,
"prompt_target": "easy_illustrious",
"prompt_separator": ",",
"prompt_remove_duplicate_tags": True,
"debug_enabled": False,
"debug_level": "basic",
@@ -43,8 +40,13 @@ def test_node_metadata() -> None:
required = ConfigurationNode.INPUT_TYPES()["required"]
assert "analyzer_model" in required
assert "debug_level" in required
# Removed, leaner inputs: no user-facing KB path, prompt target/separator, or
# analyzer system-prompt override.
optional = ConfigurationNode.INPUT_TYPES()["optional"]
assert "analyzer_system_prompt" in optional
assert "knowledge_base" not in required
assert "prompt_target" not in required
assert "prompt_separator" not in required
assert "analyzer_system_prompt" not in optional
assert "knowledge_base_reload" in optional
@@ -52,7 +54,7 @@ def test_default_inputs_produce_default_config_and_load_the_kb() -> None:
config, kb, warnings, errors = ConfigurationNode().run(**default_inputs())
assert isinstance(config, Config)
assert config == Config()
# The node now also loads the shipped reference Knowledge Base.
# The node loads the shipped reference Knowledge Base from its fixed path.
assert isinstance(kb, KnowledgeBase)
assert len(kb) > 100
assert (warnings, errors) == ("", "")
@@ -64,7 +66,7 @@ def test_inputs_flow_into_config() -> None:
inputs["resolver_max_expansion_depth"] = 4
inputs["resolver_include_nsfw"] = True
inputs["validator_allow_unknown_fields"] = True
inputs["prompt_separator"] = " | "
inputs["prompt_remove_duplicate_tags"] = False
inputs["debug_enabled"] = True
inputs["debug_level"] = "verbose"
config, _kb, _warnings, _errors = ConfigurationNode().run(**inputs)
@@ -72,30 +74,16 @@ def test_inputs_flow_into_config() -> None:
assert config.resolver.max_expansion_depth == 4
assert config.resolver.include_nsfw is True
assert config.validator.allow_unknown_fields is True
assert config.prompt_builder.separator == " | "
assert config.prompt_builder.remove_duplicate_tags is False
assert config.debug.enabled is True
assert config.debug.level == "verbose"
def test_invalid_knowledge_base_path_surfaces_error_but_keeps_config() -> None:
inputs = default_inputs()
inputs["knowledge_base"] = "does/not/exist/"
config, kb, warnings, errors = ConfigurationNode().run(**inputs)
# The config is still emitted (the Analyzer/Validator do not need the KB); the
# empty/missing Knowledge Base is surfaced as a warning.
assert isinstance(config, Config)
assert len(kb) == 0
assert "empty" in warnings.lower()
def test_system_prompt_override_is_applied() -> None:
config, *_ = ConfigurationNode().run(**default_inputs(), analyzer_system_prompt="CUSTOM")
assert config.analyzer.system_prompt == "CUSTOM"
def test_empty_system_prompt_is_omitted() -> None:
config, *_ = ConfigurationNode().run(**default_inputs(), analyzer_system_prompt="")
assert config.analyzer.system_prompt is None
def test_prompt_target_and_separator_keep_their_defaults() -> None:
# These are no longer user-facing; the compiler defaults still apply.
config, *_ = ConfigurationNode().run(**default_inputs())
assert config.prompt_builder.target == "easy_illustrious"
assert config.prompt_builder.separator == ","
def test_debug_level_options_match_config_schema() -> None:
+3 -30
View File
@@ -12,10 +12,10 @@ from nodes.scene_analyzer_node import SceneAnalyzerNode
def test_input_types_structure() -> None:
inputs = SceneAnalyzerNode.INPUT_TYPES()
# Analyzer settings now live on the Configuration node; only the per-run text
# and the optional config/override remain.
# Analyzer settings live on the Configuration node; only the per-run text and
# the optional config connection remain.
assert set(inputs["required"]) == {"natural_language"}
assert set(inputs["optional"]) == {"config", "system_prompt"}
assert set(inputs["optional"]) == {"config"}
def test_node_metadata() -> None:
@@ -72,33 +72,6 @@ def test_no_messages_yield_empty_strings(monkeypatch) -> None:
assert (warnings, errors) == ("", "")
def test_system_prompt_override_without_config(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", system_prompt="CUSTOM")
assert captured["system_prompt"] == "CUSTOM"
def test_system_prompt_override_wins_over_config(monkeypatch) -> None:
captured = {}
def fake_analyze(description, backend, config):
captured["system_prompt"] = config.analyzer.system_prompt
captured["model"] = config.analyzer.model
return CompilerResult(data="S")
monkeypatch.setattr(node_module, "analyze", fake_analyze)
config = Config.from_json({"analyzer": {"model": "mistral", "system_prompt": "FROM_CONFIG"}})
SceneAnalyzerNode().run("x", config=config, system_prompt="OVERRIDE")
assert captured["system_prompt"] == "OVERRIDE"
assert captured["model"] == "mistral"
def test_registered_for_comfyui() -> None:
assert nodes.NODE_CLASS_MAPPINGS["SceneCompilerAnalyzer"] is SceneAnalyzerNode
assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SceneCompilerAnalyzer"] == "Scene Analyzer"