diff --git a/docs/nodes.md b/docs/nodes.md index d5c0f45..c822f43 100644 --- a/docs/nodes.md +++ b/docs/nodes.md @@ -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` diff --git a/examples/workflows/scene_compiler_pipeline.json b/examples/workflows/scene_compiler_pipeline.json index 737791f..4d48937 100644 --- a/examples/workflows/scene_compiler_pipeline.json +++ b/examples/workflows/scene_compiler_pipeline.json @@ -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." ] }, { diff --git a/nodes/configuration_node.py b/nodes/configuration_node.py index 1dde1c4..665ee3f 100644 --- a/nodes/configuration_node.py +++ b/nodes/configuration_node.py @@ -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, ) diff --git a/nodes/debug_viewer_node.py b/nodes/debug_viewer_node.py index 953bbb8..7131c2c 100644 --- a/nodes/debug_viewer_node.py +++ b/nodes/debug_viewer_node.py @@ -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.", + }, ), } } diff --git a/nodes/resolver_node.py b/nodes/resolver_node.py index 3c188af..f26fbb5 100644 --- a/nodes/resolver_node.py +++ b/nodes/resolver_node.py @@ -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."}, ) }, } diff --git a/nodes/scene_analyzer_node.py b/nodes/scene_analyzer_node.py index c81085b..ddb293e 100644 --- a/nodes/scene_analyzer_node.py +++ b/nodes/scene_analyzer_node.py @@ -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 diff --git a/nodes/scene_validator_node.py b/nodes/scene_validator_node.py index b8c9775..944ce4b 100644 --- a/nodes/scene_validator_node.py +++ b/nodes/scene_validator_node.py @@ -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."}, ) }, } diff --git a/tests/test_configuration_node.py b/tests/test_configuration_node.py index f2f244d..73d2933 100644 --- a/tests/test_configuration_node.py +++ b/tests/test_configuration_node.py @@ -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: diff --git a/tests/test_scene_analyzer_node.py b/tests/test_scene_analyzer_node.py index 32c0bdb..577cf00 100644 --- a/tests/test_scene_analyzer_node.py +++ b/tests/test_scene_analyzer_node.py @@ -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"