A native select draws its option list itself, so hovering a name gave
nothing to go on: you picked by filename alone. Load Image + Pad, Load
Video and the crop / rotate / pad editors now open a list that shows the
hovered picture, or plays the hovered clip muted with its size and
length, and filters as you type; arrow keys, Enter and Escape work.
The picker (js/shared/media_picker.mjs) only reads the combo widget's
options and writes the chosen value back, so saved workflows, undo and
the API are unchanged. Pure parts live in media_list.mjs with tests.
The mode only rewrites the seed after a queue, so the first queue after
switching from Fixed to Random reused the last seed and ComfyUI returned
the cached result. Entering Random now rolls a new seed right away, and
leaving Fixed for Step takes the first step; flipping Step's direction
leaves the number alone. Checked live on a CPU instance: a Fixed run used
12345, clicking Random showed a new seed, and the next run used it.
The three Crop + Rotate + Pad nodes now share one canvas row (Fill,
Feather in px, Resize) and an Open editor | Reset crop | Reset row, the
image and frame nodes report width and height, the frame node resizes,
and Load Image + Pad has a Reset padding button. Their pages still
described the old image quick row and outputs.
Help pages named the raw widget (divisible_by, max_frames, color_match)
where the card shows a label (Multiple, Limit, Tone match), so a reader
could not find the control. Each such control now reads as the visible
label with the widget name beside it, and option values show the text
on the segment (match a, hist, cover, guided).
The 2.1.0 refresh reset Krea 2 Encode to its defaults when it wired the
prompt in, which switched VLM reference off; the example's own note, the
README and the patch's help page all describe the AnyPaint recipe with
it on. The node's tooltips and help page described only Registered
Outpaint; they now name both setups, as the card already did.
Stitch Inpaint takes stitchers from five nodes, not one. Load Image +
Pad's reference is always built. Seed has no convert-to-input and no
"↺" button any more. LoRA templates live in the browser, and the absorb
section listed only some of the loaders it reads. Video Crop + Rotate +
Pad → Frame has four outputs and no resize. Frame Interpolate names the
RAFT file and folder, and the extras install with ComfyUI's Python.
The lookup was the pack's only outbound request and a standing Registry
finding. Its info-card button, gear-menu switch, server route, sidecar
writer and hash cache are gone. A .civitai.info sidecar already beside a
LoRA is still read, so words and links from earlier lookups or other
tools keep showing.
The video transform nodes' local_path widget was read by every queued run,
so any client that can reach the unauthenticated /prompt route could make
the server open any video on the disk. Local path mode now reaches only
ComfyUI's input, output and temp folders unless ComfyUI is started with
AUSBOSS_TRANSFORM_LOCAL_PREVIEW=1, for queued runs and editor previews
alike. Tooltips, help pages, README and CHANGELOG say so; the clip node's
test fixture opts in explicitly.
Video Crop + Rotate + Pad -> Clip / -> Frame:
- The rail under the preview is a timeline: playhead scrubbing on the node
face, frame-accurate IN/OUT handles that carry the playhead, Set IN/OUT
and I/O keys in the editor; js/shared/timeline_math.mjs mirrors the
backend's inclusion rule and emits hundredth-grid seconds.
- Scrubbing no longer flickers: storyboard tiles only stand in when nearer
than the frame on stage and are decoded at their target times; the scrub
session returns the frame it already decoded instead of the next one;
served previews are cached.
- Aspect lock on the format chips (pad, lock, clear) with lockPadding in
transform_geometry; Lock aspect in the editor.
- A new source keeps fill, feather, resize, Snap and Limit and re-pads to
the lit chip; a canvas row (fill, feather, resize) sits on the node face.
- The clip node declares a black fill and feather 0 as its defaults and the
editor's Reset all follows the node's own declaration, so a fresh node or
a reset never lands on the grey, feathered canvas the LTX IC-LoRA cannot
paint; image nodes keep grey/24.
Examples:
- LTX 2.3 Video Outpaint: Select Frame -> core Generate Text writes the
positive prompt; note rewritten with the tested rules (53 renders over
six clips: only fill #000000 + feather 0 decide painting).
- Core model loaders coloured and Workflow Notes grown to fit across the
other examples.
ausboss: imported loras should land under the rows already in the stack.
Upstream entries first in chain order, then downstream; LoRA patches
accumulate, so apply order does not change what the graph computes.
Strength bars behind each row's name (name doubles as a scrub surface),
the absorb-chain gear action (core/rgthree/Pixaroma/AusBoss loaders,
both directions, originals bypassed), moved-file resolution at run time
with warn-and-skip for missing files plus the reconnect button riding
ComfyUI's R refresh, folder-free row names, the amber duplicate ring,
master-toggle memory, the control bar reclaiming the slot band, + LoRA
inside the stack, and unified strengths as a hard per-node default.
Pure logic merged from the lab's lora_lab.mjs into lora_stack.mjs;
settings_menu gains action rows and persist:false; _lora_helpers gains
match_lora_name and the resolve fallback; VALIDATE_INPUTS is structural
only so a missing file can never block the queue.
- Optional resize of the transformed output to a pixel budget, with core
Scale Image to Total Pixels semantics: megapixels x 1024^2, aspect
preserved, each dimension rounded to resolution_steps. The image uses
the chosen filter (lanczos/area/bicubic/bilinear/nearest-exact); the
mask always resizes bilinear so feathered edges cannot ring. The math
is mirrored py+js (scale_to_megapixels / scaleToMegapixels) and
fixture-tested on both sides; the new widgets append after the stable
V1 set so saved workflows keep loading, and IS_CHANGED fingerprints
them.
- A quick row under the node's canvas: Reset (rotation, crop, padding),
a Feather toggle that remembers the amount it turns off, and the
Resize toggle with its megapixel scrub box.
- The editor gains a Resize output section (megapixels and steps as
scrub controls, method select) and its status names the exact resized
size.
- The output size readout moved off the pixels: centered below the image,
flipping above when clipped, and showing the resize target.
- CHANGELOG entries for this round and the two commits before it.
- Drag-to-reorder actually drops: the row preview's reparenting silently
released pointer capture, so the gesture now listens on the window for
its whole lifetime.
- Step arrows on every strength box (configured step; Shift = 0.01), with
range tinting carried over.
- The control bar (templates, stack toggle, count, settings) sits directly
on top of the row stack and + Add LoRA pins to the node's bottom edge;
the triggers output got its narrower name (slot links reconnect by index).
- Civitai lookup reads the whole response instead of the first TCP chunk,
so successful lookups stop dying as 'lookup failed'.
- Compare and Load Video declare their height floors WITH the DOM-widget
frame allowance, so minimum-height nodes stop shaving the caption and
trim strip; panel_layout documents the rule.
- README and the LoRA doc catch up; panel_guards comments follow the
body-as-layout-box reality.
- New node: Save Image - PNG or lossless JPEG XL (optional
pillow-jxl-plugin, the pack's new jxl extra), workflow embedding on or
off (honoring --disable-metadata), an exact_name mode that saves under
exactly that filename with no counter suffix, overwrite/skip/error
collision policy, output_dir up to absolute dataset paths, a caption
.txt sidecar sharing the basename, file_path and IMAGE passthrough
outputs, and always-re-run semantics so a deleted file re-saves.
- Image Resize now only invents pixels on an explicit width+height pad
request: scale-derived modes resolve divisible_by snaps with a
sub-half-step resize instead of padding a sliver; the tour workflow
switched to an explicit 512x512 letterbox so its mask lane demonstrates
real bars.
- Registry-facing metadata: the description now names every node family
(Manager search matches against it) and the keyword list grew to match.
- Release 1.2.0: version bump, frontend version constant synced,
changelog dated, and a publish workflow (Comfy-Org/publish-node-action)
that releases to the registry whenever pyproject.toml changes on main.
- New nodes: Image Resize plus the Utility group (Text, Integer, Float,
Show Text, Math Expression, Select Every Nth, Split Batch, Merge
Batches, Free Memory) - the dependency-free gaps that let shared
workflows drop heavy packs.
- Replace with AusBoss nodes (prototype): preview-then-confirm graph
rewrite swapping known third-party nodes for pack equivalents, with a
tested translation map, per-node opt-out, and refuse-with-reason for
nodes the pack cannot stand in for.
- Load Video: single-frame FRAME picker, an honest "what will load"
label from the probed source rate, drop-a-video-to-restore. Save
Video: format-aware widgets and a file_path output. Stitch Inpaint
gained a blend_mask output, Select Frame negative from-the-end
indexing, Image Size a batch count - all appended, saved workflows
load unchanged.
- LoRA Loader: drag-to-reorder rows, hover thumbnails on rows, file
size and date in the info card.
- Appearance: flagship scheme deepened to slate-teal; the retired
bright-teal pair upgrades automatically via LEGACY_SCHEME_PAIRS.
Optional queue-completion chime, off by default.
- Image Resize invariant: pixels are only invented on an explicit
width+height pad request - scale-derived modes resolve divisible_by
snaps with a sub-half-step resize, never bars.
- Docs refreshed for six stale pages, stale text and metadata fixed
across the pack, the panel-height rule codified in AGENTS.md and the
brand skill, and ausboss_node_tour.json added - a model-free example
exercising 17 node types in one graph.
Validated: scripts/validate_nodes.py (24 files), 300 JS tests, all
Python suites under the ComfyUI embedded interpreter.
Image Compare A/B drew a chip over the top-left of the picture carrying the
resolution and a hint about how to use the panel - covering part of what you
came to look at in order to say something you need once. The resolution moved
to a caption centred under the stage, and when the two sides are different
sizes both are named there: the panel scales them to fit, so nothing else on
screen would reveal the mismatch. The empty and error states still take the
middle of the stage, where there is no picture to hide.
HOLD became an A/B toggle. Press-and-hold meant the comparison only existed
while a mouse button was down, so the two images could never be held side by
side in memory. Each click now swaps the panel and the button names the side
you are looking at; flicking between them in place is how a small difference
actually becomes visible. A workflow saved on "hold" opens on the toggle it
became rather than quietly dropping back to slide.
The example workflow was stale in ways that would have shipped to Civitai:
its overview credited SimpleWatermarkRemover - the deprecated compatibility
id, not the node the graph actually uses - still called the node Refine Mask,
and linked to github.com/auboss, which does not exist. It gained model-setup
and requirements sections, consistent node titles, and the mask settings its
single-frame test was missing: that path shipped with every Mask Refine
control at zero, so the frame it previewed was not the result the full run
would produce, which is the one thing that test exists to show.
Five things surfaced by release testing.
Node search: ComfyUI's search dialog cuts at 64 results and ranks by
[match tier, global popularity, name length], where tier 1 needs the name
to START with what was typed. Popularity is a fixed global table in which
every node from a new pack scores zero, so tier is the only lever. As
"Refine Mask" this node came 121st for "mask" and was never on screen.
Renamed to Mask Refine, which is 15th, and kept "refine mask" as an alias.
The mapping key is untouched, so saved workflows are unaffected. Aliases
alone cannot fix this: the frontend indexes them but drops them from the
tier scoring because they are a list rather than a string, so they decide
whether a node is findable, not where it lands.
Previews: Mask Refine, LaMa Inpaint and Select Frame showed whatever fed
their input, which is nothing when a segmentation node is upstream - Mask
Refine's panel was blank however the mask turned out. Each now returns its
result as a preview and the panel shows that, falling back to the input
before the first run. A node does not need to be an OUTPUT_NODE for this;
returning a ui payload is enough. ComfyUI's own preview for these nodes is
stood down so the picture appears once, in the panel, rather than also
underneath the node. That also fixes LaMa finishing a single image with an
empty panel: frame previews were only attached for batches over one frame,
on the assumption that one image would preview through the output path,
which a non-output node does not have.
Mask Refine opens on expand and blur with the other five behind MORE.
Hidden widgets keep their values and their saved positions. AUTO sets both
from the mask's size, scaled off the 8/4 hand-tuned for the watermark
workflow at 576px, because a feather is a fraction of the picture rather
than a fixed pixel count.
Save Video gains nvenc h264/h265, webm av1, ProRes mov, lossless ffv1 mkv,
gif and webp, plus pingpong and save_metadata. Every format is encoded and
read back in the tests rather than trusted from its table row. fps steps in
whole frames but stays FLOAT: an INT would refuse Load Video's fps link and
round 29.97 to 30, drifting picture off audio. mov files now carry the
embedded workflow, which they silently did not. The reload button is gone -
it only re-fetched what was already on screen.
Not done: Select Frame's preview updates on a run, not as the number
changes. Seeking to a frame needs the source fps, which the browser never
learns, so live scrubbing needs a server route - the surface Frame Chooser
was just demoted for.
Pausing the graph for an interactive pick is a bigger surface than
anything else here - a server route, a resumable pause, a panel that has
to survive a reload - and it has open issues that are not worth holding a
release for. The lab is where that belongs until they are fixed.
Removes the node, its helpers, its panel, its shared DOM-free module,
its docs and its tests, and drops it from NODE_MODULES, PUBLIC_NODE_IDS,
the panel guard list and the README. _execution_helpers stays: Load Video
and Save Video use it too.
The port lives in ComfyUI-AusBoss-Lab as AUSBOSS_LAB_FrameChooser, with
its routes moved from /ausboss/frame_chooser to /ausboss_lab/ so both
packs can be installed at once. A saved workflow using the public id
reports it as missing.
The old name reads like it transforms a clip. It takes ONE frame out of
a video and returns a single image; the editor's long timeline is there
to find that frame, not to trim a range. People reach for it expecting
video in, video out.
"grab frame", "extract frame" and "frame from video" join the search
aliases, and the description says the output is a single image up front.
The mapping key AUSBOSS_NODES_VideoCropRotatePad is untouched - it is the
workflow-compatibility contract - so saved workflows are unaffected. The
arrow is safe in a display name: names reach the frontend as JSON and are
never printed at import time, where a cp1252 console would choke. The
VALIDATE_INPUTS and alert prefixes stay plain ASCII for that reason.
The extended region breaking up is not a tuning problem. The model
places the source spanning one whole canvas axis - pad left/right and it
spans the full height, pad top/bottom and it spans the full width - and
the reference pipeline this LoRA ships with splits anything else into
TWO passes rather than doing it in one. Padding both axes and running
once puts the placement outside what the weights were trained on.
Verified on one image, one seed, changing only the geometry: padded left
AND bottom, the dress is cut off flat and the legs below it are mush;
padded bottom only, the gown continues full length with no seam.
Load Image + Pad happily pads any combination of sides, so this is easy
to hit by accident - and the usual cause is not a deliberate second pad.
Padding only the bottom of a 720-wide source at canvas_multiple 32 rounds
the width up to 832, and an 11px sliver breaks the span exactly as
completely as a deliberate 231px pad. The warning therefore reports the
spare pixels on each axis and names the multiple, instead of just saying
the placement is wrong.
Krea 2 accepts reference latents, but they arrive with no position, so the
model treats them as a loose style hint: it borrows the look and reinvents
the content. That is the whole reason an outpaint drifts instead of
continuing the photo.
Krea 2 Encode attaches the reference latents and emits the negative from
the same node, so a turbo graph at CFG 1.0 stops carrying a second text
encode that does nothing. Krea 2 Outpaint Model Patch registers those
reference tokens into the target grid at the rectangle the stitcher
records, on frame axis 1.
Load Image + Pad gained a `reference` output - the unpadded source, fitted
to a 384px long edge and a multiple of 16, because the VAE downsamples by
8 and the DiT patchifies by 2. It is APPENDED after `stitcher`, never
inserted: a workflow stores links by output slot index, so any other
position silently rewires every saved graph.
The stitcher now records where the source sits, as source_bbox in pixels
and bbox_normalized in 0..1. Padding knows the rectangle exactly, so it
rides with the canvas instead of a parallel wire that can be left
unplugged. apply_stitch reads neither key, so an older stitcher stitches
identically.
Ported from the lab rather than copied: the lab's _krea2_helpers had its
own pad_image, feather_pad_mask, plan_pad_canvas, parse_fill_color and
build_canvas_stitcher. Duplicating the padding engine here would let the
outpaint path and the inpaint path disagree about what a stitcher is, so
the public module keeps only what is specific to Krea 2 and uses the
existing helpers for the rest.
Two departures from the lab version. The model patch declared a kv_cache
input and never read it - the cache was always on - so it is wired up now.
And it imports comfy's flux internals when you run it rather than at
startup: a core release that moves one then surfaces as an error on that
node, instead of deleting it from the menu and leaving saved workflows
reporting it missing.
The old display name led with the mechanism. "Image Compare A/B" says
what the node is first, which is what someone scanning the menu reads.
"image compare" joins the search aliases; "compare" and "a/b" stay.
The mapping key AUSBOSS_NODES_Compare is untouched - it is the
workflow-compatibility contract - so saved workflows are unaffected.
Load Image + Pad does the same job from the same stage and starts from
the file, so the only thing the second node added was taking an IMAGE
wire - one idea holding two slots in the menu.
Deletes the node, its panel, its help doc and its stage-only shared
helpers (padEmptyStateText, findPadPreview), and drops it from
NODE_MODULES, PUBLIC_NODE_IDS, the README and the panel guard list.
The padding helpers are untouched; Load Image + Pad and Align Image both
use them. Two console labels there named the removed node, so
_fill_tensor now takes the source name - Align Image passes its own
pad_color, and a colour it cannot read is no longer reported against
padding. The BHWC and mode errors read "Padding"/"Pad mode" instead.
The on-node pad stage moves to ComfyUI-AusBoss-Lab, which has its own
AUSBOSS_LAB_PadImage.
The node produced a mask offset, grown and blurred - no contact
darkening, no perspective - so it read as a sticker halo rather than a
shadow in the scene. That is the approach, not a tuning problem, so it
comes out rather than shipping under the pack's name.
Deletes the node, its helper module, its test file and its help doc, and
drops it from NODE_MODULES, PUBLIC_NODE_IDS and the README. The colour
helper tests keep their two-callers case using Pad Image and Transform,
the real remaining call sites.
blur_mask and grow_shrink_mask stay in _mask_helpers - Crop For Inpaint
and Pad Image use both.
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.
Every display name swaps ' (AusBoss)' for ' 🆎' - menus, docs, tooltips,
the README, and the example workflow's notes. Mapping keys never change,
so saved workflows keep loading; 'ausboss' keeps finding everything via
the category, the AUSBOSS_NODES_ prefix, and SEARCH_ALIASES. Python error
messages drop the suffix entirely so exception text stays ASCII for
cp1252 consoles.
The LM Studio endpoint toolbar declared no getMinHeight, so the frontend
gave its wrapper a 12px sliver and the buttons clipped; it now declares
32px through every sizing path, and the buttons breathe at 22px.
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.
Every registered node now has rich help: Video Unbundle and Video Bundle
Edit only existed inside the family page, which ComfyUI's help panel
never shows because it looks docs up by exact node id, and the
SimpleWatermarkRemover compatibility alias had nothing pointing users at
LaMa Inpaint. 21 of 21 mapping keys now have a js/docs page.
Three statements the review verifiers caught as false stay fixed too:
the README said no minimum ComfyUI version is declared while
pyproject.toml declares and Manager enforces 0.27.1; the optional-extras
comment promised a warn-and-fall-back that only Stitch Inpaint actually
does, where Refine Mask deliberately stops with the package name; and
the Load Video tooltip cited 0.3.26 for a VIDEO type that landed in
0.3.31 - moot either way, since every supported core has it.
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.
Continue, cancel and the expiring countdown were all writing straight to
the pending object and reporting success. Two clicks a frame apart both
returned 200 with the second selection winning; a cancel behind a keep
turned a continuing run into an interrupt; the timer could discard an
answer the browser had just been told had landed.
Every terminal path now goes through claim_pause, which records the one
outcome under the store lock and tells every later claim it lost. The
route answers 410 for those, the panel treats 409/410 as already-handled
rather than an answer failure, and a resolved pause stops being offered
to a page that reloads before its waiter unwinds. A rejected selection
claims nothing, so the cancel a deleted node posts behind it still
releases the graph.
tests/test_chooser_pause.py drives the races with real threads; ten of
its nineteen tests fail against the previous unguarded write.
The pymatting solve costs roughly 90 ms per megapixel of paste window,
per frame - 48 ms for the 768x768 window a 1024x1024 frame produces,
106 ms for the 1440x816 window from 1080p, measured on a 16-thread
desktop CPU. A video batch with the toggle on therefore ran to the end
with no feedback and no way out. The spread now checks for a cancel and
reports progress between frames, through the same seams the LaMa helper
uses, and stops at the next frame boundary.
It also hands pymatting float32 instead of float64. The estimator casts
whatever it is given down to float32 before solving, so the wider
temporary bought nothing: outputs stay bitwise identical across the
toggle, batch and rescale paths, and writing frames into one buffer
instead of stacking them cuts peak memory growth by about a third on a
48-frame 1024x1024 batch.
Node help now carries the measured per-frame cost and says plainly that
the toggle is for finishing a chosen take, not for a long exploratory
batch.
Keep selected, Keep all, Cancel, Enter, Escape and the cancel a deleted
node sends all posted without checking whether an answer was already in
flight, so two quick Enters put the same one-use token on the wire twice
and the duplicate's 409 landed as "Answer failed" over the success the
first request had already painted. Every path now goes through a latch on
the panel state: a second submit never fires, the controls read as
unavailable until the reply lands, and the latch clears on success or
failure so a genuine network error can still be retried. Each submission
also captures the pause it was posted for, and a reply is dropped when the
panel has since resolved or moved to another token - which covers both the
late rejection and a new pause arriving on the same node mid-flight. The
latch and the staleness predicate live in js/shared/frame_chooser.mjs with
node:test coverage.