Resolve fixed switch selections during preflight

This commit is contained in:
Dominik Bargiel
2026-09-23 16:07:10 +02:00
parent 7943205e47
commit 99cc71714e
3 changed files with 100 additions and 6 deletions
+2 -2
View File
@@ -100,13 +100,13 @@ Every variation is checked on the selected Deadline worker after input staging,
- validates every `class_type` against that worker's live `/object_info`;
- rejects missing staged inputs and known browser-interactive nodes;
- removes fixed `ImpactSwitch`, `LatentSwitch`, and `SEGSSwitch` pass-through nodes by reconnecting consumers to the selected upstream input;
- resolves fixed selections from literal values or safe primitive value nodes, then removes `ImpactSwitch`, `LatentSwitch`, and `SEGSSwitch` pass-through nodes by reconnecting consumers to the selected upstream input;
- sends the complete submitted `extra_pnginfo.workflow` to metadata-aware nodes;
- verifies that ComfyUI history reports the expected output nodes and that every reported file exists;
- permits the exact `DeadlineWorkerRegistration` control prompt when both worker and distributed modes are enabled; this prompt completes through history and intentionally produces no file;
- turns prompt-validation errors, execution exceptions, early process exits, HTTP timeouts, missing output, and Deadline task timeouts into task failures.
Dynamic/connected switch selections are not rewritten. Browser-driven chooser/picker/preview-bridge nodes (`FL_ImagePicker`, `easy imageChooser`, `ImageChooser`, `PreviewChooser`, `PreviewBridge`, and `ImpactPreviewBridge`) are rejected because a farm render has no user to answer them. Other nodes using `PROMPT`, `DYNPROMPT`, `UNIQUE_ID`, or `EXTRA_PNGINFO` remain supported when the worker has the node and the submission includes full workflow metadata. Unknown third-party interactive nodes cannot be identified from `/object_info` alone; add their exact `class_type` to `KNOWN_UI_DEPENDENT_NODE_TYPES` after confirming that they wait for frontend state.
Dynamic switch selections are accepted when their selector is driven by a safe fixed-value node such as `PrimitiveInt`; the selected branch is resolved while submitting. Selectors driven by runtime or interactive nodes are rejected because a farm render has no user to answer them. Browser-driven chooser/picker/preview-bridge nodes (`FL_ImagePicker`, `easy imageChooser`, `ImageChooser`, `PreviewChooser`, `PreviewBridge`, and `ImpactPreviewBridge`) are rejected for the same reason. Other nodes using `PROMPT`, `DYNPROMPT`, `UNIQUE_ID`, or `EXTRA_PNGINFO` remain supported when the worker has the node and the submission includes full workflow metadata. Unknown third-party interactive nodes cannot be identified from `/object_info` alone; add their exact `class_type` to `KNOWN_UI_DEPENDENT_NODE_TYPES` after confirming that they wait for frontend state.
## Maintenance Scripts
+63 -4
View File
@@ -84,6 +84,21 @@ FIXED_SWITCH_SPECS = {
"SEGSSwitch": {"select": "select", "input": "input{index}"},
}
# These nodes expose a literal value through output 0. They are safe to
# evaluate during submission because their value is part of the API prompt;
# they do not depend on frontend interaction or worker runtime state.
FIXED_VALUE_NODE_TYPES = {
"PrimitiveBoolean",
"PrimitiveFloat",
"PrimitiveInt",
"PrimitiveString",
"PrimitiveNode",
"easy boolean",
"easy float",
"easy int",
"easy string",
}
FILE_OUTPUT_GROUPS = ("images", "gifs", "videos", "audio")
def get_distributed_config_for_plugin(plugin) -> Tuple[bool, bool, bool]:
@@ -1516,8 +1531,44 @@ sys.exit(0 if result.get('success') else 1)
def _is_link(value):
return isinstance(value, (list, tuple)) and len(value) == 2 and str(value[0]) != ""
def _resolve_fixed_value(self, prompt, value, workflow_metadata=None, visited=None):
"""Resolve a switch selector connected to a literal-value node."""
if not self._is_link(value):
return value, None
source_id = str(value[0])
try:
output_index = int(value[1])
except (TypeError, ValueError):
return None, None
if output_index != 0:
return None, None
visited = set() if visited is None else set(visited)
if source_id in visited:
return None, None
visited.add(source_id)
source = prompt.get(source_id)
if not isinstance(source, dict) or source.get("class_type") not in FIXED_VALUE_NODE_TYPES:
return None, None
source_inputs = source.get("inputs", {})
if not isinstance(source_inputs, dict) or "value" not in source_inputs:
return None, None
resolved, resolved_from = self._resolve_fixed_value(
prompt,
source_inputs["value"],
workflow_metadata,
visited,
)
if resolved_from is None and resolved is not None:
resolved_from = source_id
return resolved, resolved_from
def _resolve_fixed_switches(self, prompt, workflow_metadata=None):
"""Remove supported constant-selection switches and reconnect consumers."""
"""Remove supported fixed-selection switches and reconnect consumers."""
for switch_id, switch in list(prompt.items()):
if not isinstance(switch, dict):
continue
@@ -1527,13 +1578,21 @@ sys.exit(0 if result.get('success') else 1)
continue
inputs = switch.get("inputs", {})
raw_select = inputs.get(spec["select"])
if self._is_link(raw_select):
selected_index, selected_from = self._resolve_fixed_value(
prompt, raw_select, workflow_metadata
)
if selected_from is not None:
self.LogInfo(
f"Headless preflight resolved {class_type} node {switch_id} "
f"selection from fixed value node {selected_from}: {selected_index}."
)
elif self._is_link(raw_select):
raise ComfyUIError(
f"Headless preflight rejected dynamic switch {self._node_context(switch_id, prompt, workflow_metadata)}: "
"the selection is connected and cannot be proven fixed."
"the selection is connected to a non-fixed value."
)
try:
selected_index = int(raw_select)
selected_index = int(selected_index)
except (TypeError, ValueError):
raise ComfyUIError(
f"Headless preflight rejected {self._node_context(switch_id, prompt, workflow_metadata)}: "
+35
View File
@@ -31,6 +31,10 @@ def object_info():
"input": {"required": {}, "hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"}},
"output_node": False,
},
"PrimitiveInt": {
"input": {"required": {"value": [["INT", {}]]}, "hidden": {}},
"output_node": False,
},
"FL_ImagePicker": {"input": {"required": {}, "hidden": {"unique_id": "UNIQUE_ID"}}, "output_node": False},
"Explode": {"input": {"required": {}, "hidden": {}}, "output_node": False},
"DeadlineWorkerRegistration": {
@@ -79,6 +83,37 @@ class HeadlessPreflightTests(unittest.TestCase):
self.assertEqual(result["2"]["inputs"]["images"], ["4", 0])
self.assertEqual(result["2"]["inputs"]["label"], "input2")
def test_switch_selection_from_fixed_primitive_is_resolved(self):
with tempfile.TemporaryDirectory() as temp:
plugin = self.make_plugin(Path(temp))
prompt = {
"3": {"class_type": "Explode", "inputs": {}},
"4": {"class_type": "Explode", "inputs": {}},
"5": {
"class_type": "ImpactSwitch",
"inputs": {"select": ["6", 0], "input1": ["3", 0], "input2": ["4", 0]},
},
"6": {"class_type": "PrimitiveInt", "inputs": {"value": 2}},
"2": {"class_type": "SaveImage", "inputs": {"images": ["5", 0]}},
}
result, _ = plugin._preflight_worker_payload(prompt, WORKFLOW)
self.assertNotIn("5", result)
self.assertEqual(result["2"]["inputs"]["images"], ["4", 0])
def test_dynamic_switch_value_node_is_still_rejected(self):
with tempfile.TemporaryDirectory() as temp:
plugin = self.make_plugin(Path(temp))
with self.assertRaisesRegex(ComfyUIError, "connected to a non-fixed value"):
plugin._preflight_worker_payload({
"3": {"class_type": "Explode", "inputs": {}},
"4": {"class_type": "Explode", "inputs": {}},
"5": {
"class_type": "ImpactSwitch",
"inputs": {"select": ["3", 0], "input1": ["3", 0], "input2": ["4", 0]},
},
"2": {"class_type": "SaveImage", "inputs": {"images": ["5", 0]}},
}, WORKFLOW)
def test_gui_dependent_node_is_rejected_with_context(self):
with tempfile.TemporaryDirectory() as temp:
plugin = self.make_plugin(Path(temp))