Files
ComfyUI-Dataset-Gates/docs/plans/2026-06-21-image-gate-bus-design.md
T
Ethanfel 95b3417ff6 Add Image Gate send/get bus design + implementation plan
Disk-backed image bus (input/gate_bus/<id>/): gates auto-publish image+mask
to a named send_id on pass; when image input is empty they load from get_id
(dropdown) — wireless, cycle-free "restart from the gate point" across runs.
Making image optional implements ignore-on-normal-path. TDD plan with a pure
stdlib imagebus + tensor savers; comfy imports stay lazy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:45:15 +02:00

4.4 KiB

Image Gate — Send/Get Bus (teleport + checkpoint) — Design

Date: 2026-06-21 Status: Approved (brainstorming complete, ready for implementation plan)

1. Purpose

Let Image Gates pass images to each other by name through a disk-backed bus, so you can re-enter the pipeline at a gate after manual editing/looking — without dragging wires and without creating graph cycles. A gate auto-publishes its passed image (+ mask) to a named slot; another gate (or a fresh workflow) loads that slot to resume from that point.

This is an enhancement to the existing Image Gate (Manual Router) — no new node.

2. Why no wire / no cycle

ComfyUI graphs must be acyclic; a real wire from a downstream gate's output back into an upstream gate is a cycle and is rejected at validation. The bus links sender↔receiver by a string id, so there is no live wire and no cycle. "Ignore on the normal path" falls out naturally from making image optional (see §4).

3. Changes to the Image Gate

New ports/widgets (all backward compatible):

Port Type Description
image IMAGE now optional. Wired → normal path. Empty → load from get_id.
send_id STRING (widget) If non-empty, on every pass the chosen image + mask are written to the bus slot send_id (latest-wins). Empty = don't publish.
get_id STRING (widget, dropdown) Used only when image is not connected: load the latest image + mask from this bus slot, then gate as usual. Dropdown lists existing bus ids.

Existing inputs (routes) and outputs (mask, route_1..route_10) are unchanged.

4. Run logic

base = input/gate_bus
image, loaded_mask = resolve_source(base, image, get_id)
    # image given  -> (image, None)                     [normal path; get ignored]
    # else get_id  -> load (image, mask) from bus slot  [re-entry]
    # else         -> nothing: block all routes silently, return zero mask
pause + wait (Stop -> InterruptProcessingException)     [unchanged]
mask = painted-at-gate  OR  loaded_mask  OR  zeros      [precedence]
if send_id: write image+mask to bus[send_id]            [auto-publish on pass]
return (mask,) + route_tuple(chosen)                    [unchanged routing]

IS_CHANGED stays nan (always pauses). A gate with no image and no valid get_id is a silent no-op (all routes ExecutionBlocker, zero mask) so it never breaks a graph.

5. Bus storage

input/gate_bus/<id>/
├── image.png     # latest passed image for this slot
└── mask.png      # its mask (white = painted)

Latest-wins (overwrite). id is the human-chosen name. Survives restart → cross-run resume.

6. Frontend (web/image_gate.js)

  • Make the image input optional (litegraph) — the node works with it empty.
  • send_id: a plain text widget.
  • get_id: render as a dropdown populated from GET /datasete_gate/bus/list (refresh when opened), plus free-text entry.
  • Pause/preview UI unchanged — send_preview runs after the source is resolved, so get-loaded images preview correctly.

7. Code shape

  • gates/imagebus.py (new, stdlib) — slot paths, has, ensure_dir, list_ids, delete_id. Unit-testable.
  • gates/imaging.py (additive)save_image_tensor, save_mask_tensor (mirror the existing loaders). Unit-testable with torch.
  • gates/gate.py (additive)bus_save/bus_load, pure resolve_source, and the run() wiring (optional image, publish on pass). comfy imports stay lazy.
  • gates/gates_compat.py (additive)gate_bus_base()input/gate_bus.
  • gates/gate_server.py (additive)GET /datasete_gate/bus/list.

8. Edge cases

  • image empty + get_id empty/missing → silent no-op (no pause, all blocked).
  • Mask precedence: gate-painted > loaded-from-bus > zeros.
  • Same send_id from multiple gates → latest pass wins (documented).
  • get_id referencing a deleted slot → treated as missing (no-op).
  • Cross-run: publish in run A, load in run B (even after restart) — that's the whole point.

9. Testing

  • pytest: imagebus (paths/has/list/delete); imaging save→load round-trip (shapes, mask polarity); gate.resolve_source (image wins / get loads / nothing → None); bus_save+ bus_load round-trip.
  • Manual (live): publish at gate A (send_id=cp1), then a gate with empty image + get_id=cp1 loads it (even in a new workflow), edit mask, route onward; dropdown lists ids; normal wired path ignores the bus.