Skip to content

Commit 4a9ebcc

Browse files
spellcaster_core: Phase 9 — websocket dispatch + ETN inline transport
Closes the loop on the eval doc's "highest-leverage product improvement pending" (`_dev_docs/EVAL_LANGGRAPH_COMFYSCRIPT.md` §6 Implement #2). Two distinct wins shipped together: 1. **Kill the /history poll race.** Tight workflows (<2s on warm cache) routinely complete between the poll loop's 500 ms ticks; the client either misses the result or sees a stale empty entry. With ws, ComfyUI pushes an `executing` message with `node==None` the instant the prompt graph finishes. 2. **Eliminate filesystem round-trip on output.** Pre-Phase-9 output went `SaveImage -> output/foo.png -> /view?filename=foo.png`. With `SaveImageWebsocket` (ComfyUI core) / `ETN_SendImageWebSocket` (Acly's pack), image bytes arrive as binary ws frames on the same socket as the status messages. No file ever lands in `output/`. Privacy improvement + ~50-200 ms saved per image. Pair with `ETN_LoadImageBase64` (Acly's pack) for the input side — embeds input images as base64 inside the prompt JSON, eliminating `POST /upload/image` + `GET /view` for input-side round-trip too. Wire format (binary frames) -- per ComfyUI server.py: header = struct.pack(">II", event_type, image_format) frame = header + image_bytes event_type=1 is preview/output image; image_format is 1=jpg, 2=png, 3=jpeg legacy, 4=webp. What landed ----------- `comfyui-spellcaster/spellcaster_core/comfy_ws.py` (NEW, 478 LOC) * `WSImageFrame` dataclass for binary frames (event/format/bytes) * `WSDispatchResult` for the full collection from one prompt * `WSError` hierarchy: `WSUnreachable`, `WSTimeout`, `WSExecutionError`, `WSDependencyMissing` * `_build_ws_url`, `_decode_binary_frame`, `_collect_outputs_from_executed`, `_format_execution_error` helpers * `submit_and_listen()` — single entry point. Connects `/ws?clientId=<uuid>` BEFORE posting `/prompt` (race-free), then listens until the canonical done signal. Filters messages by `prompt_id` so other clients' broadcasts don't leak into the result. * Lazy import of `websockets.sync.client` so the module loads even if the package is missing (only fails at submit time with a clear message). `comfyui-spellcaster/spellcaster_core/dispatch.py` (modified) * `DispatchResult` gains `binary_outputs` (list of (format_name, bytes) tuples) and `transport` ("poll" | "websocket"). Backward-compat: defaults are empty list and "poll" so existing callers work unchanged. * `dispatch_workflow()` gains `use_websocket: bool = False` (opt-in for Phase 9) and `ws_fallback_to_poll: bool = True` (graceful degradation on ws failure: imports missing, connection refused, mid-listen drop). * Branch path replaces the submit + poll block with `submit_and_listen()` when ws is enabled. Same DispatchResult shape on the way out. * Privacy cleanup pass still runs against any FILE outputs produced in the same workflow (mixed-mode supported). * Same `extract_execution_error` / `has_usable_outputs` spirit: if execution_error fires AND outputs exist, warn + return partial; if execution_error fires AND no outputs, raise. `comfyui-spellcaster/spellcaster_core/node_factory.py` (modified) Three new methods on NodeFactory, mirroring the existing load_image / save_image pattern: * `etn_load_image_base64(image_b64)` — input via base64 (Acly's ETN_LoadImageBase64 class, GPL-3 sibling pack) * `etn_send_image_websocket(images_ref, format="PNG")` — output via ws binary frame (Acly's ETN_SendImageWebSocket) * `save_image_websocket(images_ref)` — output via ws binary frame (ComfyUI core SaveImageWebsocket; no sibling pack needed, always PNG). Documented as the lic-clean alternative when the GPL-3 sibling dep is undesirable. `tests/test_phase9_ws.py` (NEW, 28 tests) Mocks `websockets.sync.client.connect` so no real ComfyUI server is needed. Coverage: * URL building (http->ws, https->wss, trailing slash, no scheme) * Binary frame decoding (png, jpg, too-short guard, unknown-event guard) * `_collect_outputs_from_executed` (images + gifs, empty) * `submit_and_listen` happy path (text + binary mixed) * `submit_and_listen` execution_error * `submit_and_listen` execution_interrupted * `submit_and_listen` filters other clients' prompt_ids * `submit_and_listen` passes client_id in /prompt body AND matching ws URL * `submit_and_listen` post unreachable / http error paths * `submit_and_listen` progress callback fires for each stage * `dispatch_workflow(use_websocket=True)` happy + execution_error + partial-success + interrupted * `dispatch_workflow(use_websocket=True)` ws-failure fallback to poll path * `dispatch_workflow(use_websocket=True, ws_fallback_to_poll=False)` ws-failure raises hard * `dispatch_workflow()` poll path UNCHANGED — same result shape minus the new fields' defaults * NodeFactory ETN methods emit correct class_types `.gitignore` (modified) Add `tests/test_phase9_ws.py` to the carve-out whitelist (matches the convention for canonical shared harnesses; tests/* is gitignored by default). Verification ------------ `python tests/test_phase9_ws.py` -> 28/28 passed. Sibling test sweep -> all unchanged from baseline. The only failures (test_quality_boost: 3/54, test_video_layer: ImportError) reproduce on the pre-Phase-9 tree, verified via `git stash` + retest. Not caused by this change. Adoption -------- Opt-in per call: dispatch_workflow(server, workflow, use_websocket=True, # turn on ws path ws_fallback_to_poll=True) # graceful degrade For full inline-transport (no filesystem): nf = NodeFactory() img_id = nf.etn_load_image_base64(b64_input) # input-side # ... pipeline nodes ... nf.save_image_websocket([decode_id, 0]) # output-side workflow = nf.build() result = dispatch_workflow(server, workflow, use_websocket=True) # result.binary_outputs == [("png", <image_bytes>)] # result.outputs == [] (no file landed) Default behavior is unchanged (use_websocket=False); existing build_* / dispatch callers keep the historical poll path until they opt in. Per the eval doc's "ship the lower-risk transport upgrade first" guidance, this lands without any caller changes. Refs: _dev_docs/EVAL_LANGGRAPH_COMFYSCRIPT.md §6 Implement #2 + §7 _dev_docs/ARCHITECTURAL_STUDY_2026-04-30.md sprint-1 #3 + research-doc PARTIAL items Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7188519 commit 4a9ebcc

5 files changed

Lines changed: 1535 additions & 4 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ tests/*
101101
!tests/test_model_coverage.py
102102
!tests/test_cn_compat.py
103103
!tests/test_auto_updater.py
104+
!tests/test_phase9_ws.py
104105
!tests/gimp_batch.py
105106
audit_*.py
106107

0 commit comments

Comments
 (0)