Resolve fixed switch selections during preflight
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)}: "
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user