Add Image Gate (Manual Router) design + implementation plan
Interactive chooser/router: pauses execution, shows the image with up to 10 labeled route buttons + edit-mask + stop. Chosen route gets the image, others ExecutionBlocker-ed; gate-painted mask on a fixed output; stop raises InterruptProcessingException. TDD plan with a pure torch-free gate_bus; lazy comfy imports keep node logic unit-testable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Image Gate (Manual Router) — Design
|
||||
|
||||
Date: 2026-06-21
|
||||
Status: Approved (brainstorming complete, ready for implementation plan)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
An interactive "image chooser on steroids": during a prompt run the node **pauses**,
|
||||
shows the incoming image with a row of labeled **route buttons**, and waits for a human
|
||||
click. Clicking **route K** sends the image down output K (all other route branches are
|
||||
silently skipped). A **Stop** button cancels the whole run. Optionally, an **Edit mask**
|
||||
button opens ComfyUI's MaskEditor on the image and the painted mask is emitted on a
|
||||
single `mask` output. Built for manual dataset sorting/gating in the "Dataset Gates" suite.
|
||||
|
||||
Third node in the `ComfyUI-Datasete-Gates` package.
|
||||
|
||||
## 2. IO
|
||||
|
||||
| dir | name | type | notes |
|
||||
|---|---|---|---|
|
||||
| in | `image` | IMAGE | the image (or batch, routed as one unit) |
|
||||
| widget | `routes` | INT, default 2, 1..10 | number of visible route buttons/outputs |
|
||||
| widget | per-route labels | (frontend) | editable, default `1..N`; rename the visible output slots |
|
||||
| hidden | `unique_id` | UNIQUE_ID | node id, used to key the pause/choice |
|
||||
| out | `mask` | MASK | **fixed slot 0**; painted at the gate, zeros (sized to image) if none |
|
||||
| out | `route_1 … route_10` | IMAGE | dynamic; JS shows only `routes` of them, labeled |
|
||||
|
||||
`RETURN_TYPES = ("MASK",) + ("IMAGE",)*10`. The node always returns all 11 outputs; the
|
||||
chosen route carries the image, every other route returns `ExecutionBlocker(None)`. JS
|
||||
hides the unused slots (>`routes`); their `ExecutionBlocker` returns are harmless.
|
||||
|
||||
## 3. Behavior (the pause)
|
||||
|
||||
On execute:
|
||||
1. Push the image to the UI (`PromptServer.send_sync`, base64 or temp file) so the node
|
||||
body shows the preview + the N labeled route buttons + **🖌 Edit mask** + **■ Stop**.
|
||||
2. **Block** the executor thread on our own `GateBus.wait(unique_id)` (a `MessageHolder`-
|
||||
style singleton in a `sleep(0.1)` loop; separate namespace from cg-image-picker).
|
||||
3. Resolve:
|
||||
- **route K** → image to output `K`, `ExecutionBlocker(None)` to the other routes;
|
||||
`mask` = the painted mask (or zeros).
|
||||
- **🖌 Edit mask** → opens MaskEditor (reuse the pool node's clipspace flow); the mask
|
||||
is POSTed to `/datasete_gate/mask` keyed by `unique_id` and picked up on resume.
|
||||
- **■ Stop** → cancel the prompt cleanly via
|
||||
`comfy.model_management.InterruptProcessingException` (confirm exact symbol in plan).
|
||||
|
||||
`IS_CHANGED` returns `nan` → the gate pauses on **every** run (never cached).
|
||||
|
||||
## 4. Why the global mask is safe
|
||||
|
||||
Verified in `execution.py:257-266` + `305-306`: if **any** input of a node is an
|
||||
`ExecutionBlocker`, the node is skipped and the blocker propagates to all its outputs.
|
||||
So a non-chosen route's downstream (which consumes the blocked routed image) never runs,
|
||||
regardless of the live `mask` value. Caveat: a node wired to `mask` *only* (no routed
|
||||
image) would run unconditionally — not the intended wiring.
|
||||
|
||||
## 5. Code shape (same package)
|
||||
|
||||
- `gates/gate.py` — `ImageGate` node: `INPUT_TYPES`, `IS_CHANGED=nan`, `run()` (push
|
||||
preview → block → route via `ExecutionBlocker`). Pure helper `route_tuple(chosen, image,
|
||||
blocker, max_routes)` for unit testing.
|
||||
- `gates/gate_server.py` — `GateBus` (start/put/wait/cancel) + mask stash; aiohttp routes
|
||||
`/datasete_gate/choice` and `/datasete_gate/mask`; `send_preview()` helper.
|
||||
- `web/image_gate.js` — dynamic labeled outputs (show `routes` of 10), preview render,
|
||||
route/stop/mask buttons, posts the choice; reuses the pool's MaskEditor helper.
|
||||
|
||||
## 6. Edge cases
|
||||
|
||||
- `routes` changed between runs → JS re-syncs visible slots; Python clamps `chosen` to
|
||||
`routes`.
|
||||
- Stop while no mask painted → clean interrupt, no output.
|
||||
- Multiple gates in one graph → execute sequentially (single executor thread), so only one
|
||||
blocks at a time; still keyed by `unique_id`.
|
||||
- Batch input → previewed as the first image / small grid; routed as one unit.
|
||||
- External queue-cancel → `GateBus` honors the cancel flag and raises.
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- pytest: `route_tuple` (image at chosen, blocker elsewhere, correct length); `GateBus`
|
||||
(pre-seeded message returns; cancel raises; `start` resets); mask zero-fallback sizing.
|
||||
- Manual (live): pause appears, buttons labeled, click routes image to the right branch
|
||||
and only that branch runs; Edit mask round-trips and feeds `mask`; Stop cancels cleanly;
|
||||
changing `routes` adds/removes slots.
|
||||
Reference in New Issue
Block a user