PUBLIC_NODE_IDS only checked that every listed id was still registered. A
brand-new id could register and validation stayed green, so the release gate
could not tell when a node reached the public pack without anyone deciding it
should ship.
Also pins SimpleWatermarkRemover in its own LEGACY_NODE_IDS set. It is the
pre-convention id kept so already-published workflows keep loading, and it was
never listed - so the new check now asserts the compatibility shim is present
rather than flagging it as a stray.
The audit compared every node against ComfyUI core and the major packs;
this lands its outcomes in one branch.
On-node visual editing (the old Codex-pack interaction, rebuilt):
- New Load Image + Pad: image loader with whole-edge pad dragging on the
node canvas, align-to-multiple rounding, and a megapixel target that
rescales the source before padding so the mask seam stays binary.
- Pad Image gains the same handle canvas, fed by an execution-time
snapshot of its input; wireframe with diagnostic empty states before
the first run. Widgets, outputs, and saved workflows unchanged.
- Image Crop + Rotate + Pad's compact panel is now a live stage - crop,
pad, and rotate handles on the node, one shared implementation with
the full-screen editor so the two surfaces cannot drift.
Feature gaps closed against the ecosystem:
- Crop For Inpaint: target_megapixels, selectable rescale_algorithm
(recorded in the stitcher for the paste-back), and extend_* inputs
that grow the frame itself for outpainting.
- Save Video: format choice - mp4 h264, mp4 h265, webm vp9 with Opus
(resampled to 48 kHz); CRF drives all three.
- Load Video: every_nth thinning (fps output divides to match) and a
max_frames cap that ends the decode early.
- LoRA Loader: saved stack templates behind a bar button, and a console
warning when a LoRA's base-model family disagrees with the checkpoint.
- LM Studio Chat: history in/out for multi-turn chains and a json_schema
widget for structured replies.
- Drop Shadow: multiply blend and a shadow_mask output. Align Image:
offset outputs for un-aligning after sampling. Color Match:
first_frame reference mode as a one-node video flicker fix.
Removed (backed up in git history): Select Frame Range and the Video
Bundle family - core ImageFromBatch and the core VIDEO wire now cover
both jobs. Dead NODE_ID module variables cleaned out of every node file.
455 Python tests, 216 JS tests, validate_nodes.py green on 19 node files.
Align Image snaps width and height to a clean multiple - 16, 32,
whatever the model wants - because Qwen image models, VAEs and plenty of
samplers only behave on cleanly divisible sizes. Each mode implies its
own rounding, so there is no separate direction widget: resize rescales
to the nearest multiple, crop center-crops down, pad replicate-pads up,
and a side smaller than one multiple always grows to exactly one rather
than erroring. The new size comes out as INTs for wiring into latents.
Image Size reads width, height, longest edge and shortest edge off the
wire so resizes and latents key off the actual image instead of retyped
numbers that drift.
LM Studio Chat posts a prompt - and optionally one image - to a local
LM Studio server, or anything else speaking the OpenAI-compatible chat
API. The interface is the middle ground the installed ecosystem packs
miss on either side: endpoint string, model string where EMPTY means
whatever the server has loaded (a dropdown fetched at ComfyUI startup
goes stale whenever LM Studio was not running yet), optional system
prompt, prompt required unless an image is connected, temperature,
max_tokens, seed as the re-roll knob, a timeout, and an image downscale
cap. Reasoning-model think blocks land on their own output so the text
stays clean for conditioning. Stdlib urllib only - no new dependencies -
and the POST runs off the executor thread. Every error names what to
fix, with the server's own words when it had any.
Verified live against a running LM Studio: a JIT-loaded qwen3.5-9b
answered a text round trip in 7.6 s through the real node; the
no-model-loaded 400 and a dead port both surfaced their actionable
messages. The vision payload shape is unit-tested; the two local VL
quants refused images server-side (missing projector / failed load), so
the live vision check stays on the deployment checklist.
48 offline tests cover the alignment math, both size nodes, the URL and
payload builders, think-splitting, image encoding, and every mocked
network error path.
Frame Chooser, mostly. A pause did not survive a workflow load: LiteGraph
clears a graph by removing every node, so undo, a tab switch, Clear
Workflow and opening another file all fired the teardown a deleted node
uses to release its pause, and killed a run that was still going. That
now only fires for a real deletion. Answering "keep all" wrote the batch
out as 1,2,...,N into pick_list, so the next run dropped whatever the
enumeration did not reach or failed outright on a shorter clip; it
writes the empty answer it was given. And pick_list is only written back
under "keep last selection" - it pre-answers the node, so filling it in
automatically meant a chooser left at the default paused exactly once.
Refine Mask: smooth applies only the change it made to the jaggy edge
rather than binarizing everything, so an already-soft matte keeps its
alpha (binary masks stay bit-identical), and blur now reaches the
matting solve instead of being thresholded back out of the trimap.
Frame Interpolate's copy path honours batch_size; it gathered every
copied frame at once, which on a long clip at an integer multiple was a
multi-gigabyte allocation on the frames device that no setting bounded.
Subgraph execution ids are resolved by walking the chain instead of
stripping the prefix, which could match an unrelated node with the same
number. Status and runtime badges appear inside subgraphs again, and a
subgraph pause renders on the node that actually paused.
Also: the Custom colour scheme no longer strips every node's colour when
its stored colour is unreadable; Drop Shadow and Pad Image name their
own widget for an unreadable colour instead of blaming Transform; a
non-ASCII token and a non-object JSON body answer 400 rather than
throwing a 500 and stranding the pause; pick_list rejects Unicode
digits, which used to raise during validation or silently read as a
frame nobody typed.
Tooling: validate_nodes parses NODE_MODULES rather than matching it
anywhere in the text, and an unregistered module is an error - it used
to exit 0 printing nothing. It also refuses mapping keys declared
outside nodes/, which is how the registry fixtures came to be advertised
to ComfyUI-Manager as installable nodes; they are .py.txt now. And the
video test no longer leaves ComfyUI on sys.path, which was flipping
every fail-soft seam for whatever ran next.
Four holes. Two empty literals agree with each other, so a module that
registers nothing passed every rule. A del could retire a node the
scanner goes on advertising - the mirror image of the update() case
already covered. An alias made the mutation invisible: _registry =
NODE_CLASS_MAPPINGS then _registry.update(...) never says the mapping
name at all. And the permanent-id check grepped the whole file for
AUSBOSS_NODES_*, so a key renamed in the literal kept passing as long as
the old id survived in a docstring or a search alias.
The contract is now that a mapping is assigned once to a non-empty dict
literal and its name is never used again. That is what makes the rest
enforceable: the alias is refused where it is made, the last point
anything static can still see it, rather than chased through code no
checker can follow. Permanent ids are measured against the keys parsed
out of the literal.
bad_empty, bad_delete and bad_alias join the fixtures; each one was
dropped into nodes/ and confirmed to exit 1 on its own line.
Pads any side with a solid color, an averaged edge-color band,
replicated edge pixels, or a blurred and dimmed pillarbox backdrop.
The original region stays bit-identical in every mode and the second
output masks exactly the new padding, ready for outpainting.
Per-channel LAB mean/std matching against a reference image, with an
optional mask that restricts both measurement and application, a
strength blend, and batch-1 reference broadcasting. Pixels outside the
mask stay bit-identical.
An OUTPUT_NODE that saves temp previews of each batch's first frame
and renders them stacked in a DOM panel: slide mode clips B to the
pointer with a seam line, hold mode flashes B while pressed. image_a
passes through unchanged so the comparison can sit mid-graph. Pure
clip and mode logic lives in js/shared/compare.mjs under node:test.
Video Bundle packs frames, fps, and optional audio into one custom
wire; Video Unbundle unpacks it; Video Bundle Edit copies a bundle
overriding only the connected inputs. Frame count, size, and duration
are derived from the frames tensor and fps at bundle time and
recomputed on every edit, so they can never drift from the frames.
Project structure modeled on ComfyUI-Pixaroma (MIT) with original code:
fail-soft module loader in __init__.py, one file per node under nodes/,
per-node frontend folders under js/ with shared .mjs utilities, offline
validation script, docs, and a smoke-test workflow.
Phase 1 placeholder nodes: Text Box, Show Text (with JS frontend),
Random Line, Image Dimensions.