Compare commits

...
1 Commits
Author SHA1 Message Date
adachi 8f128f5417 Resolve exclusive inputs from submitted prompt connections 2026-10-16 14:33:42 +08:00
7 changed files with 67 additions and 11 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.5.11
- Resolve exclusive selection against the actual submitted prompt connections, overriding stale frontend flags when only one branch survives Get/Set, mute or bypass resolution.
- Support dynamically expanded subgraph node IDs through ComfyUI's dynamic prompt, while retaining explicit branch flags for bypassed transforms that keep both physical connections.
- Keep both-enabled/both-absent errors and include the received branch state in their diagnostic message. Restart ComfyUI after updating this backend change.
## 0.5.10
- Add a pin toggle beside Fit in Inline Mask: keep the image fitted, route plain wheel to the workflow, retain Ctrl+wheel brush sizing and save the toggle with the workflow.
+1
View File
@@ -161,6 +161,7 @@ Place the selector outside the two controlled groups and connect each group's fi
KJNodes Get/Set routes and reroutes are supported: an ignored/muted Get, its corresponding Set, or the source behind those routing nodes disables that selector branch. Missing setters and virtual routing cycles also count as unavailable. Ordinary processing nodes remain independent active sources; the selector does not guess the meaning of arbitrary processing chains.
The ignored branch is not requested by this selector; any separate preview/save outputs on that branch may still execute independently.
For raw API prompts, omit the ignored input and omit the internal `active_inputs` field; exactly one of `input_a` / `input_b` must remain.
The backend checks actual submitted connections after virtual routing: when only one input survives, it uses that branch even if cached frontend flags still report both enabled. With both connections present, the frontend flags remain necessary to distinguish bypassed transform pass-through from an enabled branch. Updating this backend logic requires a full ComfyUI restart as well as a browser refresh.
## Using a standard sampling workflow
+5 -4
View File
@@ -1,10 +1,11 @@
# Git versions and rollback
The current local version is **v0.5.10**. Both the installed plugin and the source repository retain local commits and tags. This update is also pushed to the GitHub main branch.
The current local version is **v0.5.11**. Both the installed plugin and the source repository retain local commits and tags. This update is also pushed to the GitHub main branch.
| Tag | Contents |
|---|---|
| `v0.5.10` | Pin inline fit, bound/cache inline previews, Upload label and restart comparison rounds after adoption |
| `v0.5.11` | Resolve exclusive selection from actual submitted connections despite stale frontend flags |
| `v0.5.10` / `local-before-v0.5.11` | Pin inline fit, bound/cache inline previews, Upload label and restart comparison rounds after adoption |
| `v0.5.9` / `local-before-v0.5.10` | Bound comparison bitmap allocations and redraw only the swept divider strip |
| `v0.5.8` / `local-before-v0.5.9` | Deduplicate replayed comparison previews and repair duplicate saved history |
| `v0.5.7` / `local-before-v0.5.8` | Fixed comparison images; wheel navigates the workflow; retain result history across runs |
@@ -22,13 +23,13 @@ To undo only this update, run in the plugin folder:
```powershell
git status --short
git switch --detach local-before-v0.5.10
git switch --detach local-before-v0.5.11
```
Return to this version with:
```powershell
git switch --detach v0.5.10
git switch --detach v0.5.11
```
Restart ComfyUI and hard-refresh the browser (`Ctrl+F5`) after switching versions.
+2
View File
@@ -1,5 +1,7 @@
# Verification record
- 0.5.11: all 32 backend CPU tests pass, including stale branch flags with either single submitted input, strict empty/ambiguous input errors, both-connected passthrough selection and dynamic subgraph node lookup. Fifteen real CPU queue cases verify single-branch A/B output pixels for flags -1 through 3, both-connected A/B selection, and invalid cases. Sixteen real KJ Get/Set queue cases on frontend 1.52.7 cover both renderers, source/Set/Get ignore and mute states, reverse switching, both-invalid states and workflow reload.
Environment: Windows, local ComfyUI 0.19.3, ComfyUI frontend 1.42.11, PyTorch 2.8.0, Pillow 11.0.0, Chrome 155, using a separate port 8190 CPU service with its own input/output directories and an in-memory database.
- 16 backend CPU tests pass, covering both sampling areas, edge masks, inversion, growth, feathering, surrounding-colour fill, batch mapping, source-pixel protection, empty masks, `denoise=0`, latent noise/zero modes, dedicated inpaint conditioning, different VAE compression ratios and Refiner staging.
+24 -6
View File
@@ -632,6 +632,7 @@ class AdachiExclusiveSwitch:
# including bypassed transform nodes whose links otherwise pass through.
"active_inputs": ("INT", {"default": -1, "min": -1, "max": 3}),
},
"hidden": {"prompt": "PROMPT", "dynprompt": "DYNPROMPT", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("*",)
@@ -642,25 +643,42 @@ class AdachiExclusiveSwitch:
SEARCH_ALIASES = ["exclusive switch", "auto switch", "自动二选一", "互斥", "自动切换", "adachi"]
@staticmethod
def _selected_input(input_a, input_b, active_inputs):
def _selected_input(input_a, input_b, active_inputs, prompt=None, dynprompt=None, unique_id=None):
execution_node = None
if dynprompt is not None and unique_id is not None:
try:
execution_node = dynprompt.get_node(unique_id)
except KeyError:
pass
if execution_node is None and isinstance(prompt, dict) and unique_id is not None:
execution_node = prompt.get(str(unique_id), prompt.get(unique_id))
if isinstance(execution_node, dict) and isinstance(execution_node.get("inputs"), dict):
# Get/Set and mute/bypass resolution may remove a connection after
# widgets serialize. Trust the actual submitted connections when
# only one (or none) survives, regardless of stale frontend flags.
connected = execution_node["inputs"]
actual = int("input_a" in connected) | (int("input_b" in connected) << 1)
if actual != 3:
active_inputs = actual
if active_inputs == -1:
# Raw API prompts need no frontend state: simply omit the unused input.
active_inputs = int(input_a is not _MISSING_INPUT) | (int(input_b is not _MISSING_INPUT) << 1)
if active_inputs not in (1, 2):
raise ValueError(
"Auto Exclusive Switch requires exactly one enabled input. "
"Bypass one branch or disconnect it; both enabled / both absent are invalid."
"Bypass one branch or disconnect it; both enabled / both absent are invalid. "
f"Received enabled-state={active_inputs}."
)
name = "input_a" if active_inputs == 1 else "input_b"
if (input_a if active_inputs == 1 else input_b) is _MISSING_INPUT:
raise ValueError(f"The enabled branch {name} has no input.")
return name
def check_lazy_status(self, input_a=_MISSING_INPUT, input_b=_MISSING_INPUT, active_inputs=-1):
return [self._selected_input(input_a, input_b, active_inputs)]
def check_lazy_status(self, input_a=_MISSING_INPUT, input_b=_MISSING_INPUT, active_inputs=-1, prompt=None, dynprompt=None, unique_id=None):
return [self._selected_input(input_a, input_b, active_inputs, prompt, dynprompt, unique_id)]
def select(self, input_a=_MISSING_INPUT, input_b=_MISSING_INPUT, active_inputs=-1):
selected = self._selected_input(input_a, input_b, active_inputs)
def select(self, input_a=_MISSING_INPUT, input_b=_MISSING_INPUT, active_inputs=-1, prompt=None, dynprompt=None, unique_id=None):
selected = self._selected_input(input_a, input_b, active_inputs, prompt, dynprompt, unique_id)
value = input_a if selected == "input_a" else input_b
if value is None:
raise ValueError(f"The enabled branch {selected} returned no data.")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "adachi-inpaint-workbench"
version = "0.5.10"
version = "0.5.11"
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
requires-python = ">=3.10"
license = { file = "LICENSE" }
+28
View File
@@ -42,6 +42,34 @@ class ExclusiveSwitchTests(unittest.TestCase):
self.assertIs(self.node.select(input_a=value)[0], value)
self.assertIs(self.node.select(input_b=value)[0], value)
def test_submitted_single_connection_overrides_stale_flags(self):
for name in ("input_a", "input_b"):
prompt = {"532": {"inputs": {name: ["source", 0], "active_inputs": 3}}}
for stale in (-1, 0, 1, 2, 3):
with self.subTest(name=name, stale=stale):
args = {name: None, "active_inputs": stale, "prompt": prompt, "unique_id": "532"}
self.assertEqual(self.node.check_lazy_status(**args), [name])
value = object()
args[name] = value
self.assertIs(self.node.select(**args)[0], value)
def test_submitted_empty_or_ambiguous_connections_still_fail(self):
for inputs in ({}, {"input_a": ["a", 0], "input_b": ["b", 0]}):
with self.subTest(inputs=inputs), self.assertRaisesRegex(ValueError, "exactly one"):
self.node.check_lazy_status(active_inputs=3, prompt={"1": {"inputs": inputs}}, unique_id="1")
prompt = {"1": {"inputs": {"input_a": ["a", 0], "input_b": ["b", 0]}}}
self.assertEqual(self.node.check_lazy_status(input_a=None, input_b=None, active_inputs=2, prompt=prompt, unique_id="1"), ["input_b"])
def test_dynamic_subgraph_connection_is_authoritative(self):
class DynamicPrompt:
def get_node(self, node_id):
self_id = "subgraph:532"
if node_id != self_id:
raise KeyError(node_id)
return {"inputs": {"input_b": ["nested-source", 0]}}
self.assertEqual(self.node.check_lazy_status(input_b=None, active_inputs=3,
prompt={}, dynprompt=DynamicPrompt(), unique_id="subgraph:532"), ["input_b"])
if __name__ == "__main__":
unittest.main()