Compare commits

...
758 Commits
Author SHA1 Message Date
ausboss 7bc156a819 Drop an unused import in the second-copy test 2026-11-09 15:23:08 -06:00
ausboss 7a1f89ab15 Skip a second install of the pack instead of stopping ComfyUI
Manager installs the pack in custom_nodes/ausboss-nodes and a git clone
lands in custom_nodes/ComfyUI-AusBoss. With both folders present, both
copies registered GET /ausboss/pack_modules and ComfyUI stopped at
startup: "Added route will never be executed, method HEAD is already
registered".

The first copy to load claims a marker in sys.modules. A later copy
loads no node modules, registers no routes, serves no js folder, and
prints which folder it skipped.
2026-11-09 15:21:25 -06:00
ausboss 146e3cfa8b Merge pull request #322 from ausboss/ci/publish-downloader-note
Publish Model Downloader: take the release note as an input
2026-11-09 00:35:55 -06:00
ausboss e22ddb90d5 Publish Model Downloader: take the release note as an input
The job carried the 0.1.0 note as a fixed string, so every later
version would have shown "Initial release" on the Registry. The note is
now a required input of the manual run, and the publish step refuses a
blank one.
2026-11-09 00:34:40 -06:00
ausboss db9a033f11 Merge pull request #318 from ausboss/release/2.22.1
Release 2.23.0
2026-11-09 00:16:53 -06:00
ausboss 442656c712 Release 2.23.0: date it today 2026-11-09 00:16:06 -06:00
ausboss b17f8c0355 Release 2.23.0: add the Color Match and Krea 2 Outpaint fixes 2026-11-08 23:56:09 -06:00
ausboss 92a4ae8c56 Merge remote-tracking branch 'origin/main' into release/2.22.1 2026-11-08 23:53:56 -06:00
ausboss 51a2aebf40 Merge pull request #314 from ausboss/fix/upscale-review
Color Match leaves a picture alone when matched to itself
2026-11-08 23:53:09 -06:00
ausboss 28fbd321c7 Release 2.23.0
Mask by Touch keeping picks on a new picture is a new feature, so this
release is 2.23.0 rather than 2.22.1. It also brings the stopped-run
message fix and the corrected example notes.
2026-11-08 23:48:28 -06:00
ausboss 144884fae3 Merge remote-tracking branch 'origin/main' into release/2.22.1 2026-11-08 23:45:59 -06:00
ausboss 935741b43d Merge pull request #321 from ausboss/fix/touch-stop-note
Mask by Touch: clear a stopped run's message once a run works
2026-11-08 23:45:48 -06:00
ausboss c1b620d448 Mask by Touch: clear a stopped run's message once a run works
A run that stopped at the node left its message on the face until
something else was said there. The face's own runs cleared it, but a
workflow run's result did not, so the message stayed under a good result.
The stop now marks its message, and the next result that comes in
clears it.
2026-11-08 23:44:55 -06:00
ausboss 4b1dc7ccb7 Merge pull request #320 from ausboss/fix/example-notes-text
Example notes: say what the graph does
2026-11-08 23:42:05 -06:00
ausboss c963657b6e Merge pull request #319 from ausboss/feat/touch-keep-picks
Mask by Touch: keep the picks when the picture changes
2026-11-08 23:41:52 -06:00
ausboss 5b4651df45 Mask by Touch: keep the picks when the picture changes
A different picture coming in no longer stops the run with every pick
dropped. The node moves the clicks and paint onto it, in the same places,
scaled from the size in their key to the new picture's size, and sends the
face the moved touches. The face takes them while it still holds the picks
they were moved from. New picture, under More, set to start over keeps the
old behaviour.
2026-11-08 23:29:49 -06:00
ausboss 1b1fca961c Example notes: say what the graph does
- Krea 2 Rotate + Outpaint: the note describes the tone match the
  graph runs.
- Qwen Image 2.1 Inpaint: the note says the seed is fixed.
- Qwen Image 2.1 Upscale + Restore: the Megapixels note mentions the
  roughly 2 MP minimum redraw.
2026-11-08 23:15:13 -06:00
ausboss 1568a56874 Release 2.22.1 2026-11-08 23:03:22 -06:00
ausboss 989a0e32d6 Merge pull request #317 from ausboss/fix/lifted-socket-overlap
Fix lifted inputs drawn on top of the first input in saved workflows
2026-11-08 23:01:01 -06:00
ausboss 2ce4d7c9f0 Merge pull request #313 from ausboss/fix/preview-review
Compare, preview bar and Save Image ignore late replies and stale paths
2026-11-08 22:59:56 -06:00
ausboss 25b293513e Merge remote-tracking branch 'origin/main' into fix/preview-review
# Conflicts:
#	tests/test_frontend_cache.py
2026-11-08 22:58:44 -06:00
ausboss a5190b3c6b Merge pull request #316 from ausboss/fix/switches-review
Workflow Switches: a settings switch keeps its node running
2026-11-08 22:58:12 -06:00
ausboss 9c58b5a9ed Merge pull request #315 from ausboss/fix/scripts-review
Offline checks read pyproject as TOML and catch more hidden mappings
2026-11-08 22:58:03 -06:00
ausboss 8d38e53b3a Merge pull request #312 from ausboss/fix/inpaint-review
Crop for Inpaint keeps the whole feather inside the crop
2026-11-08 22:57:54 -06:00
ausboss ef6b10a380 Merge pull request #311 from ausboss/fix/folder-review
Image Folder: fresh results on folder change, newest files kept
2026-11-08 22:57:45 -06:00
ausboss 3f7759aa79 Merge pull request #310 from ausboss/fix/canvas-review
Keep node colours, snap corner resizes and show About in Nodes 2.0
2026-11-08 22:57:41 -06:00
ausboss 95b69ce4af Fix lifted inputs drawn on top of the first input in saved workflows
Newer ComfyUI stamps a widget's input with its row position (slot.pos) and keeps using it after the widget is gone, so fixed_frames on Video Crop + Rotate + Pad -> Clip landed on force_rate. liftSocket now clears it; Resolution uses the same helper.
2026-11-08 22:21:34 -06:00
ausboss 28340ded7e Count the newer COMBO input format as a widget in the example check
ComfyUI reports newer dropdowns as "COMBO" instead of a list of choices,
so saved values for them were ignored and four examples were flagged as
missing required inputs they do have.
2026-11-08 21:18:49 -06:00
ausboss da3072a7de Workflow Switches: a settings switch keeps its node running
Review fixes for Workflow Switches.

- A switch that changes a setting on a node no longer turns that node
  off.
- LoRA choices keep every differing field and show row and group locks.
- Odd ids or titles in a saved workflow no longer break loading, and
  unsafe group colours are handled.
- The add-choice button appears as soon as a node is selected.
- Copy clones no longer leak redraw timers.

Tests: tests/workflow_switches_frontend.test.mjs and
tests/workflow_switches_regressions.test.mjs, with additions to
tests/workflow_switches.test.mjs.

Not covered: repointing switches inside copied subgraphs. The review
wrote a test for it, but nothing delivers per-subgraph id maps yet, so
that test is left out.

No inputs, outputs or mapping keys change.
2026-11-08 19:35:10 -06:00
ausboss 475d6e19ea Offline checks read pyproject as TOML and catch more hidden mappings
Review fixes for the pack's offline checks.

- The startup version reader and the checks read pyproject.toml as TOML
  instead of a regex.
- validate_nodes and registry_contract catch more ways a node mapping
  or the module list could be hidden from registry scanners.
- release_preflight flags network-client imports and spaced or
  qualified fetch calls in shipped code.
- workflow_contract verifies saved widget values against the real
  widget order, covers subgraphs and validates model download info.
- Krea 2 Inpaint Masked example: two Note nodes' named widget values
  now match their positional values, which the new check requires, and
  the file ends with a newline. The graph itself is unchanged.

Tests: tests/test_check_regressions.py, with one adjusted case in
tests/test_workflow_contract.py.

No inputs, outputs or mapping keys change. No version change.
2026-11-08 19:33:20 -06:00
ausboss b3df628a60 Color Match leaves a picture alone when matched to itself
Review fixes for the colour helpers and the Krea 2 core.

- Color Match (MKL and histogram) returns the image unchanged when the
  reference is the image itself or a same-colour reference of another
  size.
- Krea 2 outpaint stops reusing a stale reference cache: the cache is
  keyed by reference and canvas and cleared on in-place edits and at
  sampler start and cleanup.
- Other model patches (attention hooks) now apply inside the Krea 2
  outpaint path, in order, with their metadata.

Tests: tests/test_color_match.py gains the identity cases;
tests/test_krea2_core.py covers the cache and the patch order on the
CPU.

Color Match numbers change slightly for the same inputs. No inputs,
outputs or mapping keys change.
2026-11-08 19:27:31 -06:00
ausboss 2a2b31721b Compare, preview bar and Save Image ignore late replies and stale paths
Review fixes for the preview-carrying nodes.

- Compare and the input preview bar no longer rebuild their panel when
  a workflow loads, and a late Align or Save reply is ignored after the
  panel is removed or the picture changes.
- The preview bar clears its picture when the preview switch is off
  and works when two nodes watch the same source.
- Save Image forgets the old saved path when the folder changes or a
  run saves nothing, and can no longer pick the same file number as
  Compare at the same time.
- Save Image accepts bfloat16 and single-channel images.
- A blank linked caption writes no caption file, and a caption file
  that already exists follows the "on existing" setting.

Tests: tests/preview_lifecycle.test.mjs covers the panel and reply
cases; tests/test_image_save_helpers.py covers the image types, the
numbering and the caption collisions; two cache tests now call the
middleware directly instead of starting a test server.

Caption behaviour changes as described above. No inputs, outputs or
mapping keys change.
2026-11-08 19:25:24 -06:00
ausboss 68a36d99e1 Crop for Inpaint keeps the whole feather inside the crop
Review fixes for the inpaint crop and stitch helpers.

- The crop window now contains the full feathered edge, so the blend at
  the seam is complete.
- Outpaint extension bands stay fully paintable after the mask is
  shrunk or softened.
- The reported blend mask no longer extends past the area actually
  pasted.
- The Crop for Inpaint, Stitch Inpaint and Realign to Source help pages
  are corrected (Multiple default, Max zoom wording, resizing caveat).

Tests: tests/test_inpaint_crop_helpers.py covers the feather, the
bands and the partial paste window; tests/inpaint_help.test.mjs checks
the help pages against the code; the classic pin test keeps comparing
against the released output.

Crops and blend masks differ slightly from earlier releases for the
same inputs. No inputs, outputs or mapping keys change.
2026-11-08 19:23:10 -06:00
ausboss f4d42734fe Image Folder: fresh results on folder change, newest files kept
Review fixes for the Image Folder node and its routes.

- Switching folders or reloading a workflow no longer shows stale or
  mixed-up results: late replies are ignored, copy and upload locks
  hold, and removing a node closes only its own popups.
- A very large folder keeps its newest files instead of an arbitrary
  slice.
- Thumbnails refresh after a picture is rewritten in place.
- A run over more than 64 pictures marks every picture as done.
- Route errors no longer reveal server paths.

Tests: tests/image_folder_lifecycle.test.mjs and additions to
tests/test_image_folder.py cover each case; the frontend cache and
media upload tests now call the handlers directly instead of starting a
local server.

A run's completion note carries an extra context field and lists every
loaded picture. No inputs, outputs or mapping keys change.
2026-11-08 19:21:09 -06:00
ausboss 7d9e972f77 Keep node colours, snap corner resizes and show About in Nodes 2.0
Review fixes for the canvas extensions.

- A node colour picked from the right-click menu, Theme default
  included, now stays after save, reload and paste. An invalid Custom
  colour no longer overwrites the last good one.
- About this node appears in the right-click menu in Nodes 2.0, where
  the ? badge does not work.
- Resizing a node by its corner in Nodes 2.0 snaps to nearby lines;
  Shift bypasses the snap.
- A plain click on a resize handle no longer resizes the other selected
  nodes in Resize Together.
- Sideways mouse-wheel scrolling stays inside a code block that can
  scroll sideways.

Tests: tests/canvas_extensions.test.mjs runs the real extension code
against a fake page for each case above; tests/canvas_passthrough.test.mjs
gains the horizontal-wheel case.

Appearance now saves the chosen colour as a node property (ausbossColor).
No inputs, outputs or mapping keys change.
2026-11-08 19:18:28 -06:00
ausboss a6e2b65e97 README: add Hugging Face and X badges (#309) 2026-11-07 18:14:05 -06:00
ausboss d513723bb0 Release 2.22.0 (#306) 2026-11-07 18:12:07 -06:00
ausboss 9f8bf4d803 Calmer node cards and picture bar on one shared palette (#308)
* Restyle widget cards in solid gray on one shared palette

Cards, number boxes and off|on switches take their colors, font and radii
from js/shared/theme.mjs. Controls are solid gray with no outlines on a
near-black card, grays are neutral, faint text holds 4.5:1, and number boxes
in a card are as tall as the row. A test keeps colour literals out of the
three modules and checks text contrast.

* Lift the card toward the node colour and brighten the off mark

* Restyle the picture bar, its stepper and switch on the shared palette

* Darken the card a step

* Picture frame back to the lighter well; changelog covers the picture bar
2026-11-07 17:41:47 -06:00
ausboss d6f7ddc5a3 Turn Fill small holes off by default on Mask by Touch (#307)
It quietly filled holes I wanted to keep, and it's folded under More so
you don't see it's on. New nodes now start with it off. Saved workflows
keep whatever they had. The Mask by Touch example is saved with it off.
2026-11-07 17:11:08 -06:00
ausboss 8f4e6f20fd H3 Prompt Writer: only a real voice makes a swapped person speak (#305) 2026-11-07 16:58:11 -06:00
ausboss 8bc2d183c9 Mask by Touch: show a changed picture without unplugging it (#304) 2026-11-07 16:07:39 -06:00
ausboss 421849cae0 Save Image and Save Video: call the Steps switch Preview steps (#303) 2026-11-07 11:37:47 -06:00
ausboss d7415f7863 Release 2.21.2 (#302) 2026-11-07 09:52:10 -06:00
ausboss 6e48ca10d8 H3 Prompt Writer: plain swap sentences, the new person from the first frame, a looks line written from clip (#300)
A sentence like "Replace the man in the red shirt with @1" now names only the person who goes. Every swap with a picture asks for the replacement from the first frame. A text model on clip writes the @1 looks like line for hair and face when none is typed.
2026-11-07 09:49:14 -06:00
ausboss 0b9259f86b Fix Workflow Note Tile growing in classic canvas (#298)
Suppress ComfyUI's core image preview on Workflow Note panels so stale previews cannot enlarge compact tiles.
2026-11-07 02:37:50 -06:00
ausboss 0386e9c416 Keep node IDs unique across frontend smoke probes (#297) 2026-11-06 20:11:28 -06:00
ausboss 95a259cc69 Keep a new Workflow Note Tile compact in the classic canvas (#294) 2026-11-06 19:10:07 -06:00
ausboss eb3b74242a Fix shared note tiles and H3 video edit controls (#293) 2026-11-06 18:48:28 -06:00
ausboss 28c4f6aeb5 Add manual Registry publishing for Model Downloader (#292) 2026-11-06 15:11:41 -06:00
ausboss 3a4ce52760 Release 2.21.1
Update release metadata and notes for the repository-hosted README badge fix.
2026-11-06 11:48:17 -06:00
ausboss e761b3dd0b Serve the release badge from the repository (#290)
Keep the release badge available from GitHub and reject stale generated badge versions during release preflight.
2026-11-05 20:28:47 -06:00
ausboss 3b0baf351a Release 2.21.0 and keep GitHub releases current (#289)
Publish matching pack and frontend versions, release notes, and automatic GitHub Releases after Registry uploads. Point the README badge at the latest release.
2026-11-05 20:19:37 -06:00
ausboss 50389b0cdb Improve crop controls, playback, and media selection (#288)
* Group crop controls and put video scrubbing below the preview

* Stop IN where the connected clip length still fits

* Add play and pause beside the crop scrubber and clarify resize padding

* Preview the current brush beside Size in Image Compare

* Center the crop and its padding together on each axis

* Polish crop controls and load images from Outputs

* Add crop controls preview
2026-11-05 20:07:33 -06:00
ausboss 28dd526447 Remove the LoRA Loader stack row limit (#287) 2026-11-05 20:07:24 -06:00
ausboss abf6cecd52 Style workflow guide and model download tiles (#286)
* Style workflow guide and model download tiles

* Add workflow tile preview
2026-11-05 20:04:58 -06:00
ausboss 12e7d8c33f Limit Latent Size to workflow-approved resolutions and share panel controls (#285) 2026-11-05 18:23:30 -06:00
ausboss de95db228c H3 Prompt Writer: a swap that keeps the clip's sound only gives the new person their own lines, timed from that sound (#277)
* Add Load Audio node: wave, trim, preview and max length

Load a sound file and keep only the part you want. The node shows the
whole file as a wave. Drag the IN and OUT handles (or type the times),
press play to hear just that part, and set Max length to the longest
sound the next step takes: anything past it turns red and is cut off,
so you see what comes out before you run. Max length can be wired.

Only the picked part is decoded. Outputs the audio and its seconds.
Help page, README, changelog, Chinese names, tests and the API snapshot
are included.

* Load Audio: open smaller and go down to 280 wide

* Load Audio: shorter wave, length moves off the wave next to OUT

* Add the Pika H3 Flex example workflow

* H3 Prompt Writer: a swap puts each person picture in one person's place

* H3 Prompt Writer: keep quoted words in a swap and read who goes out of a longer line

A swap dropped every typed line once it had read who goes, so words in
quotes never reached H3 and the new person's lips were guessed. Those
lines now stay as the new person's words, landing on the lip movement of
the video, and a voice with a swap no longer says nobody speaks.

"Elon is @1, he replaces the person at the counter" went whole into the
description of who goes. The words after replaces (or takes the place of,
instead of, ...) are now who goes, and a name given to @1 is left out.

* H3 Prompt Writer: swap lines go to the right speaker

Names given with a comma ("Altman is @1, the man on the left. Dario is
@2, ...") no longer run into each other, and a later "Dario says" or
"He says" goes to that person. A thing that speaks, like the card
reader, says its words from off screen so nobody's lips move. "raps"
and a few other ways of speaking count as speech, so "and raps" stays
out of who goes.

* H3 Prompt Writer: keep how a swap line is said, and its exact time

"The card reader says in a flat robot voice" now names the card reader
and keeps "in a flat robot voice" as how it says the words. A time like
3.2s at the start of a line was read as a list number and lost its 3; it
now stays 3.2, a time typed inside a swap line counts too, and the first
words of a swap say when they start.

* H3 Prompt Writer: a person off screen gets their own voice

"A woman off screen says" went to the person on screen, as their voiceover,
and in a swap it landed on the swapped-in person. Now a person or a name
the line puts off screen (off screen, off camera, behind the camera, O.S.)
speaks from off screen as their own speaker, in a swap and outside one.
An unnamed voiceover, an @ or a he or she still stays the main person's.
Off-screen voices no longer get a 'stays silent with the lips closed' line.

* H3 Prompt Writer: a swap can keep the clip's clothes

A swap always dressed the new person in the picture's clothes. Typing
"keeps her outfit", "in the clip's clothes" or "keep the clothes from the
video" now takes only the face and hair from the picture and keeps the
clothes of whoever goes. "keeps her own clothes" still means the picture's.

* H3 Prompt Writer: a thing said to be off screen speaks for itself in every mode

"A card reader off screen says" got its own voice only in a swap. In copy
its moves and the other modes it became the person on screen's voiceover.
Anything a line puts off screen now speaks for itself; "a voice" and
"narrator" stay the main person's voiceover.

* H3 Prompt Writer: a swap that keeps the clip's sound gives the new person only their own lines, timed from that sound

With the clip's own sound under a swap, H3 heard every voice and moved the
new person's mouth to lines that weren't theirs. The new Clip's own sound
switch leaves out anyone else's words, says the new person keeps the lips
closed between their lines, and puts each untimed line where speech starts
in the video's sound. A typed time still wins.

* H3 Prompt Writer: tell voices apart by pitch when timing lines from the clip's sound

Loudness gaps alone put the new person's lines on the other speaker's words
when two people trade lines quickly. Each line now takes a run of speech
that fits its syllables, starts after a quiet, and keeps one voice at one
pitch. On the lip sync clips this lands every line within 0.15 s of
Whisper's times, except one soft word under a crowd.

* H3 Prompt Writer: a short name for who goes in a swap keeps their line

"The man in the car" for "the man in the back of the police car" was read
as someone else, so with the clip's sound on his line was left out. A
speaker is who goes when every word naming them appears in who goes.

* H3 Prompt Writer: a swap with the clip's sound and no words typed speaks as the video does

With nothing in quotes, it said the new person keeps the lips closed and
says nothing, even when the clip is full of speech, and the soundscape
was the place's steady sound. Now speech in the clip means the new person
speaks as in the video, and the soundscape is the clip's own sound. With no
speech heard and no words typed, it says nothing about the lips.

* Document two-person swaps and off-screen dialogue
2026-11-05 15:58:53 -06:00
ausboss 93f64d07d7 H3 Prompt Writer: swap lines keep their words, speaker, delivery and time (#275)
* Add Load Audio node: wave, trim, preview and max length

Load a sound file and keep only the part you want. The node shows the
whole file as a wave. Drag the IN and OUT handles (or type the times),
press play to hear just that part, and set Max length to the longest
sound the next step takes: anything past it turns red and is cut off,
so you see what comes out before you run. Max length can be wired.

Only the picked part is decoded. Outputs the audio and its seconds.
Help page, README, changelog, Chinese names, tests and the API snapshot
are included.

* Load Audio: open smaller and go down to 280 wide

* Load Audio: shorter wave, length moves off the wave next to OUT

* Add the Pika H3 Flex example workflow

* H3 Prompt Writer: a swap puts each person picture in one person's place

* H3 Prompt Writer: keep quoted words in a swap and read who goes out of a longer line

A swap dropped every typed line once it had read who goes, so words in
quotes never reached H3 and the new person's lips were guessed. Those
lines now stay as the new person's words, landing on the lip movement of
the video, and a voice with a swap no longer says nobody speaks.

"Elon is @1, he replaces the person at the counter" went whole into the
description of who goes. The words after replaces (or takes the place of,
instead of, ...) are now who goes, and a name given to @1 is left out.

* H3 Prompt Writer: swap lines go to the right speaker

Names given with a comma ("Altman is @1, the man on the left. Dario is
@2, ...") no longer run into each other, and a later "Dario says" or
"He says" goes to that person. A thing that speaks, like the card
reader, says its words from off screen so nobody's lips move. "raps"
and a few other ways of speaking count as speech, so "and raps" stays
out of who goes.

* H3 Prompt Writer: keep how a swap line is said, and its exact time

"The card reader says in a flat robot voice" now names the card reader
and keeps "in a flat robot voice" as how it says the words. A time like
3.2s at the start of a line was read as a list number and lost its 3; it
now stays 3.2, a time typed inside a swap line counts too, and the first
words of a swap say when they start.

* H3 Prompt Writer: a person off screen gets their own voice

"A woman off screen says" went to the person on screen, as their voiceover,
and in a swap it landed on the swapped-in person. Now a person or a name
the line puts off screen (off screen, off camera, behind the camera, O.S.)
speaks from off screen as their own speaker, in a swap and outside one.
An unnamed voiceover, an @ or a he or she still stays the main person's.
Off-screen voices no longer get a 'stays silent with the lips closed' line.

* H3 Prompt Writer: a swap can keep the clip's clothes

A swap always dressed the new person in the picture's clothes. Typing
"keeps her outfit", "in the clip's clothes" or "keep the clothes from the
video" now takes only the face and hair from the picture and keeps the
clothes of whoever goes. "keeps her own clothes" still means the picture's.

* H3 Prompt Writer: a thing said to be off screen speaks for itself in every mode

"A card reader off screen says" got its own voice only in a swap. In copy
its moves and the other modes it became the person on screen's voiceover.
Anything a line puts off screen now speaks for itself; "a voice" and
"narrator" stay the main person's voiceover.

* Document two-person swaps and off-screen dialogue
2026-11-05 15:57:11 -06:00
ausboss b3a3d7a8ca Add frameless workflow tiles and branded Markdown guides (#284) 2026-11-05 15:55:47 -06:00
ausboss c1d6136663 Input loaders: share one os.scandir walk instead of per-file stat (#279) (#281)
* Input loaders: share one os.scandir walk instead of per-file stat (#279)

* Keep input file links that resolve inside the media folder
2026-11-05 00:44:28 -06:00
ausboss 515b9ea4bd Frontend: use relative /scripts/ imports for subpath deployments (#276) (#282) 2026-11-05 00:31:20 -06:00
ausboss c37386c1d4 Seed: add a compact look, picked from the gear (#274) 2026-11-03 23:59:16 -06:00
ausboss 7cb0abaacf Add Load Audio node: wave, trim, preview and max length (#272) 2026-11-03 23:58:16 -06:00
ausboss 939d320b8b Merge pull request #271 from ausboss/fix/resize-selected-together
Line up: resize selected nodes together
2026-11-03 14:38:57 -06:00
ausboss 9bb8a201fe Merge remote-tracking branch 'origin/main' into fix/resize-selected-together 2026-11-03 14:33:13 -06:00
ausboss 52c3353c69 Resize together: rows share height, grids share both
How the selection is laid out decides what it shares: nodes stacked in a
column get the grabbed node's width, nodes side by side in a row get its
height, and a grid gets both. A pull on the top edge keeps each bottom
edge in place.
2026-11-03 14:22:46 -06:00
ausboss ded731859c Integer and Float: a big look, like Text (#270)
Integer and Float get the same gear as Text, with a big look: one large centred number with its arrows, in a taller row. The Pika H3 Flex workflow uses it for Seconds.

The look is kept in the node's properties (ausboss_text_look: "big"), so saved values and what the node sends don't change. A row with its own height in a look now takes that height when the look changes. Docs and changelog updated.
2026-11-03 13:45:54 -06:00
ausboss e14682f54f Line up: resize selected nodes together
With two or more nodes selected, pulling the edge or corner of one now
gives every selected node its width, in the classic look and Nodes 2.0.
A pull on the left edge keeps each right edge in place. Heights are left
alone, one Ctrl+Z undoes it, and Settings > AusBoss > Canvas > Resize
together turns it off.
2026-11-03 11:54:56 -06:00
ausboss 1129232f83 Merge pull request #269 from ausboss/feat/h3-writer-timeline
H3 Prompt Writer: timeline, colored words, Sound and Music boxes
2026-11-03 11:18:36 -06:00
ausboss 9ba82c45b4 H3 Prompt Writer: timeline, colored words, Sound and Music boxes
- Words light up as you type (@1, times, cuts, quotes, labels), with @
  autocomplete and a ? cheat sheet
- Timeline bar shows every picture a line names; drag a block to reorder,
  drag its edge to set its time, click an edge for a cut, click a block to
  jump to its line
- Sound and Music boxes (new optional inputs, appended) fill H3's
  overall_soundscape and non_diegetic_music
- Pictures and video as chips with their jobs; Length, Camera and Look stay
  plain rows so they can still be wired
- Help page and changelog updated; tests for the colors, autocomplete,
  timeline edits and the new inputs
2026-11-03 11:09:26 -06:00
ausboss 099b57f755 Merge pull request #266 from ausboss/feat/h3-prompt-writer
H3 Prompt Writer node
2026-11-02 18:08:09 -06:00
ausboss f27e1293b0 H3 Prompt Writer: tidy imports, add its Chinese name 2026-11-02 16:53:00 -06:00
ausboss 75621c341f Merge remote-tracking branch 'origin/main' into feat/h3-prompt-writer
# Conflicts:
#	CHANGELOG.md
2026-11-02 16:49:47 -06:00
ausboss ba8d2b8048 Merge pull request #268 from ausboss/docs/agent-guide
Add an agent guide and move contributor rules to CONTRIBUTING.md
2026-11-02 16:49:03 -06:00
ausboss 00164f1df5 Merge pull request #264 from ausboss/feat/chinese-node-names
Chinese node names and descriptions (#245)
2026-11-02 16:47:55 -06:00
ausboss 2316afee62 Merge pull request #267 from ausboss/fix/inpaint-stitch-size
Crop For Inpaint: scale crops evenly and default to multiple 32; Tone…
2026-11-02 16:05:25 -06:00
ausboss 6b1b1cfabc Crop For Inpaint: scale crops evenly and default to multiple 32; Tone match skips see-through pixels 2026-11-02 16:02:53 -06:00
ausboss aaf63b9d9c Changelog: H3 Prompt Writer 2026-11-02 14:22:27 -06:00
ausboss 17fb493a66 Merge remote-tracking branch 'origin/main' into feat/h3-prompt-writer 2026-11-02 14:20:06 -06:00
ausboss 3e2ae322f8 H3 Prompt Writer: words typed for a swap say which person goes 2026-11-02 10:44:05 -06:00
ausboss a61d3d4ee0 H3 Prompt Writer node with width and height outputs 2026-11-02 02:07:29 -06:00
ausboss dff08a3b6e Merge pull request #265 from ausboss/feat/snap-into-line
Add an agent guide and move contributor rules to CONTRIBUTING.md
2026-11-01 22:22:55 -06:00
ausboss 91b53162cc Add an agent guide to the nodes and move contributor rules to CONTRIBUTING.md 2026-11-01 20:52:18 -06:00
ausboss 1ea57b6538 Snap into line and Line up: nodes and groups line up while dragging, and a toolbar panel lines up a selection 2026-11-02 02:29:33 +00:00
ausboss 6d6876dd38 Chinese node names and descriptions (#245)
locales/zh/nodeDefs.json gives every public node a Chinese name,
description and input and output names, which ComfyUI shows when its
language is set to Chinese. Cards and help pages stay English.
tests/test_locales.py keeps the entries matched to the node API, and the
preflight lets locales/ ship.
2026-11-02 01:26:38 +00:00
ausboss f9e17c1fdd Merge pull request #261 from ausboss/feat/stitch-to-original
Stitch Inpaint: Paste onto puts a hand crop back on the whole picture
2026-11-01 02:30:11 -06:00
ausboss 21ca6e9f47 Stitch Inpaint: Paste onto puts a hand crop back on the whole picture
Image Crop + Rotate + Pad's stitcher now remembers the picture and the
crop's position. With Paste onto on original, Stitch Inpaint lays the
result on the whole picture, straight, resized or turned. The default
stays crop, so saved workflows run as before.
2026-11-01 01:24:49 -06:00
ausboss 5a41046e3f Merge pull request #260 from ausboss/feat/switches-choice
Workflow Switches: + Choice, and a guide for AI agents
2026-10-31 21:14:00 -05:00
ausboss 264c133618 CHANGELOG: + Choice and the agent guide are unreleased 2026-10-31 21:01:23 -05:00
ausboss 310d2ba5c3 Merge remote-tracking branch 'origin/main' into feat/switches-choice 2026-10-31 21:01:08 -05:00
ausboss b5bce60c22 Workflow Switches: + Choice, and a guide for AI agents
Two named settings used to take about 20 clicks through a menu and
Settings. + Choice saves how a node is set under a name, and the second
one turns the card into a pill. What a choice changes is what it holds
differently from the other choices, so it needs no off side. A choice can
also hold the nodes only it needs, so a missing model in the branch nobody
picked no longer stops the run.

js/docs/agents/workflow_switches.md tells an AI agent how to write a card
into a workflow file. It sits under js/docs because that ships with the
pack. A test holds its examples to what the card really stores.
2026-10-31 21:01:07 -05:00
ausboss e532e56ed9 Merge pull request #259 from ausboss/release/2.20.5
Release 2.20.5: LaMa Inpaint only opens known model files
2026-10-31 19:22:38 -05:00
ausboss f7d6596937 Release 2.20.5: LaMa Inpaint only opens known model files
A LaMa model file can run code when it opens, so LaMa Inpaint now checks
the file first and stops on anything it doesn't know. Frame Interpolate
errors no longer show server folders, Image Folder's "start over" keeps
going past the last picture, and One at a time in Workflow Switches holds
for new switches and for the setting being turned on.
2026-10-31 19:21:09 -05:00
ausboss 5f32967ee5 Merge pull request #257 from ausboss/fix/switches-one-at-a-time
Workflow Switches: One at a time holds for new switches and the setting
2026-10-31 15:51:45 -05:00
ausboss b01e9a4c70 Merge pull request #258 from ausboss/docs/readme-discord
README: link the Discord server
2026-10-31 13:39:06 -05:00
ausboss c669f1100d README: link the Discord server 2026-10-31 13:36:40 -05:00
ausboss 39da7a3931 Workflow Switches: One at a time holds for new switches and the setting
A new switch read on beside the rows already on, and turning the setting
on left every row on. A new switch now gives way to the rows that were
there, and turning the setting on keeps one row on.

Fixes #256
2026-10-31 08:49:11 -05:00
ausboss 8b75948359 Merge pull request #254 from ausboss/fix/image-folder-start-over-docs
Image Folder: say what "start over" does
2026-10-31 01:20:48 -05:00
ausboss 33ce662398 Image Folder: say what "start over" does
The README, help page and tooltip now say that the pictures go round
after the last one, and the CHANGELOG has the fix from #252.
2026-10-30 23:23:08 -05:00
ausboss 7f7481c505 Merge pull request #252 from MarxMitX/fix/image-folder-start-over
Image Folder: "start over" fixed
2026-10-30 23:16:36 -05:00
ausboss 68f4f4b301 Merge pull request #253 from ausboss/fix/lama-official-model
LaMa Inpaint opens only known model files; Frame Interpolate errors name no folders
2026-10-30 19:41:28 -05:00
ausboss ff6706a918 LaMa Inpaint opens only known model files; Frame Interpolate errors name no folders
A LaMa model file can run code when it opens, so the node checks the file's
SHA-256 first and opens only the two common builds of big-lama.pt. Frame
Interpolate's errors now name the file, and the folder goes to the console.
2026-10-30 18:09:45 -05:00
MarxMitX 4afb60c921 Image Folder: "start over" fixed
With "Run" -> "one per run", "After a run" -> "next" and "At the end" -> "start over", Picture
stopped at count + 1 after the last picture. The backend maps that to
picture 1, but nextPosition() capped it at count + 1 again, so every
following run loaded picture 1.

nextPosition() now takes at_the_end and correctly wraps the number around after
the last picture.
2026-10-30 23:38:34 +01:00
ausboss c4a64366a6 Merge pull request #251 from ausboss/release/2.20.4
Release 2.20.4: Workflow Switches explain "empty"
2026-10-30 13:18:35 -05:00
ausboss e4b9a08cec Release 2.20.4: Workflow Switches explain "empty"
The paste fix in 2.20.3 works when the paste happens, so a workflow pasted
and saved on an older version keeps its old numbers and still reads
"empty" after an update. The word's hover text now says what happened and
what to do: open the original file again, or add nodes from the switch's
menu. The help page says the same.
2026-10-30 13:17:56 -05:00
ausboss a5358c4f78 Merge pull request #249 from ausboss/release/2.20.3
Release 2.20.3
2026-10-29 11:37:06 -05:00
ausboss d4df8f7508 Release 2.20.3 2026-10-29 11:36:28 -05:00
ausboss e93f3337a1 Merge pull request #248 from ausboss/fix/switches-after-paste
Workflow Switches and Callout: follow the copies after a paste
2026-10-29 11:36:04 -05:00
ausboss d0658ea8b2 Workflow Switches and Callout: follow the copies after a paste
Copy and paste give every pasted node and group a new id, and so do
duplicate, Alt + drag and dragging a saved workflow onto the canvas. A
Workflow Switches card remembers its parts by id, so a whole workflow
pasted into a new tab read "empty" on every row and nothing could be
switched on; pasted into another workflow it could switch unrelated nodes
that had the old numbers. A Callout's links to a node or group broke the
same way.

The frontend's paste (LGraphCanvas._deserializeItems) returns which new
node each old id became. js/shared/paste_ids.mjs hands that on after every
paste, and a pasted card or note points at the copies of what it names.
An id the paste did not copy is kept, so a card pasted on its own still
switches the original parts.
2026-10-29 11:05:44 -05:00
ausboss 0853872174 Merge pull request #246 from ausboss/fix/save-preview-states
Release 2.20.2: Save Image shows a run more clearly
2026-10-27 08:53:49 -05:00
ausboss 2545b82219 Release 2.20.2 2026-10-27 08:51:48 -05:00
ausboss 13145ed881 Save Image: say a run is going, hold the last step, keep tiles off the picture
While the models load, an empty picture area says the run is going instead
of asking for a run. With steps on, the last step stays up until the saved
picture has loaded, so it no longer blinks out. Between a tiled upscale's
split and stitch (and for Ultimate SD Upscale) the step pictures are tiles:
the whole picture stays and the chip says "upscaling in tiles".
2026-10-27 08:51:40 -05:00
ausboss fe04d93afa Merge pull request #244 from ausboss/release/2.20.1 2026-10-26 20:18:05 -05:00
ausboss 4591dd4ef9 Release 2.20.1 2026-10-26 19:52:35 -05:00
ausboss cd05a91e14 Merge pull request #242 from ausboss/fix/touch-other-picture-message 2026-10-26 19:43:49 -05:00
ausboss 1faca35331 Merge branch 'main' of https://github.com/ausboss/ComfyUI-AusBoss into fix/touch-other-picture-message
# Conflicts:
#	CHANGELOG.md
2026-10-26 19:31:42 -05:00
ausboss 3e79954bbe Merge pull request #243 from ausboss/fix/keep-transparency 2026-10-26 19:25:52 -05:00
ausboss 8837c75553 Keep see-through parts in Crop/Stitch Inpaint and Image Compare
Stitch Inpaint takes an RGB patch over an RGBA picture and keeps the
picture's own alpha, instead of stopping on a channel mismatch. The halo
fix spreads colours only.

Image Compare keeps alpha in a preview that has see-through parts and
flags it, so the panel shows them over a checkerboard (Slide, A/B and
Erase). The Erase mix lays the top picture over the other, the saved PNG
is RGBA, and the PSD carries each layer's transparency plus a merged
alpha, its flat colours stored over white as Photoshop does. A see-through
B is resized premultiplied. Align measures over gray and warps all four
channels. Solid pictures give byte-identical files.

Refs #232
2026-10-26 18:40:49 -05:00
ausboss 69fbceb0a1 Mask by Touch: say when the picks were made on another picture
When the picture coming into the node changed after picking, the run
stopped with "nothing is picked yet" while the picks still showed on the
node. It now says the picks were made on another picture. If nothing set
to empty mask still lets the run go on.
2026-10-26 18:05:42 -05:00
ausboss 714b638ee8 Merge pull request #241 from ausboss/release/2.20.0
Release 2.20.0
2026-10-25 20:52:58 -05:00
ausboss 5d00076254 Release 2.20.0 2026-10-25 18:20:43 -05:00
ausboss 8b7a717004 Merge pull request #240 from ausboss/feat/live-step-preview 2026-10-25 16:03:09 -05:00
ausboss 6af50147fc Step preview: the caption is the step count alone; Step Preview shows the finished picture once it is saved 2026-10-25 15:35:17 -05:00
ausboss c54ee8984f Step preview: Stop takes clicks on Save Image; the step picture fills its box; help notes from real runs 2026-10-25 15:35:06 -05:00
ausboss 3ba27901cb Step Preview node, and a steps switch on Save Image and Save Video
Watch the picture form one sampling step at a time and stop a run that is
going wrong. Step Preview needs no wires and shows whichever sampler is
working, with Stop and Stop all. Save Image and Save Video get a steps
switch, off by default, that shows the steps where the saved picture goes,
with a Stop button.

A queued workflow that shows its steps asks ComfyUI for the auto preview
method for that run, only when no method was chosen in the frontend or on
the command line.
2026-10-25 15:35:06 -05:00
ausboss f131718cf8 Merge pull request #239 from ausboss/release/2.19.2
Release 2.19.2
2026-10-25 13:01:59 -05:00
ausboss e8419b2667 Release 2.19.2 2026-10-25 13:01:02 -05:00
ausboss ef851094ba Merge pull request #238 from ausboss/fix/civitai-failure-text
LoRA Loader: say why a Civitai lookup failed
2026-10-25 13:01:00 -05:00
ausboss 9aefc53134 LoRA Loader: say why a Civitai lookup failed
The button still reads Civitai lookup failed, and a message now gives the
reason. A browser that cannot reach civitai.com at all names the usual
causes: a firewall, a VPN, or ComfyUI started with --disable-api-nodes.
2026-10-25 13:00:09 -05:00
ausboss 1ec9027413 Merge pull request #237 from ausboss/release/2.19.1
Release 2.19.1
2026-10-24 18:33:06 -05:00
ausboss 641b907796 Release 2.19.1 2026-10-24 18:32:11 -05:00
ausboss 1985103fa2 Merge pull request #236 from ausboss/feat/lora-civitai-picture
LoRA Loader: Fetch Civitai info also saves the LoRA's picture
2026-10-24 18:32:00 -05:00
ausboss acad58b613 Tests: skip the picture tests where Pillow is missing 2026-10-24 18:31:12 -05:00
ausboss 15718c07ee LoRA Loader: Fetch Civitai info also saves the LoRA's picture
The browser downloads a small copy of the LoRA's first picture from
Civitai's picture server (PG-13 or milder unless the gear menu's Civitai
picture option says any), and the server saves it beside the LoRA as
<model>.preview.jpg, never replacing a picture. The LoRA Previews add-on
is no longer needed, so its hooks and install hint are gone.
2026-10-24 18:30:09 -05:00
ausboss 438ab30b79 Merge pull request #235 from ausboss/release/2.19.0
Release 2.19.0
2026-10-24 16:57:48 -05:00
ausboss a37dfb93b6 Release 2.19.0 2026-10-24 16:56:41 -05:00
ausboss a6ea043462 Merge pull request #234 from ausboss/feat/lora-ticks-groups
LoRA Loader: add several at once, groups, Civitai pictures add-on
2026-10-24 16:56:16 -05:00
ausboss da98850591 LoRA Loader: hand Civitai info to the LoRA Previews add-on
After Fetch Civitai info saves its file, LoRA Loader sends a browser
event. The separate LoRA Previews add-on answers it by saving the LoRA's
preview picture, and the card and hover show it without a reload. The
info card links to the add-on when a LoRA has Civitai info but no picture.
2026-10-24 16:55:24 -05:00
ausboss 812a22c070 LoRA Loader: groups get their own switch; Workflow Switches can own a group
Turning a group off stops its LoRAs and keeps each row's own on/off for when
the group comes back on. A switch saved with a group on and off now moves only
the group, shows one lock on the group's header, and leaves the rows inside to
the person. Renaming the group keeps the switch; clicking the lock takes the
group back.
2026-10-24 15:18:36 -05:00
ausboss 0afae99b30 LoRA Loader: Shift-click ticks a run in the picker
Shift-click a tick box or name ticks every file from the last one ticked.
Ctrl+Enter ticks the highlighted file and Shift+Enter ticks the run up to it.
2026-10-24 14:58:33 -05:00
ausboss 840f0003dd LoRA Loader: fix picker arrows and drag-out, add stack menu
- The picker's arrow keys no longer stick when the list opens under a
  pointer that has not moved: only a real pointer move takes the
  highlight.
- A group's only LoRA can be dragged out, and a group's first row
  dropped above its own header lands there instead of at the bottom.
  Just below a group's last row is now always outside the group.
- Right-click on the space around the rows opens a stack menu (add,
  group the selection, fold or open all groups, delete all) instead of
  the browser's Copy / Select all; the group menu also ends with
  Delete all.
2026-10-24 14:22:27 -05:00
ausboss 7469c5869e LoRA Loader: tick several files to add, and group rows
The + LoRA list gets a tick box beside each file. Ticked files (also by
Ctrl/Shift-click or Ctrl/Shift+Enter) are added together from a bar at
the bottom; ticks survive a search, and files already in the stack read
"in stack".

Rows can be grouped: Ctrl/Shift-click to select, right-click to group,
move to a group or take out. A group header has a switch for all its
rows, a fold arrow that shortens the node, a double-click rename, and a
grip to drag the whole group. Rows drag in and out by their grip. The
group name is stored on each row, so the stack still runs top to bottom
and groups travel with templates and undo; folded groups are a node
property.
2026-10-24 14:03:06 -05:00
ausboss a1b14cc4a4 Merge pull request #233 from ausboss/fix/note-hint-wrap
Workflow Note: wrap the hint under a model file
2026-10-24 10:18:47 -05:00
ausboss 1051bee665 Workflow Note: wrap the hint under a model file
The line under a file name was cut off with an ellipsis. It now wraps,
and the note grows to fit as it already does for other content.
2026-10-24 10:13:10 -05:00
ausboss 0a098e1027 Merge pull request #230 from ausboss/fix/validators-extra-inputs
Validators: ignore inputs they do not name
2026-10-22 17:25:23 -05:00
ausboss 8f6ac16d27 Validators: ignore inputs they do not name
A pack that wraps node validators in a catch-all takes **kwargs, so
ComfyUI hands it every input and it passes them all on. Load Image + Pad
then stopped the run with "VALIDATE_INPUTS() got an unexpected keyword
argument 'pad_left'" (#229), and every other validator in the pack would
fail the same way.

ignore_extra_inputs drops the inputs a validator does not name and shows
ComfyUI the original signature, so on a plain ComfyUI nothing changes:
only the named inputs are passed, ComfyUI still runs its own range and
list checks on the rest, and a failed check is still reported once.
Every validator in the pack wears it, and a test holds new ones to it.
2026-10-22 15:10:09 -05:00
ausboss e808f4ff51 Merge pull request #226 from ausboss/release/2.18.1
Release 2.18.1
2026-10-21 15:14:53 -05:00
ausboss 35651e3445 Release 2.18.1 2026-10-21 15:14:00 -05:00
ausboss 571f4f1896 Merge pull request #225 from ausboss/fix/asset-drop-on-panels
Panels: hand an Assets card dropped on them to the node
2026-10-21 15:13:49 -05:00
ausboss 6c705b2e42 Panels: hand an Assets card dropped on them to the node
In the classic renderer a picture's card dragged from ComfyUI's Assets
panel loaded into Image Crop + Rotate + Pad only when dropped on the
node's title bar. Dropped on the picture or the controls, the drag never
reached the canvas, which is where ComfyUI finds the node under a drop,
so nothing happened.

The pack already hands a file dropped on one of its panels to the node
underneath. An Assets card carries no file, only
application/x-comfy-asset-info, so that drag now takes the same path.
Nodes 2.0 already handed it to the node.
2026-10-21 15:04:55 -05:00
ausboss b063e6f2c1 Merge pull request #224 from ausboss/release/2.18.0
Release 2.18.0
2026-10-21 14:47:14 -05:00
ausboss 9917dc3d15 Release 2.18.0 2026-10-21 14:46:33 -05:00
ausboss de88c8d5e4 Merge pull request #221 from ausboss/feat/compare-image-b-output
Image Compare A/B: pass on image_b too
2026-10-21 14:41:30 -05:00
ausboss 8062c35f70 Merge main into feat/compare-image-b-output
# Conflicts:
#	CHANGELOG.md
2026-10-21 14:35:56 -05:00
ausboss 1f07ad8ba1 Merge pull request #223 from ausboss/fix/touch-hidden-sockets
Mask by Touch and Image Folder: hidden values take no link
2026-10-21 14:34:22 -05:00
ausboss b6012c48fb Mask by Touch and Image Folder: hidden values take no link
A text link held over Mask by Touch snapped to its hidden `touches`
input, drawn at the top edge of its settings card, and letting go wired
the text in place of the picks. Image Folder's hidden `pictures` did the
same once Folder was wired. The frontend gives every widget an input
socket unless it is marked socketless, and a link dropped on a node
lands on its first free input of the link's type, hidden ones included.

The card's `socketless` option, which only made a combo, now also makes
a one-line text widget for a STRING (markSocketless moves to
widget_card_math.mjs so it can be tested). Mask by Touch's card lists
touches and ask, Image Folder's lists pictures. The backend inputs do
not change: saved values still load by position and the API prompt
still carries them. An older workflow brings the empty slots back on
load and the card drops them again, as it already did for Seam.
2026-10-21 14:32:58 -05:00
ausboss cc5617db04 Merge main into feat/compare-image-b-output 2026-10-21 14:26:12 -05:00
ausboss 3d94052489 Merge pull request #222 from ausboss/fix/load-pictures-comfyui-made
Loaders: take files ComfyUI made, as core Load Image does
2026-10-21 14:23:29 -05:00
ausboss 5b8cad57d7 Loaders: take files ComfyUI made, as core Load Image does
Image Crop + Rotate + Pad, Load Image + Pad and Load Video refused a
picture named the way ComfyUI names a generated file
("photo.png [output]", "[temp]") with "The selected source is outside
ComfyUI's input folder." Core Load Image takes those names.

resolve_input_path now reads the name the same way the video nodes'
Outputs list already does (resolve_server_file): the tag picks the
input, output or temp folder, and the rest is a name below it. A whole
path, "~", "." and "..", and a network path are refused from the text
alone; a link that leads out of the folder is refused once resolved.
Both functions share that code. A whole path inside the input folder,
accepted before, is now refused too, as the security rule asks.

Load Video's player and the shared upstream preview stripped the tag
and asked /view for the input folder; they now keep the folder.

Tests cover input, [input], [output] and [temp] names through the
resolver and through ComfyUI's own validation next to core Load Image,
and the refused escapes. Help pages, SECURITY.md and CHANGELOG say so.
2026-10-21 14:21:55 -05:00
ausboss 2b34005150 Image Compare A/B: pass on image_b too
The node gets a second output, image_b: the B batch, untouched, the
way image_a already passes through. image_a stays the first output, so
saved workflows keep their wires. The slider, its resting side, Erase
and Save are unchanged.

Help page, tooltips, README line and CHANGELOG say so; the node API
snapshot gains the one new output.
2026-10-21 13:47:48 -05:00
ausboss 48a2ce3b33 Merge pull request #220 from ausboss/docs/readme-highlights
README: list the standout nodes at the top
2026-10-20 17:34:52 -05:00
ausboss c017554d05 README: a Mask by Touch clip, from a click to the cut-out 2026-10-20 17:33:34 -05:00
ausboss 249275b78d README: a Layout Boxes clip, new words on a neon sign 2026-10-20 16:18:22 -05:00
ausboss adf9cda5c0 README: list the standout nodes at the top, Mask by Touch and Layout Boxes included 2026-10-20 13:47:59 -05:00
ausboss 838e4fdfbe Merge pull request #219 from ausboss/release/2.17.0
Release 2.17.0
2026-10-19 19:46:50 -05:00
ausboss 4c9cecfe2a Release 2.17.0 2026-10-19 19:46:04 -05:00
ausboss 52b731f1a4 Merge pull request #218 from ausboss/feat/layout-boxes-rotate
Layout Boxes: boxes can be turned, to replace words at a slant
2026-10-19 19:46:02 -05:00
ausboss 8d94da719e Layout Boxes: boxes can be turned, to replace words at a slant
A round knob above the chosen box turns it (Shift: 5 degree steps); a Turn
field under the canvas types the angle and a double-click levels it. The
mask follows the turned box exactly, bboxes and elements keep the straight
box around it, and a turned text box adds how it is tilted to its
description. A box that is not turned saves and outputs exactly as before.
2026-10-19 19:00:42 -05:00
ausboss a808f1eb8f Merge pull request #217 from ausboss/release/2.16.1
Release 2.16.1
2026-10-19 16:40:27 -05:00
ausboss d93289393e Release 2.16.1 2026-10-19 16:39:33 -05:00
ausboss 20c771d9a7 Merge pull request #216 from ausboss/feat/layout-boxes-fine-move
Layout Boxes: easier to place boxes on a picture
2026-10-19 16:39:31 -05:00
ausboss dca5ded302 Layout Boxes: easier to place boxes on a picture
The arrow keys move a box one pixel (Shift: ten), and the grid starts off
when a picture is wired in. Handles, tags and outlines keep one size on the
screen at any zoom, and over a picture the boxes are see-through. Each box's
tag says whether it is text or an object, a chip per box picks the box to
edit, and the type buttons read as that box's own setting. A gear sets the
grid size. With a picture shown, the canvas keeps the picture's own shape
even when width and height are wired.
2026-10-19 16:38:02 -05:00
ausboss c99505c1de Merge pull request #215 from ausboss/release/2.16.0
Release 2.16.0
2026-10-19 14:32:43 -05:00
ausboss a89f49fe69 Release 2.16.0 2026-10-19 14:31:43 -05:00
ausboss 53abb05435 Merge pull request #214 from ausboss/feat/layout-boxes-mask
Layout Boxes: a mask output, to change only what is inside the boxes
2026-10-19 14:31:41 -05:00
ausboss 3379df7e7f Layout Boxes: a mask output, to change only what is inside the boxes
The mask is white inside every box and black elsewhere, at the size of the
picture on background (or the canvas without one). Draw a box over the words
on a sign, wire the mask into an inpaint chain, and only the words change.
It comes after the other three outputs, so saved workflows keep their wires.
2026-10-19 14:31:02 -05:00
ausboss bcff775507 Merge pull request #213 from ausboss/release/2.15.0
Release 2.15.0
2026-10-19 10:13:26 -05:00
ausboss 8f4a3af2b4 Release 2.15.0 2026-10-19 10:11:35 -05:00
ausboss 88a333c2af Merge pull request #212 from ausboss/feat/layout-boxes
Layout Boxes 🆎: a box canvas for layout prompts that works in both node looks
2026-10-19 10:11:26 -05:00
ausboss 166deeae67 Layout Boxes: a box canvas for layout prompts that works in both node looks
Draw boxes on a canvas and say what goes in each one: a thing, or words to
write in the picture. Wire elements to MOSS-Image Layout Prompt and the
picture puts each thing where its box is.

It gives what ComfyUI's own Create Bounding Boxes gives (preview, bboxes,
elements; same types, same order, same values from the same boxes), so it
can take that node's place, and its canvas also draws in the classic node
look. The boxes are kept as parts of the canvas in a hidden socketless
string, so a wired size never moves them.
2026-10-19 09:46:44 -05:00
ausboss c517e00d1d Merge pull request #211 from ausboss/release/2.14.1 2026-10-18 21:31:56 -05:00
ausboss bce780f259 Release 2.14.1 2026-10-18 21:23:48 -05:00
ausboss b569ffbb1b Merge pull request #210 from ausboss/fix/switch-keeps-lora-strength
Workflow Switches: a switch keeps the strength you give a LoRA row
2026-10-18 21:12:36 -05:00
ausboss a10064393b Workflow Switches: a switch keeps the strength you give a LoRA row
A switch that turns LoRA Loader rows on and off compared and wrote each
row's strength as well as whether it is on. A row turned down to 0.6 by
hand read "changed" (a pill went amber, "only partly on"), and the next
click set its model strength back to the saved one and left its CLIP
strength behind.

A LoRA row pair now carries whether the row is on, and a strength (model
or CLIP) only where the switch's saved on and off differ. pairNow compares
only what the pair carries and pairValue writes only that, so the strength
and triggers someone set stay, and the card reads on or off. A switch
saved with one strength for on and another for off still sets it, now
with the CLIP strength too. How switches are saved does not change.
2026-10-18 19:45:09 -05:00
ausboss 2f80577424 Merge pull request #209 from ausboss/release/2.14.0
Release 2.14.0
2026-10-18 15:50:23 -05:00
ausboss 2912289051 Release 2.14.0 2026-10-18 20:49:34 +00:00
ausboss b3424fa4fb Merge pull request #208 from ausboss/feat/compare-align
Image Compare: Align lines B up with A in Erase
2026-10-18 15:43:23 -05:00
ausboss b4f78cf06e Merge branch 'main' into feat/compare-align 2026-10-18 20:39:25 +00:00
ausboss 8e9c833abf Merge pull request #203 from ausboss/fix/timer-skips-touch-looks
Run Timer: leave Mask by Touch's own looks out
2026-10-18 15:39:02 -05:00
ausboss 16ce6ec82e Image Compare: Align lines B up with A in Erase
Edit models often draw the picture a few pixels to the side or a little
zoomed in, so the edges of the paint in Erase show a step. A new ALIGN
button in Erase's tool row measures B against A the way Realign to
Source does and swaps in B moved back over A. Save and Copy use the
lined-up B, and a second click puts B back as it came.

The new route takes only the run's pair key, reads the two previews the
node made, and writes the lined-up B beside them in the temp folder.
2026-10-18 20:38:39 +00:00
ausboss c9a4906d3d Merge branch 'main' into fix/timer-skips-touch-looks
# Conflicts:
#	CHANGELOG.md
2026-10-18 20:18:38 +00:00
ausboss a04f22fb49 Merge pull request #207 from ausboss/feat/browse-pictures
Compare and Save Image: step through several pictures
2026-10-18 15:18:25 -05:00
ausboss 07efe34047 Merge branch 'main' into feat/browse-pictures
# Conflicts:
#	CHANGELOG.md
2026-10-18 20:16:22 +00:00
ausboss d040ef8892 Merge pull request #206 from ausboss/fix/tiled-stitch-image-list
Tiled Upscale Stitch: work with a list of pictures
2026-10-18 15:14:34 -05:00
ausboss bf2f75d487 Compare and Save Image: step through several pictures
A list of pictures runs these nodes once per picture, and the panels
only showed one of them (#204). Arrows (2 / 5) now appear on the node
when a run brings more than one picture: Compare steps through its
pairs, A and B together, and Erase saves the pair on screen. The shared
preview panel gets the same arrows, so Save Image, Mask Refine, Mask by
Name, LaMa Inpaint and Select Frame step through their results. A node
too narrow for the arrows widens to fit while they show.
2026-10-18 17:50:53 +00:00
ausboss 5d720be96f Tiled Upscale Stitch: work with a list of pictures
A list of pictures runs Tiled Upscale once per picture, so Stitch gets
every picture's tiles in one list and one stitcher per picture. It only
read the first stitcher and stopped with a tile count error (#205).
Each stitcher now takes its own tiles off the list and Stitch returns a
list, one finished picture each. One picture comes out as before.
2026-10-18 16:47:25 +00:00
ausboss b73bfce0c8 Run Timer: leave Mask by Touch's own looks out
Mask by Touch looks at a new picture by running itself alone. The Run
Timer started for that look and could show its time as if it were the
workflow's run, and the runtime badges were cleared by it.

The queue wrapper now marks a run that targets nodes only and carries a
Mask by Touch request as the node's own. The timer and the badges skip
those runs, and take back one that started before the queue answered.
2026-10-16 09:20:21 -05:00
ausboss 22167736a0 Merge pull request #202 from ausboss/feat/lora-rows-on-box
LoRA Loader: rows on a subgraph box, and a lock on rows a switch sets
2026-10-15 11:02:06 -05:00
ausboss 79c30f632c LoRA Loader: keep the owned-row line clear of the lock
The teal line on a row a switch sets moves into the stack's padding, a few pixels left of the lock, instead of an inset edge right against it.
2026-10-15 10:58:56 -05:00
ausboss 067f5e21c8 LoRA Loader: rows on a subgraph box, and a lock on rows a switch sets
A LoRA Loader inside a subgraph gets a first gear item, Rows on the box.
Show brings its rows up the frontend's own way (a subgraph input linked to
the loras input), and the box then shows the loader's row card on its face,
bound to the box's copy of the rows, which is what runs. Each copy of the
box keeps its own rows. On the box the card keeps the rows, the master pill
and Re-check; templates and the gear stay inside. The card also appears
when loras is brought up from the frontend's Edit subgraph widgets list.

Inside, the loader says where its rows are set and shows and edits the
copy of the box it was entered through (or the only box); with several
boxes and no known way in it is read only. Take back removes the subgraph
input and keeps the rows of that box.

Workflow Switches needs no change to set rows on a box: the box's copy is
an ordinary widget value. A row that a switch sets shows a lock in place
of its grip, naming the switch on hover; a click on the lock removes that
row from the switch's saved sides (releaseStackRow), and the row still
drags. Pure helpers live in js/shared/lora_box.mjs and
js/shared/workflow_switches.mjs, with tests.
2026-10-15 10:36:10 -05:00
ausboss 2799d17b0f Merge pull request #201 from ausboss/feat/text-big-look
Text: a big look for a word or two (release 2.13.0)
2026-10-14 22:11:22 -05:00
ausboss 94221a212a Release 2.13.0 2026-10-14 22:07:46 -05:00
ausboss f9f87a1108 Text: the big look in the Callout's teal, with its glow 2026-10-14 22:05:55 -05:00
ausboss efedc1bd84 Text: keep a big-look node's saved height when a workflow loads
A Text node saved in the big look between the big floor and the usual
look's floor (424 x 152) came back at the big floor (424 x 130). ComfyUI
fills in the saved text while it configures the node, before onConfigure,
and the text's callback redrew the card in the big look with the saved
size already on the node. The card took a node under the usual floor for
one sitting at it and fitted it to the new floor.

A look that changes with no hand on the card came with the node's own data
(a load, a paste, an undo), so its height change is now kept like a
restore: the node keeps its size, lifted to the floor. Picking a look from
the gear still fits the node to it.
2026-10-14 22:05:55 -05:00
ausboss 9f8ccc557d Text: a big look for a word or two, chosen per node from a gear
The Text card gets a gear in its top-right corner with one setting, Look.
normal is the usual text box and stays the default. big shows the text in
large centred letters in a compact field as wide as the card, about two
lines tall, that grows with the node; longer text scrolls.

The look is kept in the node's properties (ausboss_text_look: "big"), not
in an input, so saved widget values and prompts are unchanged and an older
pack shows the usual box. The widget card gains a per-node look: rows can
carry a height and placeholder for another look, and the card height math
takes the look into account.
2026-10-14 22:05:55 -05:00
ausboss 37b1105527 Merge pull request #200 from ausboss/release/2.12.3
Release 2.12.3
2026-10-14 20:13:12 -05:00
ausboss f92cfc8dfb Release 2.12.3 2026-10-14 20:12:27 -05:00
ausboss f95fb2d7b1 Merge pull request #199 from ausboss/fix/compare-erase-no-grow
Image Compare A/B: Erase keeps the node's size, and a Copy button
2026-10-14 20:11:44 -05:00
ausboss 9794b19f62 Image Compare A/B: Erase keeps the node's size, and Copy puts the mix on the clipboard
Picking Erase no longer grows the node, so it never covers its neighbours:
the tool rows wrap on a narrow node and the picture gives up the room.
A node that 2.12.1 grew and saved in Erase still gets its size back.

Copy draws the mix at full size in the browser and writes it to the
clipboard as a PNG; browsers allow this on localhost and over https.
2026-10-14 19:56:34 -05:00
ausboss 57ef3662fd Merge pull request #198 from ausboss/release/2.12.2 2026-10-14 18:22:57 -05:00
ausboss b614d96929 Release 2.12.2: leave CHANGELOG.md out of the Registry archive
The Registry shows each version's notes, made from CHANGELOG.md when the
version is published, and the full list stays in the repository; nothing
in the pack reads the file. The nodes are the same as in 2.12.1.
2026-10-14 18:12:27 -05:00
ausboss bc5d884d01 Merge pull request #197 from ausboss/release/2.12.1
Release 2.12.1
2026-10-14 13:29:21 -05:00
ausboss c79cac3381 Merge main into release/2.12.1 2026-10-14 13:28:16 -05:00
ausboss 75d2b28b6f Merge pull request #196 from ausboss/docs/stale-text
Update stale text in examples, README and Registry description
2026-10-14 13:27:19 -05:00
ausboss e0fc54bceb Merge branch 'main' of https://github.com/ausboss/ComfyUI-AusBoss into docs/stale-text
# Conflicts:
#	CHANGELOG.md
2026-10-14 13:23:41 -05:00
ausboss 91e7a6b8fc Merge pull request #195 from ausboss/fix/compare-erase-width
Image Compare A/B: leaving Erase gives the node its size back
2026-10-14 13:23:06 -05:00
ausboss 68b4cf7f5c Release 2.12.1 2026-10-14 13:21:35 -05:00
ausboss d14aa17f9d Update stale text: Paint in the Inpaint example, README summary, Registry description
- Qwen Image 2.1 Inpaint example: the note and Callout name Paint (Draw before 2.12.0), need 2.12.0, and link the workflow's video
- Mask by Touch example: needs 2.12.0 for Paint and Erase
- README: section summary covers the newer nodes and no longer counts the examples; compatibility line names the nightly check; Inpaint links its video
- pyproject: Mask by Touch in the description and keywords
2026-10-14 12:56:15 -05:00
ausboss f20f977f93 Image Compare A/B: leaving Erase gives the node its size back
Erase grows a node too small for its tool rows. The size before is kept
in the node's properties, so going back to Slide, A or B returns it,
also after a reload. A side resized while erasing keeps the new size.
2026-10-14 12:51:18 -05:00
ausboss 7232bb3ea5 Merge pull request #194 from ausboss/release/2.12.0
Release 2.12.0
2026-10-14 02:12:21 -05:00
ausboss d9a38a5c05 Release 2.12.0 2026-10-14 02:11:14 -05:00
ausboss 527b5f2ded Merge pull request #193 from ausboss/docs/youtube-badge
README: YouTube badge
2026-10-14 00:50:02 -05:00
ausboss 6cf7225558 README: YouTube badge beside the release badge 2026-10-14 00:48:05 -05:00
ausboss 345d7b165f Merge pull request #192 from ausboss/feat/touch-mask-editor
Mask by Touch: Pick, Paint and Erase, one brush with Image Compare A/B
2026-10-13 23:31:35 -05:00
ausboss 049bccbcd6 Merge remote-tracking branch 'origin/main' into merge-192
# Conflicts:
#	CHANGELOG.md
#	example_workflows/Mask by Touch (AusBoss).json
#	js/shared/touch_stage.mjs
2026-10-13 23:30:58 -05:00
ausboss 7463da0e9a Merge pull request #191 from ausboss/fix/mask-by-touch-off-no-look
Mask by Touch: no "Prompt has no outputs" message while the node is switched off
2026-10-13 23:29:43 -05:00
ausboss 2d6d3ecaa6 Merge pull request #190 from ausboss/docs/youtube-links
Examples and README: link the YouTube channel
2026-10-13 23:29:26 -05:00
ausboss 42655f2606 Mask by Touch: Pick, Paint and Erase, one brush with Image Compare A/B
The node's tool row is one switch, Pick | Paint | Erase (W, B, E). Paint
adds to the mask and Erase rubs it out, exactly as drawn. Ctrl, or the
right mouse button, does the opposite while held. The brush has a size
and a soft edge, set with [ and ], Shift with them, or Alt and the right
mouse button dragged, and it shows under the pointer as it will paint.

Chips are areas: brush marks carry the number of the chip they joined
(the selected chip, else the one they touch, else the paint chip), and
erasing makes no chip. A take-away that takes nothing is not kept; the
node now says per touch whether a take-away took anything ("did"). A
thing taken away keeps its own chip. Ctrl-click un-picks a pick, a click
puts a taken-away thing back. Chips wrap onto more rows.

Soft brush marks ("s" on a paint touch) make the selection soft on both
sides: apply_item and the face's joinMasks mix by strength, the edge steps
run on soft values as the server's do, and the selection picture is grey
where a soft brush painted. The node also sends each picked thing's own
area, so the face can tell which chip a mark touches.

The look at a picture can be paused and resumed from a button under the
picture, and starts on hover also behind Reroute nodes and the pack's own
loaders.

The brush is shared: js/shared/brush.mjs and nodes/_brush_helpers.py.
Image Compare A/B's Erase mode now flips with Ctrl instead of Alt, and
takes E and B as well.
2026-10-13 22:29:10 -05:00
ausboss c91a92a19a Mask by Touch: no "Prompt has no outputs" message while the node is switched off
A workflow that switches Mask by Touch off, for example to find a thing by
its name instead, showed ComfyUI's red "Prompt has no outputs" message when
a new picture was loaded. The node still asked for its own look at the
picture, and ComfyUI leaves a bypassed or muted node out of every run, so
that run had nothing left to do.

- The node sends no run of its own while it is bypassed or muted, by hand,
  by a Workflow Switches card, or inside a box that is.
- A look it skipped that way is made up when it is on again and the
  pointer comes to its picture.
- A click while it is switched off says so on the node.

isSwitchedOff in js/shared/graph_ids.mjs answers for the node and for every
box on the way to it, with tests.
2026-10-13 22:26:48 -05:00
ausboss 7c5778b68a Merge pull request #189 from ausboss/feat/workflow-switches-pick-one
Workflow Switches: a card can show its switches as one pill, where one is always picked
2026-10-13 22:25:00 -05:00
ausboss 355a6f48d6 Examples and README: link the YouTube channel
Each example's Workflow Note gets a YouTube chip first in its links and one
linked line at the top of its how-to. The note is taller by what the new
lines need; nothing else in the examples changes. The README names the
channel next to Civitai and X.
2026-10-13 22:24:18 -05:00
ausboss de58c86ba2 Workflow Switches: a card can show its switches as one pill, where one is always picked
One at a time already made switching a row on switch the other rows off,
but every row kept its own off | on, so all of them could be off. For
choices such as masking by words or by a click, the card can now show its
rows as one control.

- A new card setting, Show as (rows | one pill), is there while One at a
  time is on. Rows is the default and looks as before.
- As one pill, each row is a segment of the pack's segmented pill, the row
  that is on is the filled one, and a click on another segment switches to
  it. A click on the picked one changes nothing, so one is always picked.
- Two to four rows whose names fit are a pill, and the card's minimum width
  follows the names so none is cut short. More rows, or longer names, are a
  list to pick from.
- A graph with none or several rows on, or a row only partly on, is drawn
  as it is, with an amber edge, and the next click leaves exactly one on.
- In the edit view the pill sits above the rows, which keep their frame
  button and menu. Switch everything off is left out of the node's menu
  while the card shows as one pill.

The stored settings grow by one key, show, which reads "rows" when it is
missing, so a workflow saved by an earlier version opens as it did. A click
on a choice uses the same plans as One at a time, for rows that hold nodes,
rows that change settings and rows with an off side.
2026-10-13 21:23:35 -05:00
ausboss b420f4a486 Merge pull request #188 from ausboss/fix/pad-note-flicker
Crop + Rotate + Pad: the amber note under the size keeps still while you drag
2026-10-13 19:13:17 -05:00
ausboss 5bf037d9b9 Crop + Rotate + Pad: the amber note under the size keeps still while you drag
With Resize on, the note that says how much the rounding stretches the
picture came and went every few pixels of a drag, and each time the picture
area and the rows under it jumped by a line.

- No note appears or changes while a pointer is down. A note the drag makes
  untrue goes away, and the note for the new size comes once, 0.2 s after
  the release. A click, a typed number or a new picture still shows its
  note at once; a run of arrow clicks or a held arrow key shows the first
  value and the last.
- The note has a line of its own that is there whenever Resize is on, with
  text or empty, so a note that comes or goes moves nothing. The Even out
  button no longer makes that line a pixel taller.
- The size line only grows while you drag: a step that comes and goes
  ("round to 8") no longer wraps it in and out.
- The full editor's size box and status panel show the same settled note,
  and the status panel keeps its height mid-drag.

The image node and both video nodes share this code. Even out now works its
padding out at the click, because the note can be a moment behind the values.
2026-10-13 16:22:14 -05:00
ausboss 27d677aaa0 Merge pull request #187 from ausboss/feat/compare-erase
Image Compare A/B: Erase mixes the two pictures by hand, with a named PNG and PSD save
2026-10-13 15:34:02 -05:00
ausboss 46f0417d61 Merge main into feat/compare-erase: the README's line on getting the newest version 2026-10-13 15:17:26 -05:00
ausboss d41aeaa1d6 Image Compare A/B: a sliding mode switch, a named save, labels that scrub
The four mode buttons become one switch, Slide | A | B | Erase, whose
marker slides to the choice. In Erase the brush's two jobs and the
picture on top are switches of the same kind, and PSD is a plain
off | on.

Save moves to the bottom right of the node, larger, with a disk icon,
in a row of its own with the name of the files and the PSD choice.

The files can be named. A Name field on the node takes letters,
digits, spaces, dots, dashes and underscores; the server checks the
same rule and refuses anything else, so a folder is never part of a
name. A new optional filename input takes a wired name, such as the
source picture's file name: its folder and extension are dropped. The
save route still names no file to read.

Size and Soft scrub from their label and from the unit inside the box
as well as from the number (the scrub control gains handle() for this).

Workflows saved before load as they were: 60 that use the node were
opened (the pack's examples, published workflows, the oldest saved
shapes), 72 nodes kept their size, mode and links. The node is one
input socket taller.
2026-10-13 15:16:57 -05:00
ausboss 8b0155f6a6 Merge pull request #186 from ausboss/docs/manager-update-line
README: how to get the newest version when Manager is behind
2026-10-13 14:28:33 -05:00
ausboss 56f6522f32 Image Compare A/B: Erase mixes the two pictures by hand
A fourth mode beside SLIDE, A and B. One picture lies on top of the
other and a brush rubs the top one away, so the other shows through.
ERASE and RESTORE pick what the brush does; Alt or the right mouse
button paints back. Size and Soft set the brush, which shows around the
pointer in teal with its soft edge. It takes Photoshop's keys: [ and ]
for the size, Shift with them for the edge, and Alt with the right
mouse button dragged across for the size or up and down for the edge.

SAVE writes the mix to the output folder as compare_00001.png without
running anything again. With PSD lit it also writes a PSD with both
pictures as layers and the paint as a layer mask on the top one, and
the mask as its own PNG. After a save the row under the picture links
each file for download.

The node keeps the strokes, not a painted picture. The panel draws
them, and the save draws them again on the server at the picture's full
size. The save route takes no file name: a run remembers where its two
previews are and gives the panel a key, Save sends the key back, and
the files get a fixed name. The PSD is written by a small writer of the
pack's own, with no new dependency.

SLIDE is still what the node opens on. The node's inputs and output are
unchanged.
2026-10-13 14:27:45 -05:00
ausboss 61baf87981 README: how to get the newest version when Manager is behind 2026-10-13 13:43:41 -05:00
ausboss 222c4e323a Merge pull request #185 from ausboss/release/2.11.2
Release 2.11.2
2026-10-13 11:49:31 -05:00
ausboss 688cbf2646 Release 2.11.2 2026-10-13 11:48:42 -05:00
ausboss 0c99ba200c Merge pull request #184 from ausboss/fix/server-file-names
Video nodes pick an output video from a list; no route sends a file by its path
2026-10-13 11:46:24 -05:00
ausboss 4fe448ccbe Video Crop + Rotate + Pad: pick an output video from a list, nothing is typed
The second source tab was Server file, a box for a typed path. It is now
Outputs: the videos in ComfyUI's output folder, newest first, in the same
list with a preview that the Uploads tab uses. The list asks the server
each time it opens (/ausboss/transform/video/outputs, no parameters), so a
video rendered a moment ago is in it.

The picked video is saved the way ComfyUI names a file in one of its
folders ("clip.mp4 [output]"). resolve_server_file reads that form and
nothing else: a whole path, a drive, "~", ".." and a network path are
refused from the text alone, and a link that leads out is refused once the
path is resolved. A workflow saved with a typed path shows nothing picked
and a line that asks for the pick.
2026-10-13 11:45:31 -05:00
ausboss 97a51f7db5 Server file takes a folder and a name; no route sends a file by its path
Video Crop + Rotate + Pad (Frame and Clip): Server file mode read a whole
path that had to lie inside ComfyUI's input, output or temp folder. It now
takes the folder and the name (output/clip.mp4) and joins them below that
folder. A whole path, a drive, "~", ".." and a network path are refused
from the text alone, and a link that leads out is refused once the path is
resolved. The hint under the field says what to type when a whole path is
in it.

LoRA Loader: the preview route sends a small JPEG made from the picture
beside the LoRA instead of the file itself.

resolve_input_path refuses when ComfyUI's folders are missing instead of
opening the name unchecked. A route's error text that names a place on the
server's disk is replaced whole (nodes/_route_errors.py), on the LoRA
routes too.
2026-10-13 10:34:11 -05:00
ausboss eecd378533 Merge pull request #183 from ausboss/fix/mask-editor-missing-flag
Crop and Pad loaders: no false "Missing Inputs" after the MaskEditor
2026-10-13 01:57:48 -05:00
ausboss 74de1107c6 Crop and Pad loaders: list a picture the MaskEditor saved
ComfyUI calls a media input missing when its value is not in the
widget's list. The MaskEditor writes the file it saved into the image
widget, and no list has that file until the lists are refreshed, so
the node was flagged the next time it came out of bypass. Both nodes
now add a picture to their list once it has loaded. Load Image + Pad
also follows a picture written that way, as the crop node does.
2026-10-12 21:48:45 -05:00
ausboss 27919f2f40 Merge pull request #182 from ausboss/fix/lora-unreadable-file
LoRA Loader: name a file that is not a LoRA, in plain words
2026-10-12 20:57:30 -05:00
ausboss b87be4410b LoRA Loader: name a file that is not a LoRA, in plain words
A row holding a Draw Things copy of a LoRA, a web page saved by a failed download or an empty file stopped the run with PyTorch's weights_only message, which never says which file is wrong. The node now reads the first bytes of each row's file before the run, names the file, says what it is and what to pick, and a file that fails while loading gets the same message with the loader's error at the end.
2026-10-12 18:10:30 -05:00
ausboss f294cf24e6 Merge pull request #181 from ausboss/release/2.11.1
Release 2.11.1
2026-10-12 09:24:33 -05:00
ausboss 5a1bfe26bb Merge main into release/2.11.1: the Qwen Image 2.1 Inpaint example joins the release 2026-10-12 09:23:16 -05:00
ausboss 4b76b2c123 Merge pull request #180 from ausboss/feat/video-workflow-examples
Examples: Qwen Image 2.1 Inpaint
2026-10-12 09:18:13 -05:00
ausboss 60330468be Merge branch 'main' of https://github.com/ausboss/ComfyUI-AusBoss into feat/video-workflow-examples
# Conflicts:
#	CHANGELOG.md
2026-10-12 09:15:57 -05:00
ausboss f88938923f Release 2.11.1 2026-10-12 03:29:27 -05:00
ausboss 619fbff9ee Merge pull request #179 from ausboss/fix/touch-not-ready
Mask by Touch: a click on a lit thing always takes it
2026-10-12 03:26:50 -05:00
ausboss 12e09fbf15 Mask by Touch: a click on a lit thing always takes it
While the node was still looking at a picture, a thing could light up
under the pointer and a click on it only left a dot. The click was held
back unless the node was already sure of the thing's name. Three clicks
left three dots, and when the look was done they became three picks at
once: the shirt, the person and one more.

- What lights up is what a click takes, also while the node still looks:
  a click on a lit thing takes that thing at once, whatever the node knows
  about its name.
- The picture says "Looking at the picture…" in its corner, with how far
  along it is ("40 of 104"), for as long as the look is not through.
- A click where nothing is lit yet still waits for the look, as before,
  but one click at a time: the picture says "Your click is in", and more
  such clicks are answered by that message instead of piling up. A click
  on a lit thing does not rush the one that waits, and answers it when it
  is at the same spot.

A click is never kept waiting longer than 45 seconds (was 12): a first
look that also loads SAM 3 takes about 25.
2026-10-12 03:26:10 -05:00
ausboss 775142b260 Merge pull request #178 from ausboss/fix/compare-edge-line
Image Compare A/B: no thin line of the other picture down the edge
2026-10-12 03:26:01 -05:00
ausboss a904150411 Image Compare A/B: no thin line of the other picture down the edge
At some node widths and zoom levels a line of the picture that was not on
show ran down the right edge of the one that was. It stood out when one
side was a padded canvas: its grey showed beside the result. Only the
node's view had it, never the saved picture.

The node drew picture B under picture A, and A carried a clip even when
it was shown whole. A browser draws a picture on whole pixels but cuts a
clip where it falls, so on a stage whose width is not a whole number the
clipped A stopped up to a pixel short of B, and B showed as a line.

A picture that is wholly covered is no longer drawn, and a picture shown
whole has no clip.
2026-10-12 03:25:23 -05:00
ausboss 8f32c74e75 Merge pull request #177 from ausboss/fix/lora-trigger-comma
LoRA Loader: a trigger with a comma in it switches on and off
2026-10-12 03:25:21 -05:00
ausboss 3c80fe9af9 Examples: Qwen Image 2.1 Inpaint, for people who get workflows from the pack
Click a thing in your picture on Mask by Touch, say what to change, and
Qwen repaints only that part. It is the workflow of its page, with the
Seed on fixed as every example here has it.

The workflow carries a note above its Workflow Note that links to the
video channel. The example check wanted every node but the Workflow Note
inside a numbered group, so that note counted as a stray node. A Callout
in the Workflow Note's own column now belongs to the note. A Callout
anywhere else outside the groups, and any other node in that column, is
still refused.
2026-10-12 01:38:09 -05:00
ausboss 0e815ee6f0 LoRA Loader: a trigger with a comma in it switches on and off
Some LoRAs list a whole sentence as their trigger. A click on it in a
row's info card added it to the row, but its chip stayed dark, and a
second click added it again instead of taking it out.

A row keeps its triggers as one comma-joined text. The chip's check and
toggleTrigger both split that text at commas and looked for the whole
trigger as one part, so a trigger with a comma of its own never matched.
A trigger is now looked for as the run of parts it becomes (hasTrigger),
and toggleTrigger uses the same search. Off takes out every copy, so a row
that already holds the sentence twice is cleared with one click. A click
refreshes every chip of the card: the same words can sit in two lists.

No Python change and no input change. The row text and the triggers
output are what they were for every trigger.
2026-10-12 01:28:52 -05:00
ausboss 8dcf622870 Merge pull request #164 from ausboss/release/2.11.0
Release 2.11.0
2026-10-12 00:08:41 -05:00
ausboss a59ca729cf Release 2.11.0 2026-10-12 00:05:29 -05:00
ausboss 6255c2293d Release notes: the pack's mark goes with the space before it
A changelog entry that opens with a node's name and a colon, as in
"Callout: links", came out as "Callout : links" in the notes sent to the
Registry: the mark after the name was taken off and its space stayed.
2026-10-12 00:05:29 -05:00
ausboss a2c584210f Releases: a version's changelog entries go to the Registry as its notes
The Registry has shown no notes for any version since 1.2.0, when
publishing moved to the publish action: it sends none. comfy-cli takes
them from COMFY_NODE_CHANGELOG, so the publish workflow now puts this
version's CHANGELOG entries there before it publishes, one short line
each: the entry's bold opening, and for a new node or example the sentence
that says what it does.

scripts/changelog_notes.py makes the notes and prints them for any
version. The release check fails when the pyproject version has no
entries in CHANGELOG.md, so a release cannot go out blank.
2026-10-12 00:05:29 -05:00
ausboss 4ad071a3ac Merge pull request #176 from ausboss/fix/touch-look-follows-picture
Mask by Touch: another picture ends the look at the last one
2026-10-12 00:05:18 -05:00
ausboss ed32667a52 Mask by Touch: another picture ends the look at the last one
Choose a picture and the node looks at it for a few seconds. Choose
another one in that time and the node finished the first look before it
started on the second, so the new picture sat there while the old one was
still being worked on.

A look gives way to the next job in ComfyUI's queue. The new picture's
run is now sent at once, behind the look that is on its way, which ends
that look within a moment. Its answer is dropped: it is about a picture
the node no longer has.

- Image Crop + Rotate + Pad and Load Image + Pad: the look at the new
  picture no longer waits for the old one to be over.
- A plain Load Image: the same, for a look that was on its way when
  another file was chosen.
- A second look at the same picture still waits for the first, and
  nothing goes behind a run that is no look.

Also: a plain Load Image that had no file when the workflow was opened
was not watched, so the first file chosen on it did not show on the node
until something else made it look again. It is watched now.
2026-10-11 23:43:11 -05:00
ausboss 6b9ad99c9b Merge pull request #174 from ausboss/fix/crop-rotate-pad-resize-first
Resize: the picture is resized first, then padded
2026-10-11 20:46:26 -05:00
ausboss 1df8f1e603 Merge pull request #172 from ausboss/fix/crop-rotate-pad-unpadded-edge
Divisible by: no strip of fill on an edge without padding
2026-10-11 20:45:58 -05:00
ausboss f601467eef Resize: the picture is resized first, then padded
With Resize on, Image Crop + Rotate + Pad and both Video Crop + Rotate +
Pad nodes resized the padded canvas as one picture. Along every seam that
mixed fill into the picture and left the mask part on, part off. A hard
paste (stitch blend 0) kept that column as a faint line, and a band under a
pixel wide after the resize came out as nothing but that column: 1 px of
padding at a 0.62 scale left a line in the stitched result whatever the
model painted.

resize_then_pad (nodes/_transform_engine.py) resizes only the picture, with
the chosen method, into the box it has on the resized canvas, and puts the
fill around it. Bands are whole pixels, the picture's edge is picture, and
a band that comes to under half a pixel is left out. The mask is 1 on the
fill; inside the box it is the transform's own mask there (the feather's
ramp, the corners of a turn, see-through parts), resized bilinear with the
picture. The prompt view is placed the same way.

The box is transform_picture_box (nodes/_inpaint_crop_helpers.py), which
the stitcher now uses for its source bbox too, so the paste and the bbox
cannot disagree. It is never empty.

The output size is unchanged. With Resize off, or with no padding, image
and mask are bit for bit what they were. With padding, the picture can sit
up to half a pixel from where it did.
2026-10-11 20:43:52 -05:00
ausboss 5ec02ef356 Divisible by: no strip of fill on an edge without padding
Image Crop + Rotate + Pad and both Video Crop + Rotate + Pad nodes rounded
the canvas up to Divisible by and put the pixels left over on the right and
bottom, whatever was padded. A 1916 x 821 picture with 3 px of padding on
the left and Divisible by 24 came to 1920 wide, with 1 px of fill on the
right edge, which had no padding. With Resize on, that strip was smeared
into the picture's last column, and the stitched result kept it as a light
line.

The canvas now rounds one axis at a time by Load Image + Pad's rule
(_pad_helpers._axis_layout): the leftover joins a side that was padded, the
right or bottom one when both are, and an axis nobody padded has its crop
trimmed evenly to the multiple below. A picture smaller than one multiple
still grows on the far side. With no padding on an axis the size therefore
rounds down, not up: 1000 x 750 at 64 gives 960 x 704, was 1024 x 768.
Inputs and outputs are unchanged.

The editor uses the same rule (axisLayout, resolvePadding and keptCrop in
js/shared/transform_geometry.mjs), so the picture on the node, the size
line and the editor match the run. The size line names fill added on any
side and pixels trimmed, and the centre buttons count fill that sits on the
left or top. The Divisible by tooltip and help pages describe the rule, and
the Krea 2 Outpaint Model Patch warning no longer says Divisible by rounds
the other axis up.

tests/test_transform_engine.py and tests/transform_geometry.test.mjs share
one fixture table, so the editor and the run are held to the same numbers.
2026-10-11 20:41:52 -05:00
ausboss 6f942ef7d3 Merge pull request #175 from ausboss/feat/touch-own-loader
Mask by Touch: the pack's own loaders show their picture by themselves
2026-10-11 19:47:53 -05:00
ausboss c7f26e676b Merge pull request #173 from ausboss/feat/switch-two-sides
Workflow Switches: a row can run one part and switch another off
2026-10-11 19:47:46 -05:00
ausboss 68dd1fe5cc Merge pull request #170 from ausboss/feat/node-basics
Five basics the normal nodes have: sums, any wire into Show Text, name tokens, word weights, Image Size's numbers
2026-10-11 19:47:38 -05:00
ausboss 331461f5f7 Merge pull request #171 from ausboss/feat/callout-links
Callout: links to a page, a node or a group
2026-10-11 19:47:35 -05:00
ausboss 8c948ab78f Merge pull request #169 from ausboss/feat/callout-small-text 2026-10-11 19:45:01 -05:00
ausboss 33a576d786 Mask by Touch: the pack's own loaders show their picture by themselves
With a plain Load Image wired straight in, the picture shows on the node
as soon as a file is chosen. With Image Crop + Rotate + Pad or Load
Image + Pad it did not: the node showed Show picture and waited for a
press, because those loaders may crop, turn or pad the file, so the file
alone is not the picture.

The node now asks for that run itself. A moment after a file is chosen on
one of those loaders, a setting on it is changed or the wire is made, the
loader's picture is on the node, cropped and all.

- Changes are waited out for 0.7 seconds, so a crop being dragged sends
  one run when it rests, not one per step.
- Only in answer to something the person did: opening a workflow sends
  nothing, and picks saved with a workflow still wait for their own
  press.
- Nothing is sent without SAM 3 wired, or with looks set to "only on a
  click or the Look button".
2026-10-11 18:14:31 -05:00
ausboss 7aa92f0c3b Tests: the Show Text size test runs without torch
The standard-library run has no torch, and the new test imported it. It
now reads a stand-in with a tensor's size, kind and value; the same case
on a real tensor runs where torch is installed.
2026-10-11 18:01:44 -05:00
ausboss 2c4276829b Workflow Switches: a row can run one part and switch another off
A row only turned its own nodes off and on. A choice that means "use this
and leave that out" needed two rows and two clicks, and the two could be
set against each other. An edit, say, takes its size from its picture, so
the node that sets the size has to sit out while the picture loader runs.

A made switch can now have an off side: nodes and groups that run only
while the row is off. Switching the row on turns them off, switching it
off turns them on, in the same click and the same undo step as the row's
own nodes.

- The row's menu gets "The selected run only while this is off" and "Take
  the selected off that side". A node is on one side only: putting it on
  one side takes it off the other.
- A row with an off side shows a small sign of two arrows in the edit
  view; its tooltip names the nodes. The frame button and "Select its
  nodes" take in both sides.
- The row reads on when its own nodes run and its off side does not, off
  when it is the reverse, and mixed otherwise.
- One at a time and "Switch everything on / off" follow each row's off
  side.

Stored as nodes_off and groups_off beside nodes and groups, and left out
while empty, so a switch that does not use it is saved exactly as before.
2026-10-11 18:00:40 -05:00
ausboss 3fb2ce3c0e Callout: links to a page, a node or a group
A note could point at a node with a linked arrow, and that was all. A note
that wants to send people somewhere else, to a video or a model page, could
only tell them where to look.

Words can now be a link, written [the words](target):
- an http or https address opens in a new tab;
- #12 brings node 12 into view, the way a linked arrow does;
- #g3 brings group 3 into view, and an arrow can point at a group too.
A note that is nothing but one link is a big button for it: its words fill
the note.

In the editor the new Link button wraps the selected words, or the word
under the caret. With a node or a group clicked on the canvas first, the
words point at it; otherwise the address comes back selected, ready to be
pasted over. An address pasted over selected words makes them a link.

Only http and https addresses become links; anything else in the round
brackets leaves the text as typed. A link is a real <a> with
rel="noopener noreferrer", it is only ever opened by a click, a press on it
never drags the note, and a double-click is one visit. An older version of
the pack shows a link as typed, so the address can still be read there.

Picking a group as an arrow's target used to write the group's number as
if it were a node's; it now writes #g and the number.

Help page, tests and CHANGELOG entry included.
2026-10-11 17:29:56 -05:00
ausboss 05038baaa3 Five basics the normal nodes have: sums, any wire into Show Text, name tokens, word weights, Image Size's numbers
Found in a sweep of the pack against ComfyUI's own nodes: things the normal
node does that the pack's node did not.

- Every number box takes a sum. Click a number, type 1024*1.5 or
  (1920-64)/2 and press Enter, as in ComfyUI's own number boxes: + - * / %,
  brackets and a sign. The reader is the pack's own (sumValue in
  js/shared/scrub_input.mjs) and is only asked when the text is not a plain
  number.
- Show Text takes any wire. Its input was STRING only. A number, an on/off
  value or a list is shown too; a picture or a latent is shown by its size.
  The output stays STRING and carries the text that is shown, so text
  passes through unchanged and a number comes out as text.
- Save Image takes name tokens in the file name, as ComfyUI's own Save
  Image does: %date:yyyy-MM-dd% and %Node.setting% are filled in the
  browser when a run is sent (js/shared/name_tokens.mjs, now shared with
  Save Video), %width% %height% and the time tokens on the server. The
  path preview shows them. A linked filename is never read for tokens.
- Ctrl + Up and Ctrl + Down change a word's weight in the pack's prompt
  boxes (Text, Krea 2 Encode). The card's text box stopped the key before
  ComfyUI's own listener saw it; those two keys now go on from a multiline
  box, and from nowhere else.
- Image Size shows what it measured beside its outputs after a run
  ("width: 1024"). The numbers go into the output labels, so the node keeps
  its size. ComfyUI says nothing about a node whose answer came from its
  cache, so the numbers are kept in the node's properties and come back
  when a workflow is opened again; a changed picture wire clears them.

No input was added, renamed or reordered. Help pages, README lines, tests
and CHANGELOG entries are included.
2026-10-11 17:10:52 -05:00
ausboss 4609d7df86 Callout: small print under a line
A Callout's text is fitted as large as the note allows, so a note with a
headline and its details ended up with all of it at one middling size. A
line can now be small print: it is drawn small under the line above it, the
first line stays big enough to read from far out, and the note holds more.

In the editor the new Small button marks the caret's line, or every line of
a selection, and takes the mark off again. In the text it is a line that
starts with six hashes and a space.

Why six hashes: every release so far already drops one to six leading
hashes from a line, so a note with small print opened on an older version
of the pack shows the same words with every line at one size, and no note
written before changes its look unless it used that exact mark. A dash was
the first idea, but a dash line is an ordinary line today and dash lists
would have shrunk.

The editor's bar was already squeezed on a narrow note. With one more
button it now drops its label and its hint under 345 px, and goes onto two
rows when the note is tall enough, so the buttons keep their size.

tests/fixtures/callout_released_notes.json holds what release 2.10.1 made
of 14 notes (dash lists, every heading mark, the example notes); the new
parser has to give the same. Help page and CHANGELOG entry included.
2026-10-11 17:10:24 -05:00
ausboss 7574ded256 Merge pull request #168 from ausboss/fix/show-blend-unpadded-edge
Show blend: no tint on an edge without padding
2026-10-11 16:56:41 -05:00
ausboss d0910073b1 Show blend: no tint on an edge without padding
In the Crop + Rotate + Pad editor, Show blend could draw a teal ramp along
the bottom or the right edge of a picture where there was no padding. The
run never blended there: only the preview was wrong. A 1916 x 821 picture
with 100 px of padding on top showed it along the bottom.

The preview drew the generated area on a small raster with one scale for
both axes, while the raster's size was rounded per axis. 921 rows came to
184.58 raster rows on a raster 185 tall, so the last row was only part
covered by the picture and read as generated, and the blend grew that row
into a ramp.

generatedAreaMask (js/shared/stitch_preview.mjs) now works the area out:
the turned picture cut to the crop, mapped onto the raster with each axis
by its own scale, each cell's share measured. An edge of the picture on the
canvas edge is exactly 0, also after a quarter turn. See-through parts are
still drawn from the picture, on the same raster. Feather, grow and blend
are unchanged, and so are the ramps at real seams.

tests/fixtures/show_blend_canvas.json holds the run's blend for eight
settings at the preview's size. tests/test_show_blend_fixture.py holds it
to the run and tests/stitch_preview.test.mjs holds the preview to it.
2026-10-11 16:54:16 -05:00
ausboss 5d20f06842 Merge pull request #167 from ausboss/feat/number-limits
Float and Integer take the limits of what they are wired to
2026-10-11 16:54:00 -05:00
ausboss 69693d360d Merge pull request #166 from ausboss/feat/lora-folder-picker-keyboard
LoRA Loader: add a whole folder, and use the rows from the keyboard
2026-10-11 16:53:45 -05:00
ausboss 9fd7699ad8 Merge pull request #165 from ausboss/ci/comfyui-updates
Tests: run the pack against the newest ComfyUI every night
2026-10-11 16:53:31 -05:00
ausboss 3d781bb0ec Float and Integer take the limits of what they are wired to
A Float wired to a sampler's denoise could be set above 1, where ComfyUI's
own Primitive node stops at 1. Both number boxes now follow their output to
every input it reaches, through Reroutes and into subgraphs, and keep to the
range all of those inputs allow: by typing, by the arrows and by a drag. A
value that is outside when the wire is made comes to the nearest limit, the
card shows the limits beside the value (0–1), and an unwired box takes any
number as before. No input was added, so saved workflows and API requests
are unchanged.

With it:
- A new box takes the value of the first input it is wired to, as the
  Primitive does. A box that came with a workflow or a paste, or one that
  was already set, keeps its own value.
- Float shows the decimals its steps reach (1.00 wired to denoise, 8.0
  wired to CFG) and goes past them only for a value that uses more. It read
  1.000 everywhere before.
- Integer's own limit goes from 2,147,483,647 to 9,007,199,254,740,991, the
  largest whole number a browser holds exactly, so a seed fits.

The walk over the graph is js/shared/wired_limits.mjs; the scrub control
gains setRange and a way to drop the zeroes its steps never reach. Help
pages, tests and the CHANGELOG entries are included.
2026-10-11 12:54:44 -05:00
ausboss b6ec6db301 LoRA Loader: add a whole folder, and use the rows from the keyboard
- + LoRA opens one list grouped by folder. A click on a file adds it, a
  click on a folder name adds every LoRA under it. Files already in the
  stack are skipped, the add is one undo step, and nothing is added when
  it would pass 64 rows. Folders fold with their own arrows; the folded
  view is remembered in the browser, and search shows matches without
  changing it.
- A row can be reached with Tab and worked with the keys: Up/Down between
  rows and + LoRA, Enter for the file picker, Space for the switch,
  Left/Right for strength (Shift for the fine step). Delete and Backspace
  are kept from ComfyUI there, so they cannot delete the node. Focus goes
  back to a row only when the keyboard was in use; after a mouse click
  the keys stay with ComfyUI as before.
- Remove this LoRA is red in the row menu.
- The row switch reports on/off to screen readers and the strength boxes
  have short names.
2026-10-11 00:14:26 -05:00
ausboss 2c8f7f110c Tests: two checks give the same answer on every CPU
The nightly ComfyUI job is the first place these two files ran with torch on
a GitHub-hosted runner, and one runner failed both on unchanged code.

- test_memory_helpers.py imported torch four times in one process:
  patch.dict(sys.modules) forgets whatever was first imported inside it. The
  fourth import ended the process without a word. Torch is now loaded before
  any test swaps modules.
- test_stitch_classic_pins.py compared blend masks by hash. The feather's
  blur takes an exp, which CPUs round differently, so two masks moved by a
  float32 step. Masks are now compared with frozen release masks at the
  tolerance the images already have, and a new test holds that file to the
  pinned hashes.

The nightly job also prints which CPU ran it.
2026-10-10 22:35:47 -05:00
ausboss 8198e8d530 Tests: run the pack against the newest ComfyUI every night
A ComfyUI update can break a node without a line of the pack changing, and
until now nothing looked: CI installs no ComfyUI. A nightly workflow now
installs ComfyUI's newest release and its development head for the CPU and
runs three checks that need no model:

- tests/test_comfy_core_contract.py reads the pack's source for everything
  it takes from ComfyUI (modules, functions, ComfyUI's own nodes it calls
  and the inputs it passes them) and looks each one up in the real thing.
- scripts/compat_boot.py asks a running ComfyUI which nodes it offers:
  every public node of the pack, and every node and required input the
  example workflows use.
- scripts/dev/face_smoke.mjs puts every node on a graph in headless
  Chrome, in both node views, and fails when a face does not come up or
  something throws.

The full Python suite runs there too. A failure on the release opens an
issue; the development head only after two nights in a row.
2026-10-10 22:06:11 -05:00
ausboss 27cf7cf321 Merge pull request #163 from ausboss/feat/mask-by-touch
Add Mask by Touch: click a thing on the picture, get its mask
2026-10-10 21:41:05 -05:00
ausboss 6338dbb49b Add Mask by Touch: click a thing on the picture, get its mask
A new node in the Mask group. The picture is on the node: moving over it
lights up what a click would take and names it, a click picks it, and two
arrows swap the pick for the next thing at that spot. Subject and
Background take the picture's main subject, or everything else, in one
click. Pick | Draw switches to a brush for an area painted by hand, + and -
add or take away, Undo takes a pick back, and every pick has a chip under
the picture. Fine edges follows hair and fur strand by strand, takes out
background seen between the strands, and gives a cut-out the thing's own
colour. The eye beside Undo and Clear shows the result as a cut-out or as
the mask. Outputs: the mask, the cut-out and the names of what was picked.

The search is ComfyUI's own SAM 3. Subject, Background and Fine edges use
ComfyUI's own background-removal model (ComfyUI 0.21 or newer). The pack
downloads nothing.

With it: a widget card can name its choices with a function and keeps
showing a value its list does not offer, the help page, an example
workflow with its thumbnail, the README section and the CHANGELOG entry.
2026-10-10 19:33:07 -05:00
ausboss ec61f13c72 Merge pull request #162 from ausboss/release/2.10.1
Release 2.10.1
2026-10-09 17:37:09 -05:00
ausboss 70bf45238f Release 2.10.1
The mouse wheel zooms and the middle button pans the graph from every part
of a node in the classic node view. The version pair and the changelog title
follow.
2026-10-09 17:36:26 -05:00
ausboss 6590bb1cc0 Merge pull request #161 from ausboss/fix/zoom-over-node-gaps
Zoom and pan the graph from every part of a node
2026-10-09 17:34:50 -05:00
ausboss e330ec312a Zoom and pan the graph from every part of a node
The wheel did nothing over a preview picture, the text of a Callout or the
padding of a card, and the middle button did not pan there. Those parts of a
panel let the mouse through, so in the classic renderer the pointer lands on
the frontend's .dom-widget wrapper, which takes the mouse and hands nothing
on. The pack looked for its panel between the pointer and the wrapper and
found none. A pointer on the wrapper now counts as over the panel inside it.

scripts/dev/wheel_sweep.mjs turns the wheel at every point of every node and
reports each spot where the graph does not zoom. In the classic renderer 33
of the 41 nodes had such spots; none has one now. Nodes 2.0 had none.
2026-10-09 17:10:20 -05:00
ausboss cd5f8ed428 Merge pull request #160 from ausboss/fix/video-tests-gpu-hidden
Tests: the video tests run with the GPU hidden
2026-10-09 17:07:14 -05:00
ausboss d00b66375c Tests: the video tests run with the GPU hidden
With a ComfyUI checkout set (AUSBOSS_COMFY_ROOT) and no GPU visible,
test_video_clip_transform, test_video_load_helpers and
test_video_stream_start failed with "No CUDA GPUs are available". The
decode loop checks for a cancel through comfy.model_management, and that
module asks for the GPU as it is imported.

core_video_api_available() now puts ComfyUI in CPU mode before it
imports comfy_api, as the node API test already does. These tests need
no GPU, so the whole suite passes with CUDA_VISIBLE_DEVICES="".
2026-10-09 17:00:29 -05:00
ausboss 2c29f2b24e Merge pull request #159 from ausboss/release/2.10.0
Release 2.10.0
2026-10-09 16:23:37 -05:00
ausboss 87e747dd71 Release 2.10.0
The Painted area switch on Image Crop + Rotate + Pad, Image Compare A/B
resting on the result, Workflow Switches finding a LoRA row by its file
name, the Denoise box in the Krea 2 Inpaint Masked example and Tiled
Upscale's report wording. The version pair and the changelog title follow.
2026-10-09 15:17:51 -05:00
ausboss 07db78284e Merge pull request #158 from ausboss/fix/switches-lora-row-by-file
Workflow Switches: a LoRA row is found by its file name, whatever folder it is in
2026-10-09 15:17:01 -05:00
ausboss 38d94b1bf0 Workflow Switches: a LoRA row is found by its file name, whatever folder it is in
A switch that turns a LoRA Loader row on and off looked the row up by its
full saved name. The LoRA Loader gives a row the folder its file is in when
Refresh or its re-check button finds the file moved, so after that the switch
no longer found the row: it left it as it was and the card read "changed".

The row is now matched by exact name first, then as the one row with that
file name (no folder, any letter case, any model extension), the way the
loader resolves names. Two rows with one file name are not guessed at.
2026-10-09 14:43:14 -05:00
ausboss 667a0c143a Merge pull request #156 from ausboss/feat/crop-keep-under-paint
Image Crop + Rotate + Pad: Painted area can keep the picture under the paint
2026-10-08 22:51:32 -05:00
ausboss 02a07b84f3 Image Crop + Rotate + Pad: Painted area can keep the picture under the paint
A part painted in the MaskEditor (see-through in the saved file) was
always handed on as flat fill. The new optional painted_area input,
"fill" by default, can be set to "keep picture": the mask is unchanged
and the image, original and prompt_image outputs carry the stored
picture there. The editor's Inpaint & Stitch section has the switch.
2026-10-08 22:50:12 -05:00
ausboss 416b92f286 Merge pull request #155 from ausboss/fix/inpaint-example-denoise
Examples: Krea 2 Inpaint Masked takes a denoise under 1
2026-10-08 22:50:04 -05:00
ausboss 6e7588e258 Examples: Krea 2 Inpaint Masked takes a denoise under 1
The sampler started from the gray-filled crop, so a denoise under about
0.85 left a flat gray patch. It now starts from the plain crop; the
gray-filled copy stays the reference. A Float - Denoise box is wired to
the sampler. At denoise 1 the prompt differs only in that wire.
2026-10-08 22:49:23 -05:00
ausboss cad96cef77 Merge pull request #157 from ausboss/fix/compare-shows-result
Image Compare A/B: show the result after a run
2026-10-07 21:09:55 -05:00
ausboss 3d9a4c77b5 Image Compare A/B: show the result after a run
With the slider on, the panel stayed on whichever picture the pointer
last left over, and a new run did not change it, so a result could
finish behind the picture it was made from.

The panel now shows the result after every run and each time the
pointer leaves. The result is image_b, or image_a when the node feeding
image_b is upstream of the node feeding image_a; when both come out of
one subgraph the same check is made inside it. The A and B buttons
still hold one picture.

Settings > AusBoss > Image Compare > Rest on the result switches the
earlier behaviour back on.
2026-10-07 20:51:54 -05:00
ausboss ade11ddf3d Merge pull request #154 from ausboss/fix/tiled-upscale-shrink-report
Tiled Upscale: the report says when a picture is made smaller
2026-10-06 21:57:08 -05:00
ausboss 210727d5ca Tiled Upscale: the report says when a picture is made smaller
Asking for fewer megapixels than the picture has got the "too close to
the picture's own size, ask for more" line, which is wrong for a shrink.
The report now says the picture is made smaller first. The help page says
when that is the right thing to do: a big photo that is noisy or soft.
2026-10-06 21:56:14 -05:00
ausboss c60a8e860c Merge pull request #153 from ausboss/release/2.9.0
Release 2.9.0
2026-10-06 21:48:15 -05:00
ausboss 7a55756cb1 Release 2.9.0
Tiled Upscale and Tiled Upscale Stitch, with the Qwen Image 2.1 Upscale +
Restore example, and the Nodes 2.0 fix for Callout. The version pair, the
changelog title, the description and the keywords follow.
2026-10-06 21:47:28 -05:00
ausboss 308472c138 Merge pull request #152 from ausboss/fix/upscale-example-notes
Upscale + Restore example: the switch sits beside Megapixels
2026-10-06 21:47:16 -05:00
ausboss 894010f512 Upscale + Restore example: the switch sits beside Megapixels
The example's controls now run in the order people use them: Megapixels,
one note about it and the Fast switch, the switch with its steps, then the
request and the seed. The picture's note says to load a picture first.
Same nodes, values and wires.
2026-10-06 21:45:16 -05:00
ausboss 7dc18c420b Merge pull request #151 from ausboss/feat/tiled-upscale
New nodes: Tiled Upscale and Tiled Upscale Stitch
2026-10-06 21:40:44 -05:00
ausboss 61d60ae7f5 Tests: drop an unused import 2026-10-06 21:38:58 -05:00
ausboss 98fb121060 Add Tiled Upscale and Tiled Upscale Stitch
Two Image nodes that take a picture past the size a model can redraw in
one go. Tiled Upscale sets the finished size in megapixels and cuts the
picture into tiles; Tiled Upscale Stitch blends the joins, keeps each
tile's colors on the picture's own and fits the result to the size asked
for. Any model can sit between the two.

Adds the tile planner with its tests, the two widget cards, help pages,
README sections, a changelog entry, the API snapshot and a new example,
Qwen Image 2.1 Upscale + Restore. The README's example count now matches
the folder.
2026-10-06 21:20:38 -05:00
ausboss baea40150d Merge pull request #150 from ausboss/fix/callout-nodes2-height
Nodes 2.0: a Callout keeps the size it is given
2026-10-06 20:58:15 -05:00
ausboss 9b3faa5c44 Nodes 2.0: a Callout keeps the size it is given
Nodes 2.0 sizes a node by its content, and the note's text was in normal
flow inside its card. So the text set the note's height: a note could not
be made shorter than its own text, and the text was fitted to a card it had
sized itself. A note made in the classic look and saved 888 x 122 was drawn
20 px taller after a switch to Nodes 2.0, a note dragged taller would not
come back down, and one dragged narrow grew to about 690 px.

The text is now absolutely positioned in the card and centred by its auto
margins, so the card takes its size from the node alone in both renderers.
The classic look is unchanged: same sizes, same pixels.

tests/panel_guards.test.mjs holds the rule for everything the note puts in
its card.
2026-10-06 18:34:35 -05:00
ausboss 6a089e2c44 Merge pull request #149 from ausboss/fix/registry-description-length
Registry: the description fits its 1000 byte limit
2026-10-06 18:09:39 -05:00
ausboss 604bd005d5 Registry: the description fits its 1000 byte limit
The 2.8.0 upload was refused with "description is too long": naming
Image Folder and Callout took the description to 1024 bytes and the
Registry takes 1000. It is 989 now, and release_preflight.py fails on a
description over the limit, so this is caught before a version lands.

The Registry's scan also warned about two statements on one line in
tests/test_image_folder.py and says that becomes an error. They are two
lines now.
2026-10-06 18:07:28 -05:00
ausboss 9c8f7ee233 Merge pull request #148 from ausboss/release/2.8.0
Release 2.8.0
2026-10-06 18:04:43 -05:00
ausboss b8bd2798f7 Release 2.8.0
Callout and Image Folder, the Nodes 2.0 size fixes, and Save Video
showing a saved gif or webp. The version pair, the changelog title, the
description and the keywords follow.
2026-10-06 18:04:10 -05:00
ausboss 863ddf47b3 Merge pull request #147 from ausboss/feat/callout-note
Callout: a note whose text fills the box, with arrows you type
2026-10-06 18:00:20 -05:00
ausboss 5e3f854a15 Merge pull request #145 from ausboss/fix/save-video-gif-webp-preview
Save Video: a saved gif or webp shows on the node
2026-10-06 18:00:12 -05:00
ausboss 64b5c25bce Merge pull request #146 from ausboss/docs/help-pages-match-nodes
Crop editors: spell Center the US way
2026-10-06 18:00:03 -05:00
ausboss 4a5b8c5af3 Callout: a note whose text fills the box, with arrows you type
A new Utility node for explaining a workflow on the canvas. The text grows
to the largest size that fits the note, so it reads without zooming in, and
each point gets a thin teal line beside it.

An arrow emoji in the text (or the arrow row under the editor) becomes a
teal arrow in the line. Click a node while editing, then press an arrow, and
the arrow is linked to that node: clicking it later brings the node into
view, the way Workflow Switches does. The link is kept in the text as #id,
so editing the words never moves it.

The node has one hidden text input and no outputs, so it never runs.
2026-10-06 17:49:53 -05:00
ausboss 1ff1a863fb Crop editors: spell Center the US way
The row next to Fill, Feather and Resize said Centre. It now says Center,
and so do the two button tooltips and the three crop help pages. Code
names and comments are unchanged, since nobody sees them.
2026-10-06 17:49:38 -05:00
ausboss 0c404b37f3 Save Video: a saved gif or webp shows on the node
With format set to gif or webp the file was saved, but the node's stage
stayed empty and said "Execution finished without video metadata".

The node has shown a gif or webp in an <img> since the two formats were
added, because a <video> cannot play either. The run's result was still
searched with a list of video file endings only, so for these two nothing
was found and the <img> was never filled. The list of picture endings now
sits beside that lookup in js/shared/video_preview.mjs, and both the
lookup and the choice of tag read it. A png is still not taken.

In the classic renderer a saved webp was also drawn a second time under
the stage. ComfyUI shows an animated webp in a widget of its own, neither
its video preview nor its picture preview, and only those two were stood
down. suppressCoreVideoPreview hides that widget as well. The node's
smallest height with a saved webp goes from 469 px back to 415 px.

Tests: the lookup finds a saved gif and webp and sends them to the <img>;
every row of the backend's VIDEO_FORMATS is read from its source and has
to be found and sent to the right tag; the animated widget is hidden on
arrival and when it is already on the node.
2026-10-06 17:35:31 -05:00
ausboss f697eaa6f6 Merge pull request #144 from ausboss/fix/nodes2-video-stage-height
Nodes 2.0: Load Video and Save Video can be made shorter than their clip
2026-10-06 17:23:41 -05:00
ausboss 1e1721ab26 Video nodes: the clip no longer sets their height in Nodes 2.0
In Nodes 2.0, Load Video and Save Video could not be dragged shorter than
their clip drawn at the node's full width. With a 9:16 clip, Load Video at
320 px wide stopped at 965 px, where the classic renderer goes to 584 px,
and Save Video at 823 px against 446 px. A node saved at its classic size
was drawn taller than saved and could cover the node under it.

Nodes 2.0 lays a node out by its content, and the saved size is only a
minimum. The shared stage kept its <video> in normal flow at full width, so
a portrait clip set the stage's height. The clip now sits out of flow and
fills the stage, and so does the <img> Save Video keeps for a gif or webp.
The stage takes its size from the node alone and the clip scales to fit.
The player fills the stage, where the preview picture is centred at its
own size, so its controls stay along the stage's bottom edge.

The classic renderer is unchanged.
2026-10-06 16:48:34 -05:00
ausboss 8a6782730b Merge pull request #142 from ausboss/fix/image-folder-saved-size
Image Folder: a node keeps the size it was saved at
2026-10-06 16:21:42 -05:00
ausboss 28ff8c1c4a Merge pull request #143 from ausboss/docs/changelog-unreleased-fixes
Changelog: the Load Video, Image Resize and Workflow Note fixes, and the help pages
2026-10-06 16:21:23 -05:00
ausboss ba94a056a4 Changelog: the Load Video, Image Resize and Workflow Note fixes, and the help pages
These are on main since the last release and were not listed yet.
2026-10-06 16:19:46 -05:00
ausboss b1cbceb42d Image Folder: a node keeps the size it was saved at
A new node opens at its smallest size plus one more row of tiles
(340 x 602). That size was set a moment after the node was made, on any
node that sat at its smallest height. A workflow that loads has put the
saved size on the node by then, so a node saved at its smallest size
looked new and was grown: saved 320 x 506 came back 340 x 602, and saved
400 x 506 came back 400 x 602. A copy, a paste, an undo and Recreate node
grew the node the same way. A node saved 2 px taller kept its size.

The opening size is now set as the node joins the graph. The card is in
place by then, and a size put on the node afterwards wins: a loaded
workflow, an undo, Recreate node. A copy gets its size before it joins
the graph, so a node that is configured with a size is marked and left
alone. A node saved under its smallest size is lifted to it, and one
saved with no size opens like a new node, as before.

tests/image_folder.test.mjs fails when the opening size is set anywhere
but as the node joins the graph, or when a node that comes with a size is
no longer marked.
2026-10-06 15:57:51 -05:00
ausboss 7a0dbe1766 Merge pull request #133 from ausboss/fix/frame-out-pad-bars-subgraph-packs
Three fixes: Load Video OUT after FRAME, Image Resize pad bars, Workflow Note Detect in subgraphs
2026-10-06 15:46:06 -05:00
ausboss d533284b66 Merge pull request #140 from ausboss/fix/nodes2-panel-min-height
Nodes 2.0: a panel keeps its minimum height when a node is dragged shorter
2026-10-06 15:44:34 -05:00
ausboss 8124c92cd9 Merge main into fix/frame-out-pad-bars-subgraph-packs 2026-10-06 15:43:10 -05:00
ausboss 20ae46ad53 Merge main into fix/nodes2-panel-min-height 2026-10-06 15:40:11 -05:00
ausboss 4708e974c8 Merge pull request #134 from ausboss/docs/help-pages-match-nodes
Help pages: say what the nodes do today
2026-10-06 15:39:51 -05:00
ausboss dd34122511 Merge pull request #138 from ausboss/fix/load-image-pad-nodes2-preview
Nodes 2.0: Load Image + Pad shows its picture once
2026-10-06 15:39:47 -05:00
ausboss 2b27fee214 Merge pull request #135: Node text: say what the nodes do today 2026-10-06 15:39:43 -05:00
ausboss 27b75d2807 Merge pull request #141 from ausboss/fix/image-folder-copy-words
Image Folder: say the pictures are copied into the ComfyUI input folder
2026-10-06 14:56:33 -05:00
ausboss 107e4af407 Image Folder: say the pictures are copied into the ComfyUI input folder
Choosing a folder in the file explorer makes the browser ask its own
question ("Upload 74 files to this site?"). A page can neither word that
question nor skip it, and it does not say where the files go.

- While the file explorer is open, the node shows a note: all the folder's
  pictures are copied into your ComfyUI input folder, and the browser's
  upload question is this copy.
- While it copies, the node names the folder and where its pictures go,
  and how far the copy is.
- The big-folder question, the message afterwards and the Browse and
  empty-gallery entries say "copy" too.
- Help page: what the browser's question means, and that a dropped folder
  is copied without it.
2026-10-06 14:55:23 -05:00
ausboss 2e06f01c97 Panels: keep their minimum height in Nodes 2.0
In Nodes 2.0 a node with a picture panel could be dragged shorter than the
panel's minimum. A corner drag took Mask Refine from 356 to 218 px and left
its picture a 2 px line, Save Image lost its saved picture, and Latent Size
lost its whole stage. The classic renderer stops at the panel's floor.

The classic floor is the minHeight a panel declares through fillNodeHeight.
Nodes 2.0 lays a node out by its content and does not use that number, so a
panel whose content has no height of its own had nothing to stop at.
fillNodeHeight now also writes the floor, less the DOM widget frame, to the
panel's element as a CSS min-height while the element sits inside a Nodes
2.0 node. It is written whenever the floor is read and when the panel is
first laid out, so a floor that follows state is followed: with the preview
switched off the node still shrinks to the bar.

The preview panel's stage also carries its own floor inside a Nodes 2.0
node. That panel's declared floor holds less frame than the shared number
takes off, and without the rule its nodes stopped 10 px under the height
ComfyUI lifts them to on the next load.

The classic renderer is unchanged.
2026-10-06 14:48:50 -05:00
ausboss 18b7c17454 Merge pull request #139 from ausboss/feat/image-folder-drop
Image Folder: bring a folder in from anywhere on the computer
2026-10-06 14:10:34 -05:00
ausboss f4a230ccec Image Folder: bring a folder in from anywhere on the computer
A folder dropped on the node, or chosen in the file explorer from Browse or
Add, is copied into ComfyUI's input folder by the browser through ComfyUI's
own upload, and the node moves to the copy. A folder with more than 300
pictures or more than 1 GB asks first. Bringing the same folder in again
writes over its copies.

A whole path pasted into Folder is read as the folder it names when it lies
inside ComfyUI's input or output folder. Any other path is still refused,
and the node now says what to do instead, with a button that opens the file
explorer. The server reads nothing outside those two folders.
2026-10-06 14:06:45 -05:00
ausboss e7c0047c5f Load Image + Pad: show the picture once in Nodes 2.0
In Nodes 2.0 the node showed its source picture twice: on its pad canvas,
and again underneath as ComfyUI's own copy of the node's images. The copy
took the node's spare height: a node 900 px tall gave the pad canvas
278 px, where the classic renderer gives it 512. With a picture wired in,
the copy showed the file named in Source, which the node was not using.

The node's own suppression removed the classic preview widget and blocked
node.imgs, but never set hideOutputImages, the flag Nodes 2.0 reads before
it draws a node's images under the widgets. It sets the flag now. The
classic renderer never reads it.

tests/core_preview.test.mjs scans every frontend file that stands the
classic preview down by hand and fails when one does not stand the
Nodes 2.0 copy down as well.
2026-10-06 13:57:42 -05:00
ausboss 4cfed6293e Merge pull request #136 from ausboss/fix/nodes2-card-height
Nodes 2.0: a card keeps its own height, the picture under it takes the rest
2026-10-06 13:46:12 -05:00
ausboss 2f456ef288 Merge main into fix/nodes2-card-height 2026-10-06 13:38:54 -05:00
ausboss 4c8907a585 Seed and Workflow Switches: keep their own height in Nodes 2.0 too
Both are cards alone on their node. In Nodes 2.0 they stretched with a
node made taller, with empty space inside the card. They call
pinVuePanelHeight like the other cards now. Run Timer stays the one panel
that fills its node on purpose, and the guard names only it.
2026-10-06 13:37:48 -05:00
ausboss fc65f84f8f Merge pull request #137 from ausboss/feat/image-folder
Add Image Folder: pick a folder, tick its pictures, run the workflow on each
2026-10-06 13:30:50 -05:00
ausboss 36d6905477 Add Image Folder: pick a folder, tick its pictures, run the workflow on each
A loader for many pictures. The card chooses the folder inside ComfyUI's
input or output folder, and a thumbnail gallery under it ticks the pictures
to use. All in one run sends every picked picture through as a list; one
per run loads one picture each run and moves on when a run is queued. It
gives image, mask, filename, index and count. Pictures dropped on the node
or added with Add are uploaded into the input folder.

Folder names and picked names are attacker-controlled: a folder is only
read as a path below the input or output folder, links that lead out are
refused, and picked names are compared with what the folder holds, never
opened.
2026-10-06 13:28:51 -05:00
ausboss 7469156a30 Cards: keep their own height in Nodes 2.0, the picture takes the rest
In Nodes 2.0 a node with a card above a picture panel gave half of its
spare height to the card. The card got empty space under its last row and
the picture was drawn at about half the size it has in the classic
renderer, on a node at its smallest size too.

Nodes 2.0 lets a widget's row take spare height whenever the widget has a
computeLayoutSize, and every DOM widget inherits one. A card that pinned
only its computeSize was pinned for the classic renderer alone.
pinVuePanelHeight clears the inherited method, and the widget cards and
Save Image's card call it. The classic renderer is unchanged.
2026-10-06 13:20:52 -05:00
ausboss 1ec9359a92 Node text: say what the nodes do today
Tooltips, two labels and one console warning still described older
versions of the nodes. Wording only: no inputs, outputs or saved values
change.

- Latent Size: the gear, its menu and an error said "Resolution".
- Video Crop + Rotate + Pad -> Clip: tooltips said Snap and Limit where
  the node says Frames for and Length, and described dragging a handle
  to move the whole window. IN and OUT trim; Length sets a frame count.
- Run Timer: earlier runs are in the right-click menu, not under the
  number.
- Krea 2 Outpaint Model Patch: the rounding strip the warning names only
  comes from Divisible by on the Crop + Rotate + Pad nodes. Load Image +
  Pad never adds one.
- Save Image: its preview switch said off skips a temp file. It writes
  none, so it gets its own tooltip; the other preview nodes keep theirs.
- Workflow Note: the Banner layout shows the title, subtitle and author.
- LoRA Loader: a docstring said the pack only reads the Civitai file. It
  also writes it on Fetch Civitai info.
2026-10-06 13:15:16 -05:00
ausboss ac0bb08949 Help pages: say what the nodes do today
32 of the ? help pages checked against the code and corrected.

Wrong claims fixed: Stitch Inpaint's empty-area check (20/255), what a
new video keeps on both Video Crop + Rotate + Pad pages, Load Video's
frame picker and player height, where Run Timer lists earlier runs,
inpainting on the Krea 2 Outpaint Model Patch page, Workflow Note's
banner layout and pack detection, Image Resize's pad bars and mask,
Save Image's path preview and tags, Free Memory's steps, and Latent
Size's sockets.

Image Crop + Rotate + Pad now follows the node from top to bottom; the
saved values no row shows moved under Technical details.

Controls are named the way the node shows them, with the input name
beside. Pages that opened with jargon now open with what the node does
and when to use it. LoRA Loader gained the Civitai lookup, suggested
strength and the gear options it did not name.
2026-10-06 12:59:04 -05:00
ausboss 8f1f22be98 Workflow Note: Detect from this workflow also reads subgraphs
It read the main canvas only, so a pack used only inside a subgraph was not listed.
2026-10-06 12:57:32 -05:00
ausboss 8cc220df6c Image Resize: pad adds no bar when width or height is 0
A width+height box with a 0 in it takes its shape from the source, so only rounding and Multiple separate the two. Pad answered that with a bar 1 or 2 px wide: a 1000x700 picture with Multiple 16 came out 1008x704 with a 2 px bar. The picture now fills the box. The output size is unchanged, and a box with both sides set pads as before.
2026-10-06 12:57:32 -05:00
ausboss 5e90c059ef Load Video: leaving FRAME with the frame at or after OUT moves OUT to the end
Picking a frame writes IN. With the frame at or after OUT the node held a start after its end, and the next run stopped with "Load Video needs start_seconds smaller than end_seconds" until a handle was moved.
2026-10-06 12:57:32 -05:00
ausboss f94feaf2dc Merge pull request #132 from ausboss/release/2.7.0
Release 2.7.0: Mask by Name and Workflow Switches
2026-10-06 11:35:04 -05:00
ausboss 8946850038 Release 2.7.0
Mask by Name and Workflow Switches, with an example workflow that shows
both. The version pair, the changelog title, the description and the
keywords follow.
2026-10-06 11:34:02 -05:00
ausboss 8d9b25733c Merge pull request #131 from ausboss/feat/mask-by-name
Add Mask by Name: type what to find in a picture and get its mask
2026-10-06 11:20:10 -05:00
ausboss 47f124bd49 Mask by Name: it needs ComfyUI 0.20, not 0.38
ComfyUI has had the SAM 3 node this uses, with the same inputs, since
SAM 3 support landed. The help page also says which ComfyUI Free VRAM
needs.
2026-10-06 11:18:32 -05:00
ausboss 64df182242 Cards: show each row's tooltip on hover
The card read a row's hover text from the widget, where the frontend does
not keep it, so no row of any card said anything on hover. It now falls
back to the node's definition, which holds the tooltips the backend
declares.
2026-10-06 11:16:19 -05:00
ausboss 50ca945339 Mask by Name: say when to turn Free VRAM on
The tooltip, the help page and the README now tell people who build
workflows to turn Free VRAM on when a big model loads after this node.
2026-10-06 11:10:17 -05:00
ausboss 73f273c3dc Cards: opening More keeps the height the person gave the node
A card whose height changed set its node to its smallest size, so a node
made taller for its picture lost that height whenever More opened or
closed. The node now grows or shrinks by what the card did when the person
changed it by hand, stays as it is when the card changed by itself, and a
loading workflow takes its saved height. A node that sat at its floor
still hugs the new floor.
2026-10-06 10:54:38 -05:00
ausboss 9e7536cbfd Mask by Name: Fill holes and Free VRAM under More
Fill holes fills gaps that lie fully inside the mask, with the same fill
Mask Refine uses. Free VRAM takes SAM 3 and its text reader out of the
graphics card's memory right after the search, with ComfyUI's own unload
of one model, also when the search found nothing. Both are off by
default.
2026-10-06 10:28:20 -05:00
ausboss 895493c49c Mask by Name: the card, the FIND button and the picture on the node
Registers the node, gives it a widget card and the shared preview panel
with a FIND tool that runs the node alone, a readout of how many things
matched and how sure SAM 3 was, and the stop message on the node's own
face. A stopped run shows the plain picture, not the last find. Each name
asks SAM 3 for up to 16 matches, and Sureness starts at 0.7.
2026-10-06 09:59:28 -05:00
ausboss 0c8c74dc3d Add the logic and backend of Mask by Name 2026-10-06 09:41:41 -05:00
ausboss bad6539bdd Merge pull request #129 from ausboss/feat/workflow-switches
Add Workflow Switches, a card that turns parts of a workflow off and on
2026-10-06 09:37:54 -05:00
ausboss 9c347f0616 Merge main into feat/workflow-switches
# Conflicts:
#	CHANGELOG.md
2026-10-06 09:36:58 -05:00
ausboss 75e5b86c70 Merge pull request #130 from ausboss/fix/drop-file-on-panel
Load a file dropped anywhere on a loader node into the node
2026-10-06 09:36:46 -05:00
ausboss 032c0cd1b4 Load a file dropped on a loader's picture into the node, not a new Load Image node 2026-10-06 08:15:30 -05:00
ausboss 3007140cd6 Workflow Switches: a click anywhere on a pill flips the switch 2026-10-06 07:34:28 -05:00
ausboss a0d47535e3 Workflow Switches: describe the settings switch 2026-10-06 00:25:43 -05:00
ausboss c5a15d99c4 Workflow Switches: a switch can change settings instead of turning nodes off 2026-10-06 00:25:24 -05:00
ausboss 164233c0df Workflow Switches: help page, README, changelog, guards and API snapshot 2026-10-06 00:01:23 -05:00
ausboss 22bd33a093 Add Workflow Switches, a card that switches parts of a workflow off and on 2026-10-05 23:59:34 -05:00
ausboss 0692e0a26d Add the logic behind Workflow Switches 2026-10-05 23:49:19 -05:00
ausboss 376b56fa2d Merge pull request #128 from ausboss/docs/kofi-link
Add a Ko-fi link: Sponsor button and a README line
2026-10-05 22:14:42 -05:00
ausboss e91c4e4f18 Add a Ko-fi link: Sponsor button and a README line 2026-10-05 22:10:54 -05:00
ausboss d87c259b64 Merge pull request #127 from ausboss/release/2.6.4
Release 2.6.4: Integer steps by 1, a note above the boxes in the examples
2026-10-05 17:04:17 -05:00
ausboss c647d5302d Release 2.6.4: Integer steps by 1, a note above the boxes in the examples 2026-10-05 16:28:45 -05:00
ausboss d8b295d597 Merge pull request #126 from ausboss/fix/whole-number-step
Integer steps by 1: whole numbers on cards no longer step by 8
2026-10-05 16:06:13 -05:00
ausboss d5c95bed83 Whole numbers on cards step by 1, pixel sizes keep 8
The card took any whole number with a range over 2048 for a pixel size and
stepped it by 8. On the Integer node that made a Steps box go 25, 17, 9, 1:
8 was out of reach without typing. Select Every Nth, Split Batch, Select
Frame and Load Video's frame limit stepped by 8 too.

A whole number now moves by its own increment. A row steps by 8 only when it
is a pixel size (a px unit, or `pixels` on a row that shows no unit, as
Image Resize's W x H), and a row can name its own `step` (Load Image + Pad's
Multiple keeps 8).
2026-10-05 15:49:12 -05:00
ausboss 78a4b0b0d8 Merge pull request #125 from ausboss/examples/notes-above-boxes
Examples: a note above the boxes says what they hold
2026-10-05 11:27:50 -05:00
ausboss 9baf1c60ea Examples: a note above the boxes says what they hold
Each example keeps its inner steps in two boxes, and nothing on the canvas
said what was in them. A note now sits at the top of "Behind the scenes",
right above the boxes. It says what they do in one sentence, then
"Double-click one to see inside." The text is a heading, so it reads at
the zoom the examples open at.

The boxes and the Models group under them move down 158 px to make room.
In Krea 2 Inpaint Masked and Qwen Image 2.1 Edit + Realign the picture
nodes beside them are that much taller, so every group still ends level,
and the saved view is re-centred (Qwen opens at zoom 0.7, was 0.72) to keep
the whole top level in sight. In the watermark example the group grows
upward instead, where there was free canvas, so its boxes and wires stay
put. The Workflow Note card loses its "The two boxes in the middle do the
work" line and is that much shorter.

What each workflow sends to the server is unchanged apart from the
Workflow Note text. Checked in the classic view and in Nodes 2.0: no
overlaps, nothing outside its group, every note fits its text.
2026-10-05 10:02:00 -05:00
ausboss 4dd1446268 Merge pull request #124 from ausboss/release/2.6.3
Release 2.6.3
2026-10-05 10:00:18 -05:00
ausboss b455b20dd3 Release 2.6.3: Ctrl + Enter in popups, Use it for LoRAs, Stitch Inpaint with RGBA, node minimum width 2026-10-05 09:52:57 -05:00
ausboss 65635a1569 Merge pull request #123 from ausboss/fix/popups-note-stitch-width
Four fixes: popup shortcuts, Workflow Note and LoRAs, Stitch Inpaint with RGBA, node minimum width
2026-10-05 09:39:59 -05:00
ausboss 5a4674f5f6 Hold each node's minimum width
Every node stops at the width it was built for when it is dragged narrower:
the growing panels and the fixed cards. Load Video's IN and OUT boxes stay
whole in a narrow node.

The frontend adds 104 pixels to a growing panel's minimum width and leaves a
fixed-height card's minimum out of its width sum, so a card fell to a 210
pixel default. Panels pass exactMinWidth to fillNodeHeight or call
holdNodeMinWidth, and tests/panel_guards.test.mjs fails a new panel that does
neither.
2026-10-05 09:35:15 -05:00
ausboss 4bdd250abb Stitch Inpaint: drop the alpha of an RGBA inpainted image over an RGB source
Qwen Image 2.1's VAE decodes RGBA, so wiring its VAE Decode straight into
Stitch Inpaint failed with "Inpainted channels (4) do not match the cropped
image (3)". A 4-channel patch over a 3-channel stitcher now loses its alpha;
every other channel mismatch still raises.
2026-10-05 09:35:15 -05:00
ausboss aea32dea2d Workflow Note: Use it covers a LoRA, and writes paths the way the folder lists them
A LoRA from the note's list that sits in a subfolder only showed as
installed, while the model, text encoder and VAE lines offered Use it. The
LoRA line now shows the folder and offers Use it as well, which writes the
found path into the LoRA Loader row. LoRA Loader follows a row change made
by another node in Nodes 2.0 too.

Use it also wrote the found path with forward slashes. A Windows server
lists files in subfolders with backslashes, and a loader only takes a file
exactly as listed, so the loader kept an unknown value. Use it now writes
the folder's own spelling.
2026-10-05 09:35:15 -05:00
ausboss be8b071c55 Ctrl + Enter runs the workflow from the boxes in our popups
2.6.2 let app shortcuts through from the boxes on a node face. Six more
boxes still stopped every key: the LoRA picker's search box, the trigger
word and template name boxes, the gear menu's boxes, the picture and video
picker's filter, and the server file path box on the video nodes. They use
keepKeyInField now, and a search box no longer treats Ctrl + Enter as a pick.

A test reads every keydown handler under js/ and fails on one that stops a
key before looking at it. The Workflow Note editor and the keep or discard
question are the two that own the keyboard on purpose.
2026-10-05 09:35:15 -05:00
ausboss 636427c212 Merge pull request #122 from ausboss/release/2.6.2
Release 2.6.2
2026-10-04 19:08:49 -05:00
ausboss f5193afbf6 Merge pull request #121 from ausboss/fix/shortcuts-in-fields
Ctrl + Enter runs the workflow from our text and number boxes
2026-10-04 19:08:06 -05:00
ausboss 90594e3b82 Release 2.6.2: Ctrl + Enter from our boxes, Workflow Note model notes, reload notice 2026-10-04 19:07:55 -05:00
ausboss e57725fd2b Ctrl + Enter runs the workflow from our text and number boxes
Every field on a node face stopped all key presses, so ComfyUI's shortcuts did nothing while the cursor was in a Text node, a number box, the Seed box or a file name box. Ctrl + Enter only queued a run after a click outside the node.

The fields now keep the keys they type and edit with (so Delete or Ctrl + A never reach the canvas) and pass app shortcuts on to ComfyUI, as its own text boxes do. One helper, keepKeyInField in js/shared/canvas_passthrough.mjs, is used by the shared card's text, multiline and colour fields, the number boxes, the Seed box, Save Image's name fields, Load Video's IN and OUT boxes and LoRA Loader's strength boxes.
2026-10-04 19:01:02 -05:00
ausboss 2a0fa2b499 Merge pull request #120 from ausboss/fix/lora-loader-ui-108
Tell a tab left open through an update to reload
2026-10-04 17:08:10 -05:00
ausboss 8cd0e585b8 Merge remote-tracking branch 'origin/main' into fix/lora-loader-ui-108
# Conflicts:
#	CHANGELOG.md
2026-10-04 17:06:06 -05:00
ausboss 15b1de4730 Merge pull request #101 from ausboss/fix/note-model-row-notes
Workflow Note: keep each model's note when the editor saves
2026-10-04 17:05:48 -05:00
ausboss dce75240d2 Merge remote-tracking branch 'origin/main' into fix/note-model-row-notes
# Conflicts:
#	CHANGELOG.md
2026-10-04 15:43:55 -05:00
ausboss 14db93f076 Tell a tab left open through an update to reload
Updating the pack in Manager restarts ComfyUI and reloads the workflow, but
not the page. The tab kept running the old AusBoss scripts and nothing said
so: the update guard only looked once, when the page loaded.

The guard now looks again every time the server comes back, asks once per
installed version, and its message stays until it is closed instead of
going away after 30 seconds.

Follow-up to issue #108. The missing LoRA Loader panel reported there was
old browser copies of the shared modules, fixed in 2.5.2.
2026-10-04 02:26:31 -05:00
ausboss d6e31b5a85 Merge pull request #119 from ausboss/release/2.6.1-civitai-lookup
Release 2.6.1
2026-10-03 19:07:01 -05:00
ausboss 491bc9086f Release 2.6.1: Fetch Civitai info is back, Ctrl + Shift drag-zoom in Nodes 2.0 2026-10-03 19:06:24 -05:00
ausboss b1c0600913 Merge pull request #118 from ausboss/fix/zoom-drag-nodes-2
Ctrl + Shift drag-zoom over our nodes in Nodes 2.0
2026-10-03 16:58:05 -05:00
ausboss 1fb650bbb2 Let Ctrl + Shift drag-zoom start over our nodes in Nodes 2.0
In Nodes 2.0 a panel sits in the node's own widget area instead of a
.dom-widget, so the pass-through did not recognise it and the drag-zoom
never started there. Both layouts are matched now.
2026-10-03 15:51:33 -05:00
ausboss 03389229ec Merge pull request #106 from ausboss/feat/civitai-lookup-browser
LoRA Loader: bring back Fetch Civitai info as a click-only browser lookup
2026-10-03 15:25:37 -05:00
ausboss da60955801 LoRA Loader: bring back Fetch Civitai info as a click-only browser lookup
The pack's server still never contacts a host. The info card's button has the
browser ask civitai.com about one file by its SHA256, then the server saves
the answer beside the LoRA as <model>.civitai.info after checking it names the
same file (size, shape and hash). The old Python HTTP call stays gone.

release_preflight now allows a raw fetch() only in js/cache_guard and
js/lora_loader. SECURITY.md, AGENTS.md, the README and the LoRA Loader help
page describe the change.
2026-10-03 15:23:57 -05:00
ausboss 3013541750 Merge pull request #116 from ausboss/release/2.6.0
Release 2.6.0
2026-10-03 15:23:03 -05:00
ausboss d67de2df67 Merge remote-tracking branch 'origin/main' into release/2.6.0 2026-10-03 15:04:37 -05:00
ausboss 0ff560e776 Merge pull request #117 from ausboss/fix/zoom-over-panels
Let the graph zoom and pan while the pointer is over our nodes
2026-10-03 15:04:32 -05:00
ausboss 8d831d97fe Let the graph zoom and pan while the pointer is over our nodes
The wheel, Ctrl+Shift drag-zoom and middle-button pan never reached the
graph from over Compare, the crop previews, the video viewers or any control
on a card. One pack-wide listener hands them back, the way ComfyUI does for
its own text boxes. Panels that scroll keep the wheel until they reach an end.
2026-10-03 14:38:33 -05:00
ausboss 99252cfa72 Release 2.6.0: corner handles, Centre buttons, mask tint, Divisible by, Latent Size on Qwen 2.1 2026-10-03 12:26:47 -05:00
ausboss b160e8bcbc Latent Size: give Qwen Image 2.1 the size set on the node
Wired straight into a Qwen Image 2.1 sampler, the node's latent made a
picture twice as wide and tall as the size shown (1024 x 768 came out
2048 x 1536). The latent now carries how far it is downsampled, as
ComfyUI's own empty-latent nodes do, so ComfyUI fits it to a model that
packs pixels differently. Other models are unchanged.
2026-10-03 12:26:47 -05:00
ausboss a7a8f072a3 Merge pull request #112 from ausboss/feat/crop-mask-tint
Show the MaskEditor mask on Image Crop + Rotate + Pad's picture
2026-10-02 20:03:17 -05:00
ausboss 41bde6a3ff Show the MaskEditor mask on Image Crop + Rotate + Pad's picture
The MaskEditor saves its picture into the image widget's stored value
without calling the widget back, so the node kept showing the old picture
and nobody could tell a mask was there. The stage now follows a source
changed that way (on the node's draw, and when the MaskEditor sets the
node's preview, which covers Nodes 2.0 too), keeping the framing.

The mask is the picture's alpha. The stage reads it once per picture by the
run's own 90% rule and draws the picture as the run uses it: kept pixels
solid, painted parts empty over the fill, with Show blend's teal on top.
The teal turns, crops and pads with the picture, in the full editor too,
and Show blend's paste mask now includes the painted parts. A gear on the
image source box turns the teal off for this browser.
2026-10-02 20:02:24 -05:00
ausboss 0babf00e92 Merge pull request #113 from ausboss/feat/pad-corner-handles
Corner handles and Centre buttons for Crop + Rotate + Pad
2026-10-02 20:02:07 -05:00
ausboss 333e680f55 Merge pull request #114 from ausboss/fix/rotate-knob-past-180
Let the rotate knob turn past upside down
2026-10-02 20:01:45 -05:00
ausboss 7d019d15d7 Add corner handles and Centre buttons to Crop + Rotate + Pad
Four orange corner handles sit outside the canvas corners, on the same
ring as the side diamonds. Dragging one makes the canvas bigger or
smaller and keeps its shape: the padding on that corner's two sides
changes and the opposite corner holds still. Alt (Option on a Mac)
changes all four sides about the centre, and can be pressed or let go
mid-drag. A corner stops when one of its sides runs out of padding.

Two Centre buttons, on the node face next to Fill and Feather and in the
editor's Padding section, put the picture in the middle side to side or
top to bottom. They only move padding from one side to the other, so the
canvas keeps its size and a lit ratio stays lit; with Divisible by on,
its strip counts, so the visible bands come out even.

The rotate knob took the spot the top-right corner handle now needs, so
it moves to the nearest free gap on the handle ring when blocked, and it
keeps its place on its arm while it turns. In the editor, Alt-drag still
pans everywhere except on a corner handle.

Frontend only: the node inputs are unchanged.
2026-10-02 19:45:54 -05:00
ausboss 0257e71ab8 Let the rotate knob turn past upside down
A picture turned to 180 degrees could not be turned back with the knob:
dragging one way it stayed at -180, and the other way it snapped back to
-180 once the pointer passed straight left of the picture. Turning
clockwise through 180 also made it jump.

The turn was the pointer's angle minus its angle when the drag began.
atan2 flips from +180 to -180 straight left of the centre, so crossing
that line read as a whole turn back, and the clamp to -180..180 then
pinned the picture at the end of the range.

The knob is now followed move by move, each step the short way round
(knobStep), and the rotation wraps instead of clamping (wrapDegrees):
past 180 it carries on from -180. The Degrees box keeps its range.
2026-10-02 19:45:10 -05:00
ausboss 379484f292 Merge pull request #111 from ausboss/fix/round-canvas-steps
Divisible by: explain Round canvas to and step it 1, 8, 16
2026-10-02 19:42:51 -05:00
ausboss 8988c63f27 Divisible by: explain Round canvas to and step it 1, 8, 16
Round canvas to on Image Crop + Rotate + Pad and both Video Crop + Rotate
+ Pad nodes is now labelled Divisible by. Its tooltip says what it does
(a few pixels of fill on the right and bottom so the width and height
divide evenly by the number), when to use it (models that need sizes
divisible by 8, 16 or 32) and that 1 is off. The input is still
canvas_multiple, so saved workflows keep their value.

Its arrows and drags added 8 to whatever was there, so from 1 (off) they
went 9, 17, 25. makeScrubInput gets a snap option: whole steps land on
multiples of the step (1, 8, 16, 24 and back down to 1), while Shift's
fine step and typed values are left alone. The resize Step box on the
same nodes and Multiple on Load Image + Pad had the same problem and use
it too.

The editor's status panel now refreshes when Divisible by changes, as it
already did for every other size control.
2026-10-02 19:18:16 -05:00
ausboss 15c485477c Merge pull request #110 from ausboss/release/2.5.2
Release 2.5.2: fix stale frontend modules after updates
2026-10-02 11:19:19 -05:00
ausboss b2f9a4c7ee Release 2.5.2: refresh cached frontend modules after updates 2026-10-02 11:16:17 -05:00
ausboss cc803c2cdf Merge pull request #109 from ausboss/fix/stitch-portable-regression
Stitch Inpaint: portable pixel checks and console-only fill advice
2026-10-02 11:16:16 -05:00
ausboss b340efb745 Keep Stitch Inpaint unpainted-area advice in the console 2026-10-02 11:02:37 -05:00
ausboss 24b3e091c9 Include Mask Refine hole size in saved fallback expectations 2026-10-02 10:48:28 -05:00
ausboss f2e28ba630 Make Stitch Inpaint image regression checks portable across CPU kernels 2026-10-02 10:48:28 -05:00
ausboss ae2de0210e Merge pull request #107 from ausboss/fix/versioned-module-urls 2026-10-01 20:22:24 -05:00
ausboss 8617fdc983 Version the pack's script imports so an update never meets old browser copies
ComfyUI sends no cache header for .mjs, and the 2.5.1 no-cache header cannot
reach a copy the browser already stored. Firefox then kept running files from
before the update, even through Ctrl+Shift+R, and the preview bar, cards and
editors failed to load.

The server now adds ?v=<tag> to the relative .mjs imports of the pack's
scripts as it serves them. The tag changes whenever any script changes, so
after an update the browser asks for addresses it has never stored. Files on
disk are unchanged. A test fails if a script names a .mjs in a way the tag
would miss.
2026-10-01 17:41:54 -05:00
ausboss 650714b194 Merge pull request #105 from ausboss/release/2.5.1
Release 2.5.1
2026-10-01 12:07:34 -05:00
ausboss 07b9321f7d Release 2.5.1 2026-10-01 11:53:03 -05:00
ausboss 08414d7a85 Merge pull request #104 from ausboss/fix/stale-frontend-after-update
Fix half-loaded AusBoss after a Manager update
2026-10-01 09:44:00 -05:00
ausboss a1e6940deb Keep the browser from running old frontend files after an update
ComfyUI sends no cache header for .mjs, so after a Manager update a browser
could reuse old shared modules while loading the new entry scripts. The new
entries then imported functions the old modules lack and about a third of the
pack stopped loading.

The pack now serves its .mjs files with Cache-Control: no-cache, and a small
entry (js/cache_guard) refreshes the stored copies and asks for a reload when
a tab runs older files than the installed version. It replaces the old
stale-cache toast in the appearance entry.
2026-10-01 09:10:47 -05:00
ausboss b4a2d319eb Merge pull request #103 from ausboss/fix/manager-search-name
Registry listing: make Manager search find ComfyUI-AusBoss
2026-10-01 08:46:13 -05:00
ausboss e7c811927c Registry listing: name ComfyUI-AusBoss and AusBoss nodes so Manager search finds the pack 2026-10-01 08:44:47 -05:00
ausboss c96885be7d Merge pull request #102 from ausboss/release/2.5.0
Release 2.5.0
2026-09-30 01:52:57 -05:00
ausboss 161b305c18 Release 2.5.0 2026-09-30 01:50:35 -05:00
ausboss 4c4655aa41 Merge pull request #100 from ausboss/rc/2026-09-30
Fixes from hands-on testing: seams, Krea and LTX painting, clearer errors, crop node UX
2026-09-30 01:27:51 -05:00
ausboss 0cd8a31fd5 Crop + Rotate + Pad: keep the Resize off | on pill inside the card 2026-09-30 01:00:59 -05:00
ausboss 4a60f58ff0 Merge branch 'fix/see-through-paint' into rc/2026-09-30 2026-09-30 00:33:42 -05:00
ausboss 7aa25f6c71 Merge branch 'fix/stitch-polish' into rc/2026-09-30 2026-09-30 00:26:34 -05:00
ausboss 3bbe3d4911 Stitch Inpaint: catch a gray fill handed back darker, and fit the advice
The unpainted check counted a flat block only within 10/255 of the fill.
Krea 2 can hand a 128 gray fill back as a flat 114 to 118, so a picture
with gray corners got no word. Flat blocks now count within 20/255, and so
does the picture's own flat area that is taken off first, so a flat wall
continued flat still stays quiet.

The message told still pictures that dark footage paints better when
brightened first. That line is now only for videos, in plainer words.

The warning had no changelog entry yet; it has one now.
2026-09-30 00:19:46 -05:00
ausboss ba2bc3e2d1 Stitch Inpaint: Tone match under blend in runs as a few large steps
Tone match ran about 3,700 small torch operations per picture. PyTorch
spreads each one over every CPU it sees, and a cloud container can show it
far more CPUs than it may use, so each one waited its turn: on a RunPod
container it added 37 to 146 seconds to a 1.6 MP picture.

The blurs now run once over every layer they need, and the fit, the
held-out check and the curve inversion run over whole arrays: the band's
samples are summed per knot segment (sorted once, one reduction per pass
for all three fits), every curve is solved in one batched call, and the
curves are read and inverted over the whole picture at once. That leaves
about 18 multi-threaded operations. A single picture reuses the values it
found while measuring when the correction is applied.

The result matches the old code: same decisions, and at most 0.0002 of one
8-bit level apart on real runs (the fit's sums are only added in another
order). The old code is kept in the tests as a reference, and a new test
pins the two together on turned, straight, dark, thin-feather, clip and
alpha cases.
2026-09-30 00:04:49 -05:00
ausboss ff7b751544 Merge branch 'rc/2026-09-30' into fix/see-through-paint 2026-09-29 23:53:53 -05:00
ausboss b904fad791 Docs: see-through pictures and the prompt_image output
The README and the changelog say what Image Crop + Rotate + Pad now does
with see-through parts of a picture, and what prompt_image is for.
2026-09-29 23:53:53 -05:00
ausboss 7424203063 Examples: the tested fixes, laid out for this release
Krea 2 Inpaint Masked keeps a person you leave unpainted (Mask Refine's
Max hole size at 2). Klein 9B Outpaint blends its seam in, with Tone match.
Qwen Image 2.1 Edit + Realign fills see-through parts with gray and its
prompt writer keeps what the request doesn't name. The Krea 2 Outpaint and
Rotate + Outpaint notes add a tip for pulling far on one side, and the
examples that need this release say so. LTX 2.3 Video Outpaint makes room
for the taller clip node. Every file was re-saved by the frontend and
checked in the classic view and in Nodes 2.0.

The changelog no longer says Krea 2 Outpaint keeps its 3:4 ratio lit.
2026-09-29 23:29:54 -05:00
ausboss fc4caf961e Crop + Rotate + Pad: build the prompt view in whole numbers
The same pixels as before, from integer maths over the see-through part's
bounding box only: a 17-megapixel cutout no longer spends seconds on
floating-point work across the whole canvas.
2026-09-29 23:28:41 -05:00
ausboss 9fcc0450dc Krea 2 outpaint examples: the prompt writer reads prompt_image
Krea 2 Outpaint and Krea 2 Rotate + Outpaint describe the canvas before
painting it. They now read the pad node's prompt_image, where see-through
parts of a picture show as white, so a cutout is described on a real
backdrop instead of the gray fill. For a picture with no see-through
parts prompt_image is the canvas itself, so nothing else changes.
2026-09-29 23:18:32 -05:00
ausboss a416420bfa Crop + Rotate + Pad: paint see-through parts, add prompt_image
A see-through part of a picture - a cutout's missing background, an oval
photo's corners, a hole - kept its stored colour blended over the fill.
That colour is often black or the old background, so the model got a
dark or light ring round the picture and Stitch Inpaint pasted part of it
back. A pixel less than 90% as solid as the picture's most solid pixel is
now empty, like a corner a turn leaves: filled with the fill colour,
marked in the mask, new area for both seams. Kept pixels are fully solid
in their own colour.

A new last output, prompt_image, is the image with see-through parts on
white instead of the fill, for the node that writes the prompt: shown the
gray fill, a prompt writer describes a gray backdrop and the model keeps
it. original shows see-through parts on white instead of black.

A picture at least 90% solid everywhere goes through exactly as before;
the new tests hold every output and both seams to the old transform.
2026-09-29 23:18:19 -05:00
ausboss 36a0b5ca6a Merge branch 'fix/mask-refine-fill-limit' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 22:39:30 -05:00
ausboss 11b5a6d56a Mask Refine: Max hole size, so fill holes leaves the part you kept
Paint everything except a person or a pet, turn on fill holes, and it
filled the unpainted subject too, so the part meant to stay was repainted.
The new optional max_hole_size input (a percent of the picture, 0 to 100)
sets the biggest hole fill holes fills. The default, 0, fills every hole
exactly as before, so saved workflows and API prompts run the same; at 2
the gaps a quick brush leaves still fill and a subject left unpainted
stays. Areas touching the picture's edge are never holes, as before.

- Hole sizes come from scipy's labels, the same 4-connected holes its
  fill finds; the torch fallback labels the same 8-connected holes as its
  own fill. Each picture in a batch is measured on its own, after expand.
- The card gets a Max hole size row with a % unit, dimmed while Fill holes
  is off, and a resetUnknown fallback of 0 for 2.x saves that hold the
  card's empty value in its slot.
- Tests: a hole above the limit stays, holes up to it fill, 0 matches the
  old fill, edge-touching areas never fill, the torch fallback agrees,
  batches, and older saves opening with 0.
- Node API snapshot, help page and changelog.
2026-09-29 22:38:36 -05:00
ausboss e4501b2e0c Cards: a number widget holding an old save's empty value counts as unknown
holdsUnknownValue judged only switches and choices, so a number input
appended after a release could not be given a resetUnknown fallback: an
older save hands it the card's empty value, and it stayed there, which the
prompt then fails to convert. A number widget now holds only a finite
number. Only widgets named in a card's fallbacks are ever reset, so no
existing node changes.
2026-09-29 22:38:23 -05:00
ausboss ae2af9db47 Merge branch 'fix/clip-node-ux' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
#	example_workflows/LTX 2.3 Video Outpaint (AusBoss).json
#	js/input_preview/index.js
2026-09-29 22:16:33 -05:00
ausboss d6ca2ec6e5 Clip timeline: tidy the header comment and an unused variable 2026-09-29 22:07:50 -05:00
ausboss f7c6803f2b Crop + Rotate + Pad: the new notes no longer make the node taller
A node sized for the old face could grow into the node below once the new
notes showed. Now:
- The size line names the lit ratio where it acted ("pad to 16:9
  1821x1024") instead of adding a line, and "already 9:16: pick another
  ratio or turn it" sits on the same line when it fits.
- The Even out, To start and Full clip buttons fit their lines.
- "= the whole clip. Shorten it to move IN" fits beside the Length on the
  example workflow's node.
- A new picture only ever shrinks the picture area to its shape; it never
  grows the node.
2026-09-29 22:04:25 -05:00
ausboss 3044e16eb3 Merge branch 'fix/clear-errors' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 21:50:27 -05:00
ausboss 3283ceb1db Changelog: clearer errors for strangers' first runs 2026-09-29 21:44:06 -05:00
ausboss f909b6704e Stop a run that has no picture yet, and show a failed check once
Many shared workflows save their loaders blank so people load their own
picture. Pressing Run first started the run, and core Load Image failed
inside it with "[Errno 21] Is a directory". Now the run stops before it
is sent: one message, "Load a picture first:" and the node's name, and
the loader gets a red outline (on the classic canvas and in Nodes 2.0).

It covers core Load Image, Load Image (as Mask), Load Image (from
Outputs) and Load Video, and the pack's Load Image + Pad, Image Crop +
Rotate + Pad, Load Video and both Video Crop + Rotate + Pad nodes (the
file or the local path, whichever the mode reads). It looks at the
prompt the frontend is about to send and walks it the way ComfyUI does,
so bypassed and muted loaders, loaders fed by a link, loaders no output
needs, loaders behind a lazy input and outputs left out of "Queue
selected output nodes" never stop a run. Prompts sent straight to the
API are not checked.

The frontend has no extension hook that can stop a queue, so
app.queuePrompt is wrapped; it only reads, and builds the prompt a
second time only when a blank loader is on the canvas. Anything
unexpected lets the run through unchanged.

When ComfyUI does reject a prompt, a check that reads several inputs
comes back once per input. api.queuePrompt's answer is tidied so each
AusBoss node shows one entry per message, on the input the message is
about. Other packs' nodes are left as ComfyUI sends them.
2026-09-29 21:44:01 -05:00
ausboss 653cf2c8cc Merge branch 'fix/transform-first-pick-refit' into fix/clip-node-ux
# Conflicts:
#	CHANGELOG.md
#	js/shared/transform_editor.mjs
2026-09-29 21:43:45 -05:00
ausboss 8aaa3376b5 Test fillNodeHeight against its own contract
The test carried a copy of the frontend's space-sharing code, and ran
our helper only inside that copy. It now checks what the helper
promises in our own words: no computeSize left behind, a floor with no
ceiling, floors read on each call and made safe, options kept, and a
hide and show that leaves the panel growing again, using the pack's own
hideWidget and showWidget.
2026-09-29 21:43:40 -05:00
ausboss 341b1cef51 Check the node API snapshot in CI, without ComfyUI
The snapshot test needed a ComfyUI checkout, so CI always skipped it and
a renamed or reordered input passed every CI check. The same comparison
now also runs on an offline load of the pack: a standard-library Python
with torch, NumPy, Pillow, PyAV and SciPy replaced by stand-ins that
accept anything. Input and output names come from the node code itself;
with ComfyUI, a test checks that the offline load describes every node
exactly as the real load does. The ComfyUI load now runs in CPU mode, so
it never touches a GPU.

Also:
- The "became required" rule had no test of its own: its example also
  broke the input order, so deleting the rule failed nothing. It now has
  two cases only that rule can catch.
- --update refuses to write a snapshot that would break saved workflows
  or API prompts, unless --allow-breaking says the node never shipped.
2026-09-29 21:43:40 -05:00
ausboss 0f689e3bea Test that file reads stay inside ComfyUI's folders
Widget values reach the loaders through ComfyUI's /prompt route, and
resolve_input_path and local_preview_allowed are what keep those reads
inside ComfyUI's input, output and temp folders. No test covered them:
removing the outside-the-input-folder check, the network-path check or
local path mode's network check passed every test.

tests/test_media_paths.py covers climbing out with "..", absolute paths,
links pointing outside, "[output]" names, network paths refused before
the path is touched (even when a ComfyUI folder itself sits on the
share), and local path mode. The guards need neither torch nor PyAV, so
the media module's heavy imports are stubbed when missing and the file
runs in CI. Each guard's removal fails at least two tests.
2026-09-29 21:43:30 -05:00
ausboss 484ba61d13 Save Video: open the encoder before the first frame
The nvenc encoders are in the libav build with or without an NVIDIA card
and only fail when they open. That happened on the first frame, outside
the handler that explains it, so picking "mp4 h264 nvenc" without the
card ended in a bare "Unknown error occurred: avcodec_open2(...)". The
encoder now opens as the stream is set up, and the message leads with
what to do: the nvenc formats need an NVIDIA GPU, pick "mp4 h264". Other
encoders that fail to start name themselves without the NVIDIA advice.
2026-09-29 21:43:30 -05:00
ausboss 5a6317467a Say "No mask painted" when a picture reaches a mask node without one
Core Load Image hands out a 64x64 mask of zeros when the picture has no
mask painted on it, and picking a new picture after painting one gives
the same. Krea 2 Inpaint Masked then stopped inside Crop For Inpaint with
"Mask size (64, 64) does not match image size (1216, 832)".

Crop For Inpaint, Color Match and Mask Refine (with its picture wired)
now say "No mask painted: right-click your picture, choose Open in
MaskEditor, paint the area, then click Save." A mask made for another
picture names both sizes. LaMa Inpaint used to resize the blank stand-in
and hand the picture back unchanged; it says the same now. An empty mask
of the picture's own size still runs as before: Crop For Inpaint takes
the whole picture and stitches it back untouched.
2026-09-29 21:43:20 -05:00
ausboss cb05fcc9b7 Docs and changelog for the clearer canvas and preview controls
The help pages, the README and the changelog use the new names (Round
canvas to, Frames for, Server file, the Resize switch) and describe the
always-scrubbing timeline and its grip, the padlock rules, the "already
this shape" note, the resize warning and Even out, the preview switch
bringing its picture back, Compare's labels and the Workflow Note's Use it.
2026-09-29 21:43:07 -05:00
ausboss ac9cc58592 Report each failed input check once, and let ComfyUI check the rest
A VALIDATE_INPUTS that takes **kwargs makes ComfyUI skip its own range
and list checks for every input, and a failed check comes back once per
input: Image Crop + Rotate + Pad without a picture showed 20 copies of
"Select or upload a source file first". Each validator now names only
what it reads.

- Image Crop + Rotate + Pad, Load Video and LaMa Inpaint read one input
  and report one error, on it. Video Crop + Rotate + Pad (Frame and
  Clip) read the three that choose the source. LoRA Loader reads the
  stack and on_missing; Save Image reads its two names and output_dir.
- Save Video has no validator any more: ComfyUI's own check holds a
  typed fps to 0.01-240.
- Load Video and the Clip node check the trim window, force_rate and
  Fixed frames as they run, where a wired bound has its value. They are
  async, so ComfyUI starts them ahead of other nodes.
- The sources still skip ComfyUI's list check on purpose, so uploads,
  subfolders, MaskEditor saves and a stale video choice in local path
  mode keep working. The path checks behind them are unchanged.
- crop_aspect_ratio accepts any whole-number W:H, as the transform
  always has, so a workflow made with someone else's presets still runs;
  anything else is now refused before the run.

tests/test_validate_inputs.py pins what each validator names (runs in
CI) and, with ComfyUI, drives ComfyUI's own validation.
2026-09-29 21:43:01 -05:00
ausboss 33dae551df Merge branch 'examples-refresh' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
#	example_workflows/Krea 2 Outpaint (AusBoss).json
#	example_workflows/LTX 2.3 Video Outpaint (AusBoss).json
2026-09-29 21:42:25 -05:00
ausboss d27040e20b Merge branch 'fix/krea2-outpaint-paint' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 21:41:09 -05:00
ausboss f9bca9927a Merge branch 'fix/stitch-seam-turned' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 21:41:09 -05:00
ausboss 67c620589e Merge branch 'feat/workflow-note-fit' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 21:41:09 -05:00
ausboss b842b15df7 Merge branch 'fix/nodes2-undo-stale-faces' into rc/2026-09-30
# Conflicts:
#	CHANGELOG.md
2026-09-29 21:41:09 -05:00
ausboss a7e7795ac2 Merge branch 'fix/ltx-outpaint-sides' into rc/2026-09-30 2026-09-29 21:40:54 -05:00
ausboss 71b354f54b Merge branch 'fix/save-image-nodes2-height' into rc/2026-09-30 2026-09-29 21:40:53 -05:00
ausboss 5b2f2508ab Preview and padlock: two leftovers after the first pass
- A node dragged taller while its preview was off keeps that height when
  the preview comes back.
- A new picture with no ratio lit starts with the padlock off, as the
  untouched picture has no shape to hold.
2026-09-29 21:40:23 -05:00
ausboss 40571ce86b Workflow Note: point loaders at a model found in a subfolder
The note said "installed" for a model kept in a subfolder while the
workflow's loader still asked for the bare file name, which ComfyUI could
not find and marked red. Such a row now says where the file is ("in
LTXV 2.3/") and has a Use it button that points those loaders at the found
copy. The check stays on the ComfyUI server's own model lists.
2026-09-29 21:33:25 -05:00
ausboss 6e5a1e4783 Compare: A and B sit on either side of the split line
The A | B badge stayed in the middle of the picture while the split line
moved. Now A rides just left of the line and B just right of it, on the
picture each one names; with one picture showing, its label sits alone.
2026-09-29 21:33:25 -05:00
ausboss 69902c61f0 Number boxes: Shift slows the drag where there is no finer step
Shift is the fine step on every number box, but on whole-number boxes such
as Feather the fine step was the step, so Shift did nothing. There it now
slows the drag four times instead. Pressing or letting go of Shift during
a drag carries on from the current value instead of jumping.
2026-09-29 21:33:25 -05:00
ausboss fe7f301264 Crop + Rotate + Pad: clearer padlock, Fit, added space and editor
The padlock:
- Reset and tapping the lit ratio turn it off.
- It does nothing on the untouched picture (the row says Source), so a
  trim there only trims.
- The row says Held while it is on, and a locked drag splits the new
  bands evenly (each move is solved from where the drag started).
- Reset crop also removes the bands the lock added.
- The lit ratio and the lock share the pack's solid teal "on" look;
  hover is a light outline.

Shapes and sizes:
- A picture within about 1% of a ratio counts as that ratio: tapping 9:16
  on a nearly 9:16 photo adds no 1 px band.
- While a ratio is lit, the line under the picture says what it did,
  and "Already 9:16: nothing to add. Pick another ratio or turn it." when
  there was nothing to do (a saved ratio re-applies to every new picture).
- The resize warning reads "1.5% taller: each side rounds to 32 px"; its
  tooltip names a Step that avoids it, and in pad mode Even out adds a few
  pixels of padding so nothing stretches.
- Fit dims and says "pick a ratio first" until a ratio is lit.
- Over the picture, the cursor and small arrows show only the ways it can
  move; with nowhere to go it is a plain pointer.

Drawing:
- Added space is drawn in the real fill colour with a faint hatch, and
  only the picture the crop cuts away is darkened, so black bands on the
  black stage show.
- The rotate knob keeps clear of the padding diamonds and crop squares.
- On a big node the controls stay at most 460 px wide, centred, and only
  the picture grows. A new picture fits the picture area to its shape,
  never taller than the height you gave it.

Words: Align is "Round canvas to", Local path is "Server file", Resize is
an off | on switch, and the canvas row labels read like the others.

The full editor is built from the node's own controls: the ratio row with
its turn button and padlock, the Fit switch, and number boxes for the
rotation (Shift for 0.1 degree) in place of the coarse slider. Ratios
from ausboss_presets.json that no button shows sit in a More list. The
size box keeps clear of the handles, and the editor's button style no
longer reaches the number boxes.
2026-09-29 21:33:25 -05:00
ausboss 8ab3ce334a Preview switch: on again brings back the last result at the old height
Switching a preview off and on again (Mask Refine, LaMa Inpaint, Select
Frame, Save Image) left an empty box until the next run, and the node came
back shorter than it was.

Now the node remembers its height when the preview goes off (saved with
the workflow) and returns to it, with the last result in the box. If the
node ran while its preview was off, the box says "Run the workflow again
to see it here" rather than showing an older picture. With nothing to show
yet it says "Run the workflow to see it here".

Mask Refine's AUTO button now says what it does in its tooltip.
2026-09-29 21:33:06 -05:00
ausboss 1c812492d0 Clip timeline: the bar always scrubs, and a grip slides the clip
With Length on, pressing between IN and OUT was a hidden "move the whole
clip" grip. With a Length as long as the clip it had nowhere to go, so the
bar and IN seemed frozen and nothing said why.

Now a press or drag anywhere on the bar moves the playhead, Length on or
off. The kept part carries a small grip that slides the clip (IN still
slides it while Length is on); a free window slides with its IN and OUT
together. When the Length already covers the whole clip, the Length row
says so: "= the whole clip. Shorten it to move IN."

Also:
- Snap reads "Frames for: LTX (8n+1) / Wan (4n+1) / any"; the stored
  values are unchanged.
- "To start" and "Full clip" look like buttons.
- Turning Length off no longer moves the playhead, as the help page says.
2026-09-29 21:33:06 -05:00
ausboss 2df49c6f26 Krea 2 outpaint examples: wire the mask into Krea 2 Encode, feather 32
Both Krea 2 Outpaint and Krea 2 Rotate + Outpaint now pass the pad mask to
Krea 2 Encode, so AnyPaint sees the new area in the picture's own colour
and paints it instead of copying the gray padding. They also feather the
join 32 px instead of 12, the width of the band AnyPaint was made to
redraw: a thin strip of new area had come back as a flat band with a hard
line at 100%, and with 32 it continues the picture.
2026-09-29 18:01:00 -05:00
ausboss a9663166d7 LTX outpaint example: paint the new area on any clip
At strength 1 the outpaint IC-LoRA left the new area black on some clips,
most often when the prompt described a close, framed shot. The LoRA now
runs at 1.5, and the prompt instructions ask for a wide shot of the whole
place with what fills every side, so they work for side and top/bottom
padding alike.
2026-09-29 17:51:01 -05:00
ausboss faf0462067 Stitch Inpaint: say when an outpaint comes back unpainted
An outpaint whose new area still shows the plain fill color used to come
back as a finished-looking picture with bars. Stitch Inpaint now measures
how much of the new area is still flat at the fill color, beyond what the
source picture itself has, and when a quarter of it (or half of any
checked frame) is left, says so in the console and in a toast.

The check lives in its own helper, works for images and video and for any
fill color, and stays quiet on dark scenes that are painted.
2026-09-29 17:51:01 -05:00
ausboss d14b717090 Stitch Inpaint: Tone match works with Seam set to blend in
Blend in kept the model's colour as painted, so a model that paints
lighter, darker or warmer left the new area off-tone. Classic's Tone
match is no answer on turned pictures: it reads the grey rim a turn
leaves on the picture's edge as drift, and its flat offset lifts the
black of a dark new area.

With Tone match on, blend in now compares the model's version of the
strip it redrew with the picture at the same pixels (tone layers over
the same clean picture pixels, clipped highlights left out), fits the
drift as curves over brightness with a pinned and a free part, checks
the fit on held-out tiles, adds a local gain along the edge only when
that holds out too, and takes the result back off the model's picture
before the blend. Blacks stay black; nothing changes when the check
fails, the model kept the colours, or there is no feather to read. A
clip is measured once.

Classic is unchanged (its pins), and blend in at Tone match 0 is pinned
to the 2.4.0 stitch. The card no longer dims Tone match under blend in.
2026-09-29 16:56:12 -05:00
ausboss 9abead5a78 Krea 2 Encode: show AnyPaint the new area in the picture's own colour
A dark or colourful picture padded with the #808080 gray came back from
Krea 2 AnyPaint with the gray still there: a gray frame around the picture,
or gray corners on a turned one. AnyPaint was trained with the new area of
its reference filled with the median colour of the picture, so a gray patch
next to a dark photo reads to it as part of the scene and it paints it back.
It did the same with every Krea 2 model file tried.

Krea 2 Encode gets an optional mask input, appended last. When it is wired,
the new area of the reference (mask above 0.5) is refilled with the median
colour of the rest before it reaches the VAE and the vision tower. The
canvas the rest of the graph uses keeps its gray. Without the link nothing
changes.
2026-09-29 16:50:26 -05:00
ausboss 58b450d21e Crop + Rotate + Pad: the editor's Multiple and Steps are Align and Step
The node face calls canvas_multiple Align and resolution_steps Step; the
full-screen editor called the same two settings Multiple and Steps.
2026-09-29 16:41:58 -05:00
ausboss ee8501db5e Save Video and Compare: show the stored result when a tab opens again
Both panels filled only from onExecuted. Going to another workflow tab and
back rebuilds the nodes and restores the tab's results in ComfyUI's store
without an onExecuted, so they said "Run to preview" over a result they
had just made, in both renderers. On onNodeOutputsUpdated (a tab's results
restored) an empty panel loads its node's stored record. The output key
helper moves to shared/prompt_scope.mjs for the three users.
2026-09-29 16:41:32 -05:00
ausboss 8ed782de89 Crop + Rotate + Pad: one state per ratio button, a padlock, a real turn
Prototype of the ratio row, for review. A chip used to stay lit after a
drag changed the shape (16:9 lit on a 1889x1280 canvas), a second tap on
it locked and a third cleared every pad, the portrait/landscape button
threw away hand edits, a locked diamond dragged inward did not move, and
Crop | Pad looked like two more ratio chips.

- A chip is lit only while the canvas (crop plus padding, before Align)
  has its ratio; the pick (properties.ausboss_fit_aspect) is dropped when
  a gesture leaves it, so a new clip is only fitted to a shape the canvas
  still has. Without a picture a tapped ratio waits for the first one.
- Tap = reach the ratio (pad, or crop with Fit on crop), tap the lit one =
  back to the whole picture. No other chip state.
- Holding the shape is a padlock button (ausboss_aspect_lock): drags keep
  the lit ratio, or the custom one, from the moment you grab a handle.
- The orientation button turns the shape: pad mode pads the current crop
  to the turned ratio, crop mode turns the crop box about its centre.
  Nothing to turn (no picture, untouched, square): only the labels turn.
- Custom / Source caption, and the real ratio on the size line.
- Fit is a segmented pill; switching it re-reaches a lit ratio.
- Rotation keeps a lit ratio (and a held custom one).
- Dragging the picture where the crop spans the source slides it inside
  its padding (slidePadding).
- The editor's Ratio list applies at once; Hold shape is the padlock.

Saved properties and widget values keep their names and meaning; an old
locked chip opens with the padlock on. Pure rules in transform_geometry
(canvasSize, aspectMatches, turnAspect, turnedCrop, padAround,
slidePadding, ratioLabel, isUntouched) with node:test coverage.
2026-09-29 16:22:27 -05:00
ausboss f1fedbf345 Crop + Rotate + Pad: keep the picture on the node big enough to see
The stage could be squeezed to about 130 px on all three nodes: the
declared floors counted the rows around it short, and the Clip node's rows
alone take 450 px. A padded portrait clip ended up a sliver in a 16:9
canvas.

- The stage's floor follows its width (stageHeightForWidth: 0.6 x width,
  200 to 340 px). The panel's minimum is the measured rows around the
  stage plus that floor, so a corner drag stops there; before the panel
  is on screen a per-kind estimate stands in. A new node opens at it.
- keepStageRoom grows the node to the same floor when a row appears later
  (Resize, a wrapping readout), and sets it as the stage's CSS minimum so
  Nodes 2.0, which sizes nodes by content, holds it too.
- exactMinWidth and holdVueNodeMinWidth make 330 the real minimum width in
  both renderers (the classic corner drag jumped to 434; Nodes 2.0 went
  down to 225 and crushed the chip row).
- The source card's "Choose an uploaded ..." line hides once a file is
  chosen, which is the room older layouts need for the new floor.
- The LTX 2.3 example moves the two nodes under the Clip node down for its
  new height; the contract and a live load find no overlaps.
2026-09-29 16:05:38 -05:00
ausboss 5babcc7ed6 Keep a run's results on the workflow tab that queued it
Every workflow tab in a page shares one server connection, and ComfyUI
files a node's results by node id. A run from one tab that finished while
another tab was open landed on the open tab's node with the same id. On
Video Crop + Rotate + Pad -> Clip that was a second video under the
buttons that grew the node over the one below; Show Text and Seed also
saved the other workflow's text and seed into this one, and Save Video and
Compare replaced their own result.

- The three Crop + Rotate + Pad nodes stand down ComfyUI's image and video
  previews (they send no picture of their own), in both renderers.
- suppressCoreVideoPreview sets hideOutputImages, so Nodes 2.0 no longer
  draws Save Video's clip a second time under its player.
- shared/prompt_scope.mjs notes which graph queued each run (the queue
  call is wrapped for the prompt id). For a result from another tab, the
  AusBoss node here gets this tab's own stored result back, every pack
  onExecuted handler returns early, and the result is handed to the owner
  tab's AusBoss nodes when that tab is open again. Core nodes are left
  alone.

Tests: prompt_scope (scope rules plus a policy check that every
onExecuted handler asks isForeignRun first) and core_preview (every
transform kind suppresses both previews; video suppression sets
hideOutputImages) fail on main and pass here.
2026-09-29 15:53:03 -05:00
ausboss b71cc0271c Workflow Note: keep each model's note when the editor saves
The editor's model table had columns for file, folder, size and URL but
none for the row's note, and the form read each row back without it. Save
(from the form or the JSON view) wrote every model with an empty note, so
the grey line under each file name was gone after the first edit. On a
note with model notes, Cancel and Escape also asked to discard changes
when nothing had changed, because the form never matched the saved card.

The table now builds its header and cells from MODEL_COLUMNS, which has
one column for every field a model row keeps, and reads rows back with
rowFromCells. That adds a Note column. tests/workflow_note.test.mjs checks
that the columns cover every kept field and that an unchanged table saves
back the same card.

On frontend 1.53.6 (classic canvas and Nodes 2.0), the LTX 2.3 Video
Outpaint example keeps its five model notes through a how-to edit and
Save; Cancel with no edits closes without asking; a note edited in the
new column and a model added with a note both show and survive a save and
reload. The table fits the dialog without scrolling sideways from 700 to
1800 px wide.
2026-09-29 15:04:39 -05:00
ausboss 1a25b8312f Examples: line up the group bottoms again
Groups that shared a bottom edge before the Nodes 2.0 sizing share one again: the shorter ones grow to the tallest. In the Krea 2 Outpaint and Krea 2 Rotate + Outpaint boxes, the two prompt groups that were 2 px apart now share a bottom too. Only group heights change; values, links, titles and the API prompt are unchanged.
2026-09-29 11:32:08 -05:00
ausboss 3d498a0260 Examples: size groups for Nodes 2.0 and give the notes room
Nodes 2.0 draws some nodes taller than classic, so every group now holds its nodes with 20 px in whichever renderer draws them bigger, and what sits below a grown group moves down with it. Show Text moves below Generate Text in Qwen Image 2.1 Edit + Realign, and the Watermark Remover's Reroutes get room above the nodes they feed, so nothing overlaps in either renderer. Each Workflow Note gets 12 px of spare height in classic. Values, links, titles and the API prompt are unchanged.
2026-09-29 10:58:16 -05:00
ausboss 2bc842bd41 Workflow Note: grow to fit its text in the classic canvas
A note saved a few pixels shorter than its content showed a scrollbar and
hid its last row. The LTX 2.3 Video Outpaint note in the smaller example
layout (440 x 810) was 6 px short, so its links sat behind the scrollbar.
The classic canvas keeps a node's saved height; Nodes 2.0 already sized
the note to its content.

The note now measures its card's natural height at the node's width and
grows the node to fit when a workflow opens, after an edit, after the file
check, and when the node's width changes, since the text rewraps. It never
shrinks the node. The load fit runs before ComfyUI snapshots the opened
workflow, so a note that grows on load does not mark the workflow modified.
A corner drag stops at the fit, as it does in Nodes 2.0: holdNodeMinHeight
lifts the node's computeSize height to the measured floor. A card less
than half a pixel short is left alone, because Chrome rounds that away and
draws no scrollbar. Nodes 2.0 is untouched.
2026-09-29 10:50:57 -05:00
ausboss a7c85b42d4 Merge pull request #97 from ausboss/fix/panels-out-of-saves 2026-09-29 08:39:05 -05:00
ausboss 5626350f60 Nodes 2.0: give each node its own face back after undo
Undo and redo rebuild every node in place, with the same ids. Nodes 2.0
usually keeps each node's face, and the slot that holds a DOM panel only
mounts an element when it is first made. So after Ctrl+Z every AusBoss
node kept showing the removed node's panel, the rebuilt node's panel
never reached the screen, and changes made on that face never reached
the queued prompt.

When an AusBoss node is removed, the panels it has on screen are
remembered by graph, node id and widget name. When a node with that id
is configured, each of its off-screen panels takes the place of the
remembered one if that one still sits on this node's face. Classic mode,
new and pasted nodes, and faces the frontend mounted itself are left
alone.
2026-09-29 06:55:45 -05:00
ausboss ead9518c5f LTX 2.3 Video Outpaint example: make the note tall enough for its links
The note was 6 px short, so its link buttons sat behind a scrollbar.
2026-09-29 05:56:49 -05:00
ausboss b62bda9ba2 Crop + Rotate + Pad: the first file after a blank load is a new picture
A transform node is created on the first file in the input folder. When
a workflow saved with an empty loader configured it blank, the node kept
remembering that first file, because a blank source never replaced the
remembered one. Picking the first file then looked like no change: the
workflow's saved rotation, crop and padding stayed and a lit ratio was
not refit. Any other file reset as it should.

The node's first sync and a workflow load now remember what the node
holds, blank included (rememberedSource in transform_geometry.mjs). Only
a blank the user makes, such as Local path before a path is typed, keeps
the last source, so going back to the same file is still no change.
Covers the image node and both video nodes.
2026-09-29 05:15:06 -05:00
ausboss 7b8aa698e6 Preview nodes keep their size in Nodes 2.0
In Nodes 2.0 a node's saved height is only a minimum and its content can
make it taller. The preview panel's picture was in normal flow, so a
portrait result drawn at full width grew Save Image from 730 to 1276 px in
the Krea 2 Outpaint example, over the Run Timer under it. Nodes 2.0 also
drew its own copy of the result under the panel, because the panel never
set hideOutputImages.

- The stage's picture is out of flow and centred, so the stage gets its
  size from the node and the picture scales to fit, in both renderers.
- suppressCoreImagePreview sets hideOutputImages; classic never reads it.
- In Nodes 2.0 that copy was the only thing filling node.imgs, so the
  panel falls back to the node's stored output (app.nodeOutputs) and
  re-reads it when a workflow tab restores its outputs. node.imgs still
  wins where it exists, so classic keeps its old path.

Select Frame, Mask Refine and LaMa Inpaint share the panel and grew the
same way. Core Save Image and Preview Image keep their size in Nodes 2.0.
Classic is unchanged.

Tests: the stage-picture guard in panel_guards, hideOutputImages in
core_preview and the stored-output lookup in input_preview fail on main
and pass here.
2026-09-29 05:11:19 -05:00
ausboss 7e318a26b9 Watermark Remover example: use ComfyUI's own SAM3
The example finds the mark with ComfyUI's built-in SAM 3.1 instead of
ComfyUI-RMBG, so it needs no other node pack and no extra download on the
first run. One set of settings feeds both the one-frame check and the whole
clip, and the layout matches the other examples. The README table and the
CHANGELOG describe the refreshed examples.
2026-09-29 04:42:56 -05:00
ausboss b00b34ab03 LTX 2.3 Video Outpaint example: smaller layout
The same outpaint on 13 top-level nodes instead of 23, with the prompt
writing and the painting in two boxes. Both LoRAs load in one LoRA Loader,
the prompt instructions sit in their own Text node, and the video node saves
no padding with its 16:9 ratio chip lit, so a new clip is padded to fit.
Same models and output.
2026-09-29 04:41:07 -05:00
ausboss def82c08e0 Klein 9B Outpaint example: padding follows the picture, no second face
The example now pads with Image Crop + Rotate + Pad's ratio buttons, so the
padding is worked out for whatever picture is loaded instead of the
sample's pixels. The auto description describes only the setting: when it
described the person in a tight portrait, Klein painted a second face under
the join. Smaller layout with two boxes; needs AusBoss nodes 2.4.0. The
thumbnail shows the new result.
2026-09-29 04:40:39 -05:00
ausboss e61c9f23fc Qwen Image 2.1 Edit + Realign example: smaller layout
The same edit on 14 top-level nodes instead of 24: the prompt writer and the
edit sit in two boxes. Node titles use the real node names, the model
loaders are color-coded and the note is down to three steps. The graph and
its output are unchanged.
2026-09-29 04:17:58 -05:00
ausboss fd619d1675 Krea 2 outpaint examples: open with no turn and no padding
Both examples open with an empty image loader but still saved the sample's
turn and padding. Picking the first file in the input folder kept them, so
that picture got the sample's 12 degree turn or its 1045 px bands. They now
save no turn, no crop and no padding; Krea 2 Outpaint keeps its 3:4 ratio
chip lit, so a new picture is padded to 3:4.
2026-09-29 04:06:33 -05:00
ausboss 65bd29f26e Krea 2 Inpaint Masked example: turn Stay in picture on
The example saved Stay in picture off, so a big painted area was cropped past
the picture edge and painted at a smaller scale. It now saves it on, saves the
Stitch Inpaint seam as classic, and says it needs AusBoss nodes 2.4.0. The
thumbnail shows the new result.
2026-09-29 04:04:47 -05:00
ausboss 8d03e59d2b Open 1.x saves with the preview and Stop on missing LoRA on
Workflows saved by the 1.x releases also end in a panel's empty value: the
preview panel on LaMa Inpaint and Mask Refine (Select Frame from 1.2.0) and
LoRA Loader's rows. The inputs added in 2.0.0 sit in those slots, so preview
opened as "" (the switch showed on, but the prompt ran without a preview)
and on_missing opened as "" (the menu showed Stop on missing LoRA off while
the node still stopped the run). Each now opens on its default, which is
also what an API prompt without the input runs with: the three cards reset
preview through resetUnknown, and LoRA Loader, which has no card, applies a
RESET_UNKNOWN when a workflow opens. On a real 1.x save (pack 1.3.0 saved by
frontend 1.47.12) only those four values change.

The guard's history now reaches back to 1.0.0. Mask Refine's preview was
inserted ahead of an existing socket rather than appended, so the table now
lists each node's last input as of this guard and APPENDED_SINCE declares
everything added since the node first saved an empty value. The fallback
check also reads RESET_UNKNOWN, and tests/test_card_fallbacks.py becomes
tests/test_saved_fallbacks.py, which checks both kinds.
2026-09-29 03:20:45 -05:00
ausboss 2a3268f934 Cards and panels: leave them out of saved workflows
Every card and DOM panel was added with options.serialize = false, which
only keeps a widget out of the queued prompt. The frontend saves each
widget whose own serialize is not false and hands saved values back by
position, so each card and panel saved an empty value of its own after the
node's real values, and an input appended to the node later opened holding
it (Stitch Inpaint's seam, Crop For Inpaint's keep_inside). Every DOM widget
now also sets widget.serialize = false, as the frontend's own previews do,
so new saves carry only real values.

Older saves still end in those empty values, and loading them now skips
the cards and panels. On every node that saves by position they come after
all real values (Save Image's card sits mid-list, but Save Image saves by
name), so nothing moves. On frontend 1.53.6 the examples (the 21 from
before the trim and the 7 now) and 14 older saved workflows queue
byte-identical prompts before and after, keep every value, and save and
reopen cleanly.

An input appended later still needs a fallback for those older saves.
tests/saved_widget_values.test.mjs checks that every DOM widget sets the
flag, that the examples keep their values with the panels skipped, and that
each input appended to a node whose older saves end in an empty value is
declared and, as a widget input, has a card resetUnknown fallback that
fires. tests/test_card_fallbacks.py (ComfyUI's Python) checks each fallback
is the input's own default. AGENTS.md states the rule.
2026-09-29 03:20:45 -05:00
ausboss 8b6e0036b7 Merge pull request #96 from ausboss/examples-and-readme
Trim the example workflows and rewrite the README
2026-09-29 02:53:40 -05:00
ausboss 83bf4bac1d Merge branch 'main' of https://github.com/ausboss/ComfyUI-AusBoss into examples-and-readme
# Conflicts:
#	CHANGELOG.md
2026-09-29 02:53:00 -05:00
ausboss 69470bc04d Watermark Remover example: save the note's named copy under its real name 2026-09-29 02:39:01 -05:00
ausboss 2bbe5014b4 Save the example media loaders empty
Every image and video loader in the seven examples is saved with no file
picked, in both saved widget copies. Remove the three sample pictures the
new examples added, put the Registry archive budget back to 12 MB, and
update the README examples section and the CHANGELOG.
2026-09-29 02:35:58 -05:00
ausboss 2ecd4acba9 Merge pull request #95 from ausboss/fix/crop-stay-in-picture-old-saves
Crop For Inpaint: open older saves with Stay in picture on
2026-09-29 02:31:56 -05:00
ausboss dc8593f690 Merge pull request #94 from ausboss/docs/blank-media-loaders
Example workflows: media loaders are saved blank
2026-09-29 02:31:44 -05:00
ausboss ca7ef10223 README: use the posted workflow covers 2026-09-29 02:25:06 -05:00
ausboss 97d9194ad5 Krea 2 Outpaint example: give its subgraphs their own ids 2026-09-29 02:07:03 -05:00
ausboss 577d69c1bf Crop For Inpaint: open older saves with Stay in picture on
A workflow saved before 2.4.0 holds the widget card's own empty value in
the slot keep_inside took when it was appended. The frontend restores
widget values by position, so keep_inside loaded as "", the card showed it
off and the queued prompt sent "", which ComfyUI reads as False: the old
padded crop. An API prompt saved before 2.4.0 has no keep_inside and runs
with the node's default, True. The same workflow gave two different crops.

The card now puts True, the node's default, back on a keep_inside that is
neither true nor false before it draws, the way it already resets an
unknown Seam on Stitch Inpaint. The resetUnknown check moves into
widget_card_math.mjs (holdsUnknownValue, resetUnknownValues) and learns
switches: the old check only knew a choice's options, so it would have
reset every saved switch. A saved true or false is left alone.

The backend is unchanged. ComfyUI converts a BOOLEAN "" to False before
VALIDATE_INPUTS or the node sees it, so an API prompt that carries "" (an
API export made on 2.4.0) still runs with it off.
2026-09-29 02:06:24 -05:00
ausboss f34169a132 Update the Krea 2 Outpaint example
Replace it with the version that pads with Image Crop + Rotate + Pad,
with its sample picture and a new thumbnail. Point the README's Load
Image + Pad section at Klein 9B Outpaint, and update the example table,
the outpaint caption, the Krea 2 Outpaint Model Patch help page and the
CHANGELOG. Raise the Registry archive budget to 18 MB.
2026-09-29 02:06:08 -05:00
ausboss aebf63ccc3 Example workflows: media loaders are saved blank
A file name in Load Image, Load Image + Pad, Load Video or Image Crop +
Rotate + Pad is one the stranger doesn't have. ComfyUI flags the node red
when the workflow loads, with a toast and a Setup required entry, until
they pick a picture. Saving the loaders blank loads clean.

Adds the rule to the example workflow section, and says "model loader" in
the bullet about official file names so the two don't overlap. The
existing examples are not changed here.
2026-09-29 02:03:32 -05:00
ausboss 33ed838bb6 Rewrite the README 2026-09-29 01:59:24 -05:00
ausboss 3669a37171 Merge pull request #93 from ausboss/fix/card-notify-widget-changed
Cards: tell the node when a card control changes a widget
2026-09-29 01:57:25 -05:00
ausboss 3a315e2924 Trim the example workflows
Keep Krea 2 Outpaint, Klein 9B Outpaint, LTX 2.3 Video Outpaint,
Qwen Image 2.1 Edit + Realign and Simple Video Watermark Remover, and add
Krea 2 Inpaint Masked and Krea 2 Rotate + Outpaint with their sample
pictures. Remove the other sixteen examples and the sample picture only
they used.

Update the README example table and links, the Realign to Source help
page, a test comment, the CHANGELOG and the pyproject description, and
raise the Registry archive budget to 13 MB.
2026-09-29 01:52:07 -05:00
ausboss 0e3f8ec0e0 Cards: tell the node when a card control changes a widget
The card wrote a widget's value and ran its callback, but never called
node.onWidgetChanged, which is where ComfyUI's frontend re-scans a node
for missing media. Picking an existing file in Load Image + Pad's Source
list left the red outline, the "Media input missing" toast and the Setup
required entry on, still naming the old file.

Every card control now writes through commitWidgetValue, which sets the
value, runs the callback and then calls onWidgetChanged, in the order
LiteGraph's own widgets use.

Adds tests for it, plus a changelog entry.
2026-09-29 01:40:13 -05:00
ausboss 85b1e122cc Merge pull request #92 from ausboss/release/2.4.0
Release 2.4.0
2026-09-29 00:32:57 -05:00
ausboss 43a15370d9 Release 2.4.0 2026-09-29 00:31:47 -05:00
ausboss 7ced07431a Merge pull request #90 from ausboss/fix/stitch-seam-blend
Stitch Inpaint: add a Seam option for outpaint canvases
2026-09-29 00:31:09 -05:00
ausboss 20afbf2536 Stitch Inpaint: move Seam into a gear menu
The card keeps its old look and height. Seam has no row and no socket
now: a small gear in the card's top-right corner opens a menu with the
two choices, each with one plain line. With blend in on, Tone match and
Fix edge halo stay in place but dim and stop taking input, with a hover
hint, and a "blend in" chip beside the gear shows the mode.

The seam widget is a socketless combo, so the frontend makes no input
slot for it in either renderer; a stale unlinked slot from an earlier
save is dropped on load. The backend input is unchanged.

The widget card gains two generic options for this: rows can be muted
by the current values, and a card can carry small corner tools that
every row's control keeps clear of. A gear-menu choice can list one
line per option.
2026-09-29 00:19:01 -05:00
ausboss 89e82f6d70 Stitch Inpaint: say when blend in helps
Blend in suits turned pictures and outpaints with Tone match off. With straight padding and Tone match on, classic usually looks as good, because the model's slight tone drift stays as painted in blend in. The help page also limits the unchanged round-trip promise to the classic seam.
2026-09-29 00:19:01 -05:00
ausboss 65784651d1 Stitch Inpaint: add a Seam option for outpaint canvases
Seam is a new optional choice, appended after color_match. "classic"
is the default and the old stitch, bit for bit. "blend in" hands the
model's picture over to the source by depth into the picture: colour
and tone change over slowly, fine detail changes over in a short ramp
where the two line up, the new area keeps the model's pixels, and the
deep picture keeps its own. There is no global tone shift, so
color_match and fix_edge_halo do not apply and the card hides them.

The depth map comes from the generated-area mask of a Crop + Rotate
+ Pad stitcher, or from the source rectangle of a Load Image + Pad
stitcher, with sides flush with the frame left out. It is computed
once per stitcher and shared by every frame, with scipy when present
and an exact torch fallback otherwise. Crop For Inpaint stitchers
keep the classic stitch. With blend in, blend_mask is the hand-over
weight.

An older workflow can carry the card's empty value in the slot Seam
now takes; the card resets it to classic on load and the node accepts
it as classic. Classic output is pinned against the previous code for
Load Image + Pad, Crop + Rotate + Pad, Crop For Inpaint and clip
stitchers.
2026-09-29 00:19:01 -05:00
ausboss 3071f7c877 Merge pull request #91 from ausboss/fix/load-video-wired-bounds
Load Video: accept a wired start or end time
2026-09-29 00:10:40 -05:00
ausboss 697d29905d Load Video: accept a wired start or end time
A wired input reaches VALIDATE_INPUTS as None, and the window check called
float() on it, so wiring anything into start_seconds or end_seconds made the
whole prompt fail validation with a TypeError. Skip the window check while
either bound is wired; the file check and the check for typed values stay,
and the loader checks the window again when it runs.

Adds a test for a wired start, a wired end, both wired, and for the checks
that still apply, plus a changelog entry.
2026-09-29 00:01:50 -05:00
ausboss e4421774b1 Merge pull request #89 from ausboss/example-lora-file-names
Use plain file names in three example workflows
2026-09-28 22:32:59 -05:00
ausboss b12011ce75 Merge pull request #88 from ausboss/update-contributor-docs
Update contributor documentation
2026-09-28 22:32:55 -05:00
ausboss 97c0778b6f Use plain file names in three example workflows 2026-09-28 22:02:19 -05:00
ausboss 964e82ad7a Update contributor documentation 2026-09-28 22:02:19 -05:00
ausboss ea562b778b Merge pull request #86 from ausboss/feat/resolution-width-lora-names
Latent Size (was Resolution Master): resizable; LoRA Loader long-name choice
2026-09-28 19:40:20 -05:00
ausboss 59a12aee6c Merge remote-tracking branch 'origin/main' into feat/resolution-width-lora-names
# Conflicts:
#	CHANGELOG.md
2026-09-28 19:39:31 -05:00
ausboss ece03a3dc4 Merge pull request #85 from ausboss/fix/crop-inside-picture
Crop For Inpaint: keep the crop inside the picture (on by default)
2026-09-28 19:34:42 -05:00
ausboss daee65c719 Merge remote-tracking branch 'origin/main' into fix/crop-inside-picture
# Conflicts:
#	CHANGELOG.md
2026-09-28 19:01:47 -05:00
ausboss 6c8ff82f5c Merge pull request #84 from ausboss/fix/krea2-inpaint-claims 2026-09-28 18:57:17 -05:00
ausboss 45b27dd94a Latent Size (was Resolution Master): resizable; LoRA Loader long-name choice
Resolution Master is renamed Latent Size: the old name belongs to another
node pack. The node id AUSBOSS_NODES_Resolution is unchanged, so saved
workflows and API graphs load and run as before. Workflows only store a
title that differs from the default, so they pick up the new name; a saved
title that starts with "Resolution Master 🆎" gets the new name in its
place on load. The example, its thumbnail and the README image follow.

- Latent Size can be resized: wider stretches the chips, fields and size
  picks, and taller gives the stage the room. It opens at 340 x 531 as
  before and goes down to 320, where the size row tightens so W, H and MP
  stay whole.
- LoRA Loader: gear -> Long names, end (the default, as before) or middle,
  which keeps the checkpoint number at the end of a name in view. The
  picker opens wide enough for long names and shortens the folder first.
- The frontend adds 104 px to a DOM panel's minWidth before using it as
  the node's floor (LGraphNode.computeSize), so a node that opened
  narrower jumped wider the moment its corner was touched: the LoRA Loader
  could not go under 424. fillNodeHeight gains exactMinWidth, which
  measures that padding and takes it back off, and holdVueNodeMinWidth
  lends the floor to Nodes 2.0, whose corner drag reads the node's inline
  min-width instead. Both nodes use them; the other panels keep their old
  floors for now.
- README: the workflow-tools image shows Latent Size alone; the Seed card
  is gone.
2026-09-28 18:34:03 -05:00
ausboss b8c9dff562 Crop For Inpaint: keep the crop inside the picture (on by default)
When the grown context box was bigger than the picture on an axis (a wide
or tall mask, or a small picture), fit_rect centred it and the overflow was
replicate-padded: the sampler got stretched copies of the edge pixels, and
a megapixel target spent its budget on them. On a 1184x2112 portrait with
the coat painted, only 42% of the canvas was the picture and the new part
was drawn at 0.52x.

New optional input keep_inside (card row "Stay in picture", on by
default) clamps the box to the picture on the overflowing axis before it
is fitted, so the crop holds only real pixels (0.80x on that portrait).
The helper keeps the old padding by default for existing callers; off on
the node is the old crop. Appended as the last optional input, so saved
workflows keep their values; ones whose box ran past the picture now
render a little differently.
2026-09-28 18:07:50 -05:00
ausboss 750f1e4860 Krea 2 Outpaint docs: drop the inpaint claim, fix Crop For Inpaint
The example's Workflow Note and the Krea 2 Outpaint Model Patch help page
said you could paint over part of the picture with the mask editor and pad
at the same time. Load Image + Pad drops the picture's alpha, its mask
covers only the new padding, and it hides the core preview the mask editor
opens from, so that never worked. Both now say the workflow only extends
the picture outward.

The help page also said a Crop For Inpaint stitcher places the reference
over the full frame. On source rectangle it falls back to where the crop
sat in the full picture, so the page now says to use whole canvas with one.
2026-09-28 15:20:15 -05:00
ausboss 83a6b7448f Merge pull request #81 from ausboss/release/2.3.0
Release 2.3.0
2026-09-27 21:55:45 -05:00
ausboss 2a29a3330a Release 2.3.0: describe Realign to Source in plain words 2026-09-27 21:51:46 -05:00
ausboss baa7ab7794 Merge remote-tracking branch 'origin/main' into release/2.3.0 2026-09-27 21:50:31 -05:00
ausboss 81cb12eaa5 Merge pull request #83 from ausboss/fix/run-timer-glow-slowdown
Run Timer: paint the glow once instead of blurring the canvas every frame
2026-09-27 21:50:05 -05:00
ausboss cd03e8ed5a Merge pull request #82 from ausboss/feat/realign-margin
Realign to Source: margins, whole-pixel cuts and big zooms; Load Image + Pad mirror fill
2026-09-27 21:47:51 -05:00
ausboss 588271ac42 Run Timer: paint the glow once instead of blurring the canvas every frame
Once the readout held a time, drawReadout ran a canvas filter blur on the
graph canvas three times a frame (LED halo, digits, tenths). Chrome sends
every filtered draw through an offscreen layer the size of the whole
canvas, so the entire graph redrew about 5x slower: 60 -> ~17 fps while
panning a test graph (issue #77).

The blurred passes now go into a small cached layer (cachedLayer in
js/shared/run_timer.mjs) that is repainted only when the digits, LED,
size or zoom bucket change; each frame copies it with drawImage and draws
the crisp LED and digits directly. The readout is pixel-identical within
one colour step.
2026-09-27 21:45:36 -05:00
ausboss 62fcc88262 Edit + Realign example writes its own prompt; plain docs; LoRA Loader in every example
- Qwen Image 2.1 Edit + Realign: type a short edit and Qwen3-VL 8B, the
  text encoder the edit already loads, writes the full instruction. The
  consistency LoRA sits in a LoRA Loader row, switched off.
- LTX 2.3 Video Outpaint: the distilled LoRA and the outpaint IC-LoRA load
  in one LoRA Loader instead of two core loaders. The workflow checks now
  refuse core LoRA loaders in examples.
- Realign to Source: description, tooltips and help page rewritten in
  plain words, with the technical detail at the end of the help page.
- Settings menus: in a short window the rows no longer squeeze together
  and overlap; the menu scrolls instead.
- AGENTS.md: text people read starts with the plainest version.
2026-09-27 21:00:26 -05:00
ausboss ff59d55f41 Realign to Source: margins, whole-pixel cuts and big zooms
An edit made at the picture's own size loses whatever the model pushed
past the canvas edge, so realigning it left an empty strip on 13 of 16
held-out edits. Realign to Source now takes an optional stitcher: given
Load Image + Pad's stitcher for the padded canvas the edit was made on,
it measures inside the picture's area only and returns just that area,
so what the model drew into the margin fills the edges.

- A drift within half a pixel of a whole-pixel shift is cut straight
  out of the edit with no resampling; an edit that already lines up
  comes back untouched.
- The coarse pass also tries the edit at 0.86x-1.28x, so a large zoom
  is found; models often reframe a padded canvas by zooming in to fill
  it, and that zoom does not count toward Max zoom.
- The report says whether the frame was cut out or warped and names the
  margin each side needed when an edit ran out of picture.

Load Image + Pad gains a mirror fill, which models read as more of the
scene: over 16 edits it provoked a reframe on 2, against 7 for edge
pixels and 5 for flat gray. The Qwen Image 2.1 Edit + Realign example
now edits on a 64 px mirror margin with the stitcher wired.
2026-09-27 19:48:55 -05:00
ausboss bb4b00d172 Release 2.3.0: Realign to Source, a wired image into Load Image + Pad, LoRA links 2026-09-27 18:11:12 -05:00
ausboss 9d803d28f4 Merge pull request #80 from ausboss/fix/lora-symlinked-files
LoRA Loader: accept a LoRA file that is a link
2026-09-27 18:10:02 -05:00
ausboss c8383da808 LoRA Loader: accept a LoRA file that is a link
resolve_lora_path followed links before checking that the file sits in
models/loras, so a LoRA kept on another drive and linked into the folder
was refused as escaping it and then reported missing, which stops the
run when the node is set to stop on a missing LoRA. Containment is now
checked on the path as written, with '..' folded but links not followed,
the way ComfyUI's own loaders treat linked files.
2026-09-27 18:09:29 -05:00
ausboss 4f1073edd7 Merge pull request #78 from ausboss/feat/qwen-edit-consistency-lora
Qwen Image 2.1 Edit example: load the consistency LoRA
2026-09-27 18:06:46 -05:00
ausboss e1b52a43a0 Merge main into feat/qwen-edit-consistency-lora 2026-09-27 18:06:18 -05:00
ausboss c2e3ac5ba5 Merge pull request #79 from ausboss/feat/load-image-pad-input
Load Image + Pad: pad a wired image
2026-09-27 18:06:11 -05:00
ausboss 9e33f62ba6 Merge main into feat/load-image-pad-input 2026-09-27 18:04:30 -05:00
ausboss 65d810d1ce Qwen Image 2.1 Edit example: link the consistency LoRA in the note
The Workflow Note's consistency LoRA paragraph now links the LoRA's
Hugging Face and Civitai pages where it is first mentioned, not only in
the links list at the bottom.
2026-09-27 17:59:24 -05:00
ausboss 187b84a40f Qwen Image 2.1 Edit example: load the consistency LoRA
A LoRA Loader between the diffusion model and the reference cache loads
qwen-image-2.1-consistency.safetensors (step 1500) with the step 2000
file as an alternative row, and stops the run if the file is missing.
Qwen Image 2.1 restyles otherwise often come back about 4-5% taller and
shifted: over 36 held-out restyles the worst corner moved 24.3 px
(median) without the LoRA and 1.6 px with it.

The Workflow Note lists both files with their download links, the
README requirement row names the LoRA, and the example picture is a new
render of the updated graph.
2026-09-27 17:27:05 -05:00
ausboss 72bd394f1d Merge pull request #76 from ausboss/feat/realign-to-source 2026-09-27 13:59:37 -05:00
ausboss fdbc674d8a Add Realign to Source (experimental): undo an edit's zoom and shift
Qwen Image 2.1 draws broad edits (watercolor, anime, oil) a few percent
taller than the source, up to about 12%, differently for every seed, and
snapping sizes does not change it. Realign to Source measures that zoom
and shift against the source and warps the edit back onto the source's
frame, at the source's size. empty_mask marks the strip the model pushed
out of view; report gives the measured zoom, shift and worst corner, or
why a frame was left as it is. The display name carries EXPERIMENTAL.

Promoted from the lab node. The measurement is unchanged (the same
matrices on 166 real Qwen 2.1 renders). The warp now samples the edit
with its edge pixels extended for every fill, so the gray and source
fills no longer draw a dark line beside the strip.

New example: Qwen Image 2.1 Edit + Realign, the Qwen 2.1 Edit graph with
the realign step after decode, its report, and before and after compares.
2026-09-27 13:53:59 -05:00
ausboss 8e3745d1c7 Load Image + Pad: tell a wired source from the None ComfyUI passes
Validation now names source_image instead of asking for input_types.
ComfyUI hands a linked input to VALIDATE_INPUTS as None and leaves an
unwired optional one out, so a sentinel default tells them apart. That
keeps ComfyUI's own link-type check (input_types switched it off), drops
the copy of it, and a missing file is one error again instead of one per
named argument. No **kwargs, so ComfyUI's list and range checks run for
every other input.
2026-09-27 02:56:48 -05:00
ausboss f32917321e Load Image + Pad: pad a wired image
An optional source_image socket pads an image from another node instead
of the chosen file, so a step before the pad (flattening a transparent
picture onto white, say) no longer has to be saved and loaded back.

- The wire wins over the file; validation skips the file check while it
  is connected and runs core's link-type check itself, since asking for
  input_types turns core's off for the node.
- A wired run hands the canvas a small temp preview (one file per node,
  overwritten each run) and the true source size, so the stage draws the
  last image it padded; before the first run it shows an outline.
- Card rows gain linkedBy, so the Source row dims while a wire replaces it.
- Appended and optional with no widget: saved workflows and API prompts
  load unchanged. Node API snapshot refreshed.
2026-09-27 01:45:51 -05:00
ausboss 746181e190 Merge pull request #75 from ausboss/fix/transform-stitch-bundle
Transform + stitch fixes: mask-only feather, rotated colour match, no Load Image + Pad strip, Crop + Rotate + Pad polish
2026-09-27 00:47:15 -05:00
ausboss 4b68408933 Merge pull request #74 from ausboss/docs/proof-packages
Add the proof package standard for fixes and model releases
2026-09-27 00:44:51 -05:00
ausboss fad53a6df4 Merge pull request #73 from ausboss/feat/editor-cancel-discard
Editors: Cancel button with a discard-changes prompt
2026-09-27 00:44:06 -05:00
ausboss c94862635a Proof packages: keep local servers and paths out of PRs
The PR is public, so its Proof section carries only what anyone can check
from the repo: the claim, the tests and the numbers. Server URLs, workflow
folders and scratch paths go to proof.md and the report to ausboss. The
machine's dev-server rules move to the creator playbook.
2026-09-26 23:24:54 -05:00
ausboss 77ba516318 Point agents to the creator playbook skill 2026-09-26 22:28:21 -05:00
ausboss 33cdf80a60 Merge fix/pad-leftover-sliver: Load Image + Pad adds no strip along an unpadded edge
Resolved: CHANGELOG keeps every bullet, with this bundle's transform and
stitch bullets grouped after main's.
2026-09-26 22:26:38 -05:00
ausboss ed548503f7 Merge fix/rotated-seam-color-match: tone match reads only picture pixels on rotated canvases
Resolved: CHANGELOG keeps both bullets. The feather branch's
perfect-continuation test left rotated-and-padded out because of this
bug; with both fixes in, it covers that layout too.
2026-09-26 22:26:13 -05:00
ausboss c4a0e011e6 Merge fix/crop-rotate-pad: rotation keeps the crop, size readout, Clip Length (B2 dropped)
Resolved:
- node_image_crop_rotate_pad.py: optional inputs are the four resize
  inputs, then stitch_blend, then stitch_grow; node_api.json regenerated
  with test_node_api.py --update (no breaking change against main).
- test_transform_outputs.py: the optional-inputs test expects the stitch
  inputs after the resize ones.
- transform_editor.mjs: the Inpaint & Stitch section and its status line
  serve the image node too (feather branch) beside the size-chain status
  (this branch); the Cancel code is #73's, line for line.
- LTX example note and the Clip help page carry both branches' wording.
- CHANGELOG keeps every bullet.
2026-09-26 22:25:48 -05:00
ausboss d3e2c4bdd5 Merge feat/editor-cancel-discard (#73): the editor Cancel change fix/crop-rotate-pad was built on
# Conflicts:
#	js/shared/transform_editor.mjs
2026-09-26 22:22:59 -05:00
ausboss 996403d511 Merge fix/transform-mask-only-feather: feather softens the mask only 2026-09-26 22:22:41 -05:00
ausboss 415ca971ad Load Image + Pad: no strip along an edge nobody padded
Rounding the canvas to Multiple put the leftover on the right and bottom
whatever was padded, so the published Krea 2 Outpaint workflow (top and
bottom padded, Multiple 16, Budget 1.6) grew a 6 px grey strip down the
photo's right edge. Krea painted it flat, the feather opened the photo
beside it, and Stitch's colour match read it as a seam.

The leftover now joins a padded side (the far one when both are). An
axis nobody padded is scaled onto the multiple when a Budget is set (one
uniform scale) and trimmed evenly without one; the node's canvas shows
the trim as -N px. The JS preview (pad_canvas.mjs / pad_panel.mjs)
mirrors the Python plan to the pixel. Crop + Rotate + Pad keeps its old
rule for now.
2026-09-26 22:22:14 -05:00
ausboss 0025994528 Stitch tone match: read only picture pixels on rotated canvases
On a Crop + Rotate + Pad canvas that was rotated and padded (a canvas
multiple's few pixels count), the source rectangle is the crop, which
holds the rotation's empty corners. color_match read their fill as
picture and shifted the whole fill toward it: up to 33 dE on a flat
test, 57 with a resize. The transform stitcher now carries its
generated-area mask; the seam reading skips void pixels (2 px clear of
them), and a line with no picture takes the band's global offset.

Load Image + Pad and Crop For Inpaint carry no mask and stitch exactly
as before; a pin test holds Load Image + Pad's colour-matched bands to
their old values.
2026-09-26 22:22:14 -05:00
ausboss 95feae209f Drop the unfaded stitch canvas (B2): feather is mask-only now
fix/transform-mask-only-feather stops the transform fading the picture,
so the canvas the stitcher holds is already the unfaded one. Remove
transform_pil_layers, keep_unfaded and resize_transform_to_megapixels,
their three tests and the CHANGELOG bullet; the image node keeps its
optional resize inputs and the integer aspect maths stays.
2026-09-26 22:22:05 -05:00
ausboss a3a9be54bf Crop + Rotate + Pad: rotation keeps the crop, size readout, Clip Length
- Rotating keeps the crop's size over the same part of the picture from
  the knob, slider, number box and Reset rotation (cropForRotation).
- A readout under the picture names every step that sets the output size
  (crop -> pad -> align -> resize) and warns when the resize Step
  stretches the picture or Align pads fill ahead of a resize; ticking
  Resize shows Megapixels and Step on the face.
- Locked-aspect crops use integer maths (_ratio_box / ratioBox), so the
  size shown equals the run.
- Image Crop + Rotate + Pad's four resize inputs are optional, so API
  prompts saved before them validate again.
- Video Crop + Rotate + Pad -> Clip: one Length switch replaces the Fixed
  frames and Limit boxes; OUT sits on the last output frame; a connected
  fixed_frames / frame_load_cap takes over the length.
- The stitcher pastes the unfaded canvas when feather > 0
  (transform_pil_layers / keep_unfaded). Superseded by mask-only feather
  and dropped in the next commit.

Carries a copy of the editor Cancel / discard-prompt change from
feat/editor-cancel-discard (#73), which this work was built on.
2026-09-26 22:21:16 -05:00
ausboss 1ef1b31918 Crop + Rotate + Pad: feather softens the mask only; image stitch inputs
The image and video transform nodes faded the picture into the fill
colour across the feather ramp. Outpaint models read that ramp as
content (the Qwen 2.1 outpaint LoRA painted a darker band along a tilted
edge, LTX's IC-LoRA left feathered bars unpainted), and the stitcher
kept the faded pixels, so Stitch's colour match measured its drift
against them and pulled the fill toward grey (d(ab) 18.5 at feather 24 on
a straight pad, 2.6 now). Feather now shapes only the mask and the stitch
blend built from it; the picture keeps a hard edge against the fill, so a
feathered canvas is the feather-0 canvas pixel for pixel.

Image Crop + Rotate + Pad gains the clip node's optional stitch_blend
(32) and stitch_grow (0) inputs, and its editor shows the same Inpaint &
Stitch section with Show blend. The editor's live fade overlay goes with
the fade.
2026-09-26 22:21:09 -05:00
ausboss 32a4b7314c Add the proof package standard for fixes and model releases
Every fix PR and model release now ships a failing-then-passing test,
before/after evidence, and a dev server with ready-to-run PROOF workflows,
so the owner can see a change work before merging or publishing.
2026-09-26 22:15:29 -05:00
ausboss 2d8273ebb6 Merge remote-tracking branch 'origin/main' into feat/editor-cancel-discard
# Conflicts:
#	CHANGELOG.md
2026-09-26 22:14:12 -05:00
ausboss 70b3135df3 Merge pull request #72 from ausboss/feat/save-image-preview
Save Image: preview switch and Embed workflow: no | yes
2026-09-26 22:08:37 -05:00
ausboss 8ddf38a197 Merge pull request #71 from ausboss/fix/compare-hover-polish
Improve A/B Compare snapping and add explicit preview locks
2026-09-26 22:08:21 -05:00
ausboss 235225000c Editors: Cancel with a discard-changes prompt
The Crop + Rotate + Pad editor gains Cancel beside Save & close; Escape
now cancels. Changed edits ask first (shared js/shared/discard_prompt.mjs)
and Discard restores the node to its state when the editor opened. The
Workflow Note editor asks before Cancel, Escape or a backdrop click drops
edits, and a JSON edit carried back to the form no longer outlives Cancel.
2026-09-26 17:59:48 -05:00
ausboss 2a5df316cb Save Image: preview switch and Embed workflow no | yes
Save Image joins the shared input_preview panel with the PREVIEW switch,
backed by a new optional preview input (display only: the file is saved
and listed in the queue either way). The embed row reads
Embed workflow: no | yes.
2026-09-26 17:58:25 -05:00
ausboss f616661eff Show A on the left and B on the right in Compare 2026-09-26 17:58:11 -05:00
ausboss 8314bde808 Center Compare identity divider with equal-width labels 2026-09-26 17:54:02 -05:00
ausboss 9e086969bf Improve Compare slider snapping and explicit A/B preview locks 2026-09-26 17:51:07 -05:00
ausboss 77477c1646 Merge pull request #69 from ausboss/feat/source-previews
Preview the file under the pointer in every Source list
2026-09-25 14:27:01 -05:00
ausboss 679fbcdb8f Merge main into feat/source-previews 2026-09-25 14:16:06 -05:00
ausboss 7e697d5f3d Merge pull request #68 from ausboss/fix/seed-roll-on-switch
Roll the seed when switching to Random
2026-09-25 14:05:59 -05:00
ausboss f63b6aaaaa Merge main into fix/seed-roll-on-switch 2026-09-25 14:04:14 -05:00
ausboss 5ca4d818d7 Merge pull request #70 from ausboss/fix/qwen3vl-caption-thinking
Keep Text Generate's thinking on for the Qwen3-VL captions
2026-09-25 14:01:29 -05:00
ausboss 963b4dfe48 Merge pull request #67 from ausboss/docs/readme-creators
Point readers to the Civitai workflows and pitch the creator nodes
2026-09-25 14:01:13 -05:00
ausboss e94d50a9a7 Keep Text Generate's thinking on for the Qwen3-VL captions
Krea 2's text encoder is Qwen3-VL-4B-Instruct, which has no thinking
mode. With thinking off, ComfyUI appends an empty think block and the
model often ends its turn at once, so Krea 2 Prompt from Image could
return no text. The five examples that caption with Qwen3-VL now keep it
on; the Krea notes explain it instead of blaming greedy sampling.
2026-09-25 09:06:32 -05:00
ausboss 153eb3a25d Preview the file under the pointer in every Source list
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.
2026-09-25 09:02:20 -05:00
ausboss 48e225766e Roll the seed when switching to Random
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.
2026-09-25 08:48:13 -05:00
ausboss b1330993f9 Point readers to the Civitai workflows and pitch the creator nodes
Adds a line before Install linking the Civitai workflows and X with a star
ask, a short For workflow creators section (Workflow Note, Image Compare,
Seed, Run Timer, Save Image) and its row in the Find a node table.
2026-09-25 08:44:53 -05:00
ausboss 081a14217b Merge pull request #66 from ausboss/docs/manager-missing-nodes-tip
Show what to click when Manager can't find AusBoss
2026-09-25 00:18:25 -05:00
ausboss 5428a149c2 Show what to click when Manager can't find AusBoss 2026-09-24 23:23:31 -05:00
ausboss 9f96e7b673 Merge pull request #65 from ausboss/chore/share-ready-polish
Release 2.2.0: share-ready pass, no network code, new examples
2026-09-24 15:52:51 -05:00
ausboss de3112d547 Release 2.2.0: no network code, consistent nodes and new examples 2026-09-24 15:51:32 -05:00
ausboss f995ab6332 Hold Krea 2 Outpaint's canvas at 1.6 MP
With the budget at 0 a large photo kept its full size, and on a 4 MP
canvas Krea 2 painted the new sky a shade darker, creased at the corners
and cut invented limbs at the old edge. A 16-run matrix on two sky
photos (VLM on/off, feather 64/96, full size vs 1.6 MP) and a budget
sweep put the cause on canvas size alone: 1.6 and 2 MP were clean, 2.5 MP
began to band and 1 MP went blotchy. The example now uses the 1.6 MP
budget the Klein outpaint already had; the note and thumbnail follow.
2026-09-24 15:50:23 -05:00
ausboss 0932478408 Name the node tour and watermark remover like the other examples 2026-09-24 15:38:05 -05:00
ausboss 0532757b10 Keep Frame's resize inputs optional and pin the node API
Video Crop + Rotate + Pad -> Frame had gained its four resize inputs as
required, so an API prompt saved before them stopped validating. They
are optional now; the widget order is unchanged.

tests/test_node_api.py loads the pack the way ComfyUI does and holds every
node to tests/fixtures/node_api.json: inputs and outputs may only be
appended, never renamed, reordered, removed or newly required. Its rule
tests need only the standard library and run in CI.
2026-09-24 15:11:22 -05:00
ausboss 555ddcd586 List the three new examples in the README and changelog 2026-09-24 15:02:22 -05:00
ausboss 3d63196866 Add a Dataset Frames from a Clip example
Load Video, Select Every Nth, Image Resize and Align Image turn the pier
clip into nine 768 x 1344 frames, and Save Image writes each with a
caption sidecar and size-and-counter names. No models.
2026-09-24 14:59:39 -05:00
ausboss 43a799c26b Add a LaMa Object Removal example
The rowboat PNG's painted mask goes through Mask Refine, Crop For Inpaint,
LaMa Inpaint and Stitch Inpaint, with Compare against the source: the
first LaMa example that needs no other pack, only big-lama.pt.
2026-09-24 14:59:39 -05:00
ausboss 3d295b71ac Add a model-free Video Reframe and Slow Motion example
The pier clip is cropped to a square on the Clip node, Frame Interpolate
doubles its frames with blend, and Save Video plays them at the source
rate for 2x slow motion; Select Frame and Compare check an in-between.
No models, and it runs in seconds on a CPU.
2026-09-24 14:59:39 -05:00
ausboss 04a6243efc Say what Align Image's crop and pad and Color Match's first-frame mode really do 2026-09-24 00:49:14 -05:00
ausboss 28753d3d61 Describe the shared transform face and Load Image + Pad's Reset padding
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.
2026-09-24 00:48:13 -05:00
ausboss 63eb70b940 Document Select Every Nth's fps pair and Show Text's copy button 2026-09-24 00:46:14 -05:00
ausboss 358ea00b4b Name controls in the help pages the way the node faces show them
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).
2026-09-24 00:46:14 -05:00
ausboss a5070a793b Describe the Mask Refine fold and the transform resets as they now work 2026-09-24 00:46:14 -05:00
ausboss 3522c293ee Say what Color Match's first-frame mode can and cannot fix 2026-09-24 00:46:14 -05:00
ausboss 32b18d01f4 Name the Krea 2 patch output model, as the node does 2026-09-24 00:46:14 -05:00
ausboss 0948ad49ee Move the status and runtime badges clear of the node-source badge
Both badges floated above the right end of the title bar, the spot where
the frontend draws its own node-source badge, so turning them on hid one
behind the other. They sit above the left end now.
2026-09-24 00:42:43 -05:00
ausboss 4b812414d9 Describe the unreleased changes in the changelog 2026-09-24 00:38:37 -05:00
ausboss 1ac1506deb List what the workflow contract checks in AGENTS.md 2026-09-24 00:37:55 -05:00
ausboss d4cedb81be Show Select Every Nth doing the thinning in the node tour
The tour's Select Every Nth sat at nth 1, doing nothing, because it could
not carry the frame rate. Load Video now decodes every frame and Select
Every Nth keeps every other one and hands the halved rate to Frame
Interpolate; the output is unchanged: three seconds, 72 frames at 24 fps.
2026-09-24 00:37:50 -05:00
ausboss 58c0a5fab0 Check the example problems this round fixed so they stay fixed
The workflow contract now fails an example whose named widget values
disagree with the values it loads, whose id is not a random UUID, whose
saved zoom is too low to draw widget text, whose download info names a
file its loader does not select, or whose save node writes into a
folder.
2026-09-24 00:36:09 -05:00
ausboss 448194520b Make the examples' named widget values match the values they load
The frontend saves each node's widget values twice, by position and by
name, and restores by name when its experimental Restore widget values
by name setting is on. Nine example nodes had a stale named copy: the
watermark example would have loaded a different source clip and trim
window, two MiniMax H3 samplers would have randomized their seed, and
three copies had the wrong keys. Each now mirrors its loaded values.
2026-09-24 00:35:10 -05:00
ausboss e31282a0b5 Turn VLM reference back on in the Krea 2 Outpaint example
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.
2026-09-24 00:33:41 -05:00
ausboss adf0c28029 Store numbered stage groups in order in four examples 2026-09-24 00:31:55 -05:00
ausboss 135b410150 Stop promising source audio from the silent sample clip
ausboss_pier_9x16.mp4 has no audio stream, so the node tour, LTX and
watermark setup cards now say Save Video keeps the source audio when the
clip has a track.
2026-09-24 00:31:55 -05:00
ausboss 8086eacdde Strip other tools' metadata from the example workflows
Drop cg-use-everywhere's ue_properties and ue_links/links_added_by_ue,
comfyui_mcp workflow ids and VideoHelperSuite's VHS_* settings. None of
these packs is part of the examples; extra.ds and the pack's own node
properties stay.
2026-09-24 00:31:55 -05:00
ausboss 5b1f117314 Open the examples at a zoom that still draws widget text
Fourteen examples saved a view scale between 0.41 and 0.51, under the
frontend's default level-of-detail cutoff (about 0.57), so they opened
with blank widget faces. Raise those to 0.6 and keep each offset, so the
view still starts at the graph's top-left corner.
2026-09-24 00:31:55 -05:00
ausboss ae10a18929 Give every example workflow a valid unique id 2026-09-24 00:31:55 -05:00
ausboss 814b0a2bba Save LoRA Loader's on_missing value in six examples
These loaders were saved before on_missing existed, so their stored
[stack, separator, panel] values put the panel's "" into the on_missing
combo. Store the current order with the default "error".
2026-09-24 00:31:55 -05:00
ausboss ba128f94bc Credit SAM3 Segment to ComfyUI-RMBG in the watermark example 2026-09-24 00:31:55 -05:00
ausboss 0aa39eaa99 Link the caption encoders to the Qwen3-VL 8B download they load 2026-09-24 00:31:55 -05:00
ausboss ce3030354b Point example model loaders at the official download filenames
Eleven examples saved the maintainer's local subfolders (Flux.2 Klein 9B/,
Krea 2/, LTX/, minimax-h3/, Upscale-Models/ ...), so the files the setup
cards link to showed up as "Value not in list". Every core loader now
selects the basename of its download URL, carries a matching
properties.models entry, and the Krea cards name the official
krea2_turbo_fp8_scaled.safetensors their link fetches.
2026-09-24 00:31:55 -05:00
ausboss db7535751d Add the megapixel resize to Video Crop + Rotate + Pad → Frame
The image and clip transform nodes could resize their output to a pixel
budget; the frame picker, whose single frame often feeds an image or
video model, could not. It takes the same appended resize inputs, off by
default, and its face and editor show them like the other two.
2026-09-24 00:31:55 -05:00
ausboss 83b8f299f1 Give the three Crop + Rotate + Pad faces the same controls
The image node had a feather on/off checkbox and no fill colour on its
face while the video nodes showed a fill swatch and a feather amount,
and only the image node had a full Reset. All three now show the same
canvas row (fill, feather in px, and the resize budget where the node
has one) and the same Open editor | Reset crop | Reset buttons.
2026-09-24 00:29:49 -05:00
ausboss 53331fa346 Report the output size from the image and frame transform nodes
Load Image + Pad, Image Resize, Align Image and the Clip node all hand
their width and height to the next node; Image Crop + Rotate + Pad and
Video Crop + Rotate + Pad → Frame did not, so sizing a latent from them
took an extra Image Size node. Both append width and height outputs.
2026-09-24 00:28:19 -05:00
ausboss a2c6b343d1 Seek transport streams from a keyframe before the target
A transport stream seeks to whichever packet precedes the target, not
to a keyframe, so after a seek the decoder dropped everything up to the
next keyframe: a trimmed load began late or, on a clip with a single
keyframe, found no frames at all, and the preview showed and reported
a later frame than the one asked for - frame 0 included, since a seek
to the stream's own start lands past its first keyframe.

decode_from checks the first frame out of a seek and, when it lands
after the target, seeks again from further back and from the top last.
Load Video, the clip decode and the frame previews all seek through it.
2026-09-24 00:26:58 -05:00
ausboss 3b00a6e68e Count video time from the stream's first frame
Trim windows, frame indices and preview times count from the video's
first frame (frame i at i / fps), but the decoders compared them with
the file's own timestamps. A video stream that starts late - about
1.4 s into a transport stream, after an MP4 edit list's delay, or
behind leading audio - lost frames from every window, reported shifted
frame indices that the editor wrote back into frame_index, and cut its
audio out of sync.

stream_seconds moves a decoded time onto the video clock and every
decode, preview, storyboard and audio cut now goes through it. The
metadata duration falls back to the stream's own length rather than
the container's, a late audio track is padded with silence up to its
first sample, and core's VIDEO trim is shifted onto the file's
timestamps.
2026-09-24 00:26:58 -05:00
ausboss b7c55e27d4 Give Load Image + Pad a Reset padding button
The three Crop + Rotate + Pad nodes can reset their canvas from the node
face; Load Image + Pad, the pack's outpaint loader, could only be reset
by zeroing four hidden rows. A corner button on the stage clears every
unlinked side and shows only while there is padding to clear.
2026-09-24 00:26:58 -05:00
ausboss b62805685b Let Select Every Nth carry the frame rate through
Load Video and the Clip node divide their fps when they skip frames,
but Select Every Nth changed only the batch, so a thinned clip played
too fast unless the rate was recomputed by hand. An optional fps socket
and an appended fps output now do that division.
2026-09-24 00:25:30 -05:00
ausboss 9a0f80c483 Retire the dated registry audit and 2.1 workflow review
Both were point-in-time reports whose status lines had gone stale and
whose evidence lived only in untracked scratch folders. What still holds
moved to AGENTS.md (registry history, example workflow rules) and
SECURITY.md (what the rules do not cover); CHANGELOG.md already records
the changes they described.
2026-09-24 00:24:00 -05:00
ausboss c390a412c9 Update the agent and contributor guides to the current pack
AGENTS.md: the lab sync is the maintainer's step, the no-network rule and
the link helper are written down, the node template it pointed to never
existed, validation lists all four checks, publishing follows version
changes only, and the dated registry status becomes durable history. A
new section sets the bar for example workflows. docs/adding_a_node.md
adds the PUBLIC_NODE_IDS step and the documentation a node needs, and
the brand skill matches Save Image's pill and the shared helpers.
2026-09-24 00:23:48 -05:00
ausboss b241467620 Describe the LoRA Loader triggers output as it now behaves 2026-09-24 00:22:35 -05:00
ausboss 01e77fafd3 Keep the execution helpers importable without torch
Moving comfy_torch_device into _execution_helpers put a top-level torch
import there, which broke the standard-library test run in CI. torch is
now imported inside that one function, and its tests skip where torch is
absent.
2026-09-24 00:22:16 -05:00
ausboss 84ee1fd366 Ignore the packed archive and local worktrees and settings
`comfy node pack` writes node.zip into the checkout, and local worktrees
and machine-local settings are per-machine state.
2026-09-24 00:20:58 -05:00
ausboss 1221b9b0a3 Add issue and pull request templates and editor settings
Bug reports ask for the ComfyUI, frontend and pack versions, the install
method, the node, the steps, expected and actual results, and a workflow
or screenshot; ideas start from the workflow problem; security reports
are pointed at the private policy. .gitattributes and .editorconfig pin
what the tree already holds (LF, UTF-8, spaces), and renormalizing
changed no file. Both stay out of the Registry archive.
2026-09-24 00:20:58 -05:00
ausboss 46d4ee39d6 Read the ComfyUI address from COMFY_URL in the dev scripts
The five CDP scripts looked for ComfyUI at http://127.0.0.1:8188/ only;
COMFY_URL now overrides it. cdp_nav_debug.mjs says so when no ComfyUI
tab is open instead of crashing, and cdp_drag.mjs gives up on a request
after 25 s like cdp.mjs rather than waiting forever. The live-testing
notes mention COMFY_URL, give the full path of the screenshot pairs,
and say that only CDP_RELOAD=1 reloads into a fresh tab.
2026-09-24 00:20:58 -05:00
ausboss 74992136df Run stdlib tests, a JavaScript syntax check and ruff in CI
The validate job gains a ten-minute timeout and three steps: the ten
Python test files that need only the standard library (the registry
status test now runs among them), a syntax check of every js/**/*.js
and .mjs file, and ruff 0.16.3 with the pyflakes and syntax-error
rules. Plain `node --check` exits 0 for a broken .js file that uses
import or export, so each file is piped in with --input-type=module.
2026-09-24 00:20:58 -05:00
ausboss f37ca474f6 Name the registry fixtures by their real extension 2026-09-24 00:20:58 -05:00
ausboss 39c636dd10 List the real Registry archive in the release preflight
Check 4 tried .comfyignore on 22 hand-picked paths with its own
gitignore matcher. With git it now lists what comfy-cli packs - tracked
files minus the ones .comfyignore matches - and fails when anything
outside the runtime allowlist ships, when .comfyignore drops a runtime
file, when an allowlist entry, js/docs/ or example_workflows/inputs/
ships empty, or when the archive grows past 12 MB (201 files and 7.1 MB
today). A copy without its own git repository keeps the sample-path
check.
2026-09-24 00:20:58 -05:00
ausboss 58b5d05202 Check public node names and help pages, and allow appended outputs
The transform output check now pins the first four outputs instead of
the whole tuple: saved workflows link outputs by slot index, so those
can never move, but a fifth output appended later is compatible. Two
new checks hold today and keep holding: every public display name ends
with the pack suffix, and every public node has js/docs/<key>.md. The
docstring lists the checks in order and describes check 4 as the code
enforces it, and the never-filled warnings list is gone.
2026-09-24 00:20:58 -05:00
ausboss b4f985a8f4 Validate only the Python files a registry scanner reads
The compile check walked every .py under the checkout, gitignored
_scratch/ ports and nested checkouts included, and py_compile wrote a
__pycache__ folder next to each file it checked. It now compiles in
memory. It and the stray-mapping scan (and its test) share one file
list: tracked plus untracked-but-not-ignored files from git, or, when
git cannot answer for the tree (an installed copy), a walk that skips
dot-folders, caches and _scratch/.
2026-09-24 00:20:58 -05:00
ausboss 1fb4ca9db5 Keep README media out of the Registry archive
The README's images and GIFs were 7.3 MB of a 14.4 MB archive, and only
GitHub shows them; nothing in the pack loads assets/. The editor
swap-file patterns and the one for a local copy of the brand skill
matched no tracked file, and comfy-cli packs tracked files only, so
they are gone.
2026-09-24 00:20:58 -05:00
ausboss 99c563ab5d Correct stale comments, docstrings and exports
Comments credited IS_CHANGED with re-running Image Crop + Rotate + Pad
on a new budget and with keeping a scrub from re-running the Clip node,
but every widget value is already in ComfyUI's cache key: the first
happens anyway and the second is the frontend's fixed serializeValue.
Module docstrings named one node where two or three share the helpers,
the resize/pad plumbing has more than two copies, LaMa's hidden-input
comment sat above the optional preview block, and the inpaint helpers'
__all__ left out build_transform_stitcher and the tone helpers.
2026-09-24 00:20:11 -05:00
ausboss 2c6e4d97b5 Correct stale tooltips and stitcher wording
Load Image + Pad and the three Crop + Rotate + Pad nodes make stitchers
too, but Stitch Inpaint's description, its stitcher tooltip and its
wrong-input errors named only Crop For Inpaint, and the Krea 2 patch
named only Load Image + Pad. Crop For Inpaint claimed its sampling mask
is never feathered though mask_blur softens it; the Frame node's
original output mentioned a resize it cannot do; and Load Image + Pad
said its reference is not computed when unwired, when it is built on
every run.
2026-09-24 00:20:11 -05:00
ausboss c046d973c5 Drop the unused names ruff reports
An unused math import in the Krea 2 core, an unused os import and
stitch_blend_from_mask import in the clip tests, and result assignments
nothing read in the Save Image caption test. ruff check --select F,E9
is clean again.
2026-09-24 00:20:11 -05:00
ausboss f2a512acd7 Drop unreachable branches in Align Image padding and Save Image previews
_fill_tensor already returns the pad color sized to the image's
channels, with extra channels opaque, so _color_pad's own alpha branch
could never run; a test now pins the opaque alpha instead. Save Image
confines every destination to the output folder before writing, so the
preview loop's "saved outside the output folder" fallback was dead too.
2026-09-24 00:20:11 -05:00
ausboss 466123c266 Remove unused node-id constants and NAME_MODIFIERS
CROP_NODE_ID, STITCH_NODE_ID, NODE_ID and LEGACY_NODE_ID were never
read - the mappings already use the string literals registry scanners
require - and leaving them around invites the variable-as-mapping-key
pattern that hides nodes from those scanners. NAME_MODIFIERS in the
image save helpers had no reader either.
2026-09-24 00:20:11 -05:00
ausboss 8c537139cf Share the interrupt, progress, warn-once and device seams
Four helper modules each carried their own _raise_if_interrupted, three
their own _progress_bar and three their own _warn_once, and Frame
Interpolate borrowed comfy_torch_device from LaMa's module.
_execution_helpers now holds one copy of each and the helpers import
them, with the old behavior kept exactly: progress_bar is the plain bar
those loops always built (unlike frame_progress it has no guard for
unknown totals), and warn_once takes each module's own seen-set, key
and cap (none for the stitch, 64 for presets, 256 for colors).
2026-09-24 00:20:11 -05:00
ausboss 2714b1873c Name Stitch Inpaint and the stitcher producers in input errors
The crop helpers' image and mask checks always said "Crop For Inpaint
expected...", including when Stitch Inpaint was handed a malformed
inpainted batch and when Load Image + Pad or a Crop + Rotate + Pad node
built its stitcher. The checks now take the calling node's name, and
each of those nodes passes its own. Crop For Inpaint's messages are
unchanged.
2026-09-24 00:20:11 -05:00
ausboss f45b565a16 Name the Clip node in its decode errors and memory advice
Video Crop + Rotate + Pad -> Clip decodes through Load Video's helper,
whose errors hard-coded "Load Video" and, when a trim would not fit in
memory, told the user to set custom_width/custom_height - inputs the
Clip node does not have. The decode now takes the calling node's name
and its own advice; the Clip node suggests every_nth, max_frames and
fixed_frames. Load Video's messages are unchanged.
2026-09-24 00:20:11 -05:00
ausboss 63b7f3b5f9 Report Save Video's filename_prefix errors against Save Video
Save Video shares sanitize_exact_name with Save Image, which hard-coded
its own node and input, so a rooted prefix such as "/abs/x" failed with
"Save Image: exact_name must be a relative name". The helper now takes
the node and input to name; Save Image's wording is unchanged.
2026-09-24 00:20:11 -05:00
ausboss 60fd7ce8cf Declare LoRA Loader's on_missing choices from MISSING_MODES
_lora_helpers.py defines the tuple but nothing used it; the node spelled
the same list out again. It now reads the constant, so the two cannot
drift apart. The choices and their order are unchanged.
2026-09-24 00:20:11 -05:00
ausboss 9f75e2928c Leave a missing LoRA's trigger words out of the triggers output
In skip mode a row whose file cannot be found is warned about and left
out of the stack, yet its trigger words still reached the prompt,
steering the image toward a LoRA that never loaded. apply_lora_stack can
now report the rows it skipped as missing, and the node joins words only
from the rest. Rows parked at strength 0 keep their words, as the node
docs promise, so comparing with and without a LoRA changes only the
weights. The output tooltip no longer says "comma-joined": the
separator is configurable.
2026-09-24 00:20:11 -05:00
ausboss 0710d864ae Settle every Save Image collision before writing any frame
With on_existing set to "error", the policy was checked frame by frame
inside the write loop, so a batch whose second name already existed
wrote the first image and its caption sidecar and only then stopped -
despite the comment promising the whole batch is validated first. The
collision check now runs for every destination, after the path checks
and before the first write, so an error leaves the folder untouched.
2026-09-24 00:20:11 -05:00
ausboss 364db11318 Honor --disable-metadata in Save Video
Save Image and core's Save Video both leave the prompt and workflow out
when ComfyUI is launched with --disable-metadata, but Save Video only
read its own save_metadata toggle, so a server whose owner had turned
metadata off still embedded the graph in every video. It now consults
the same flag Save Image does.
2026-09-24 00:20:11 -05:00
ausboss f5cf891661 Trim Stitch Inpaint's blend mask to the frames that came back
apply_stitch already keeps the leading frames when a video model returns
fewer than the stitcher holds (LTX keeps 8n+1, Wan 4n+1), but
stitch_blend_mask still demanded the full per-frame mask, so the node
printed "stitching the first 49" and then failed building its blend_mask
output. Both paths reproduced it: the Clip node's per-frame stitcher and
Crop For Inpaint with a per-frame mask. The mask now follows the stitch;
asking for more frames than the stitcher holds is still an error.
2026-09-24 00:20:11 -05:00
ausboss af12fec620 Correct node help pages that no longer match the nodes
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.
2026-09-24 00:20:05 -05:00
ausboss b2d40a1b9f Bring the README up to date for new users
Add Manager and comfy-cli installs and explain updates, correct the
upgrade note (1.x lost more than LM Studio Chat), fix the optional
extras commands (installing the pack as a package fails), describe the
pack-wide tools the README never mentioned, and link the changelog,
contributing guide and security policy.
2026-09-24 00:18:21 -05:00
ausboss 685aff6c1d Restore the 1.2.0 changelog heading
An edit after the 1.2.0 release renamed its heading to Unreleased while
adding new entries above it, so the 1.3.0 release absorbed every 1.2.0
entry. The heading goes back above the first 1.2.0 entry.
2026-09-24 00:16:50 -05:00
ausboss 657b47cc4a Publish only when main's version changes, after the offline checks
The publish workflow uploaded on any pyproject.toml change reaching main,
so a keywords or description edit sent the same version to the Registry
again, a manual run could publish from any branch, and nothing required
validation to pass first. It now compares the version with the previous
commit, runs only on main, runs the offline checks, and publishes after
they pass.
2026-09-24 00:15:59 -05:00
ausboss b625b9306c Share one isAusbossNode check between appearance and recreate 2026-09-24 00:13:41 -05:00
ausboss 216c885b75 Show every pack message through one toast helper
Four extensions each carried their own copy of the toast-or-console
fallback, transform upload failures used a blocking alert(), and a
failed video drop onto Load Video only logged to the console. They all
go through showToast now, so every message looks and behaves the same.
2026-09-24 00:13:30 -05:00
ausboss c6f812eaae Correct two stale frontend header comments 2026-09-24 00:12:40 -05:00
ausboss 54b2ea3347 Add a copy button to Show Text
Seed and Workflow Note could copy their text; Show Text, whose job is
showing text, could only be selected by hand, and a long result shown
truncated could not be copied whole at all. The corner button copies
the complete string and reports whether it reached the clipboard.
2026-09-24 00:12:15 -05:00
ausboss dfc5f03795 Make every LoRA Loader number a scrub control
The info card's suggested-strength bounds were bare number inputs and
the gear menu's default strength and step were typed text boxes, the
last numbers in the pack that did not scrub. The scrub control gains an
optional empty state for bounds that may be absent ("any"), and gear
menu scrub rows take their own step and precision.
2026-09-24 00:11:15 -05:00
ausboss f656a3d954 Record LoRA, Compare and Mask Refine panel edits in undo history
The pack's DOM panels must tell ComfyUI's change tracker about their
writes, and several did not: LoRA Loader row edits other than a few
menu actions, Compare's slide / A-B mode, and Mask Refine's AUTO. LoRA
rows now record on every commit, with strength drags recording once on
release, and the other two record when they change a saved value.
2026-09-24 00:08:36 -05:00
ausboss 4fe99e6764 Make the image transform face Reset match its tooltip
The quick Reset under Image Crop + Rotate + Pad promised rotation, crop
and padding but ran the editor's full reset, which also restored fill,
feather and Align. It now resets only the shape, the same keys a source
swap resets. The editor's Reset transform tooltip names Align, which it
does reset.
2026-09-24 00:06:47 -05:00
ausboss 402bd671fd Keep a node's own settings when a gear menu is reset
Reset restored every schema default, including the entries that mirror
the open node rather than a stored preference. On the LoRA Loader that
turned separate strengths off and copied each row's model strength over
its CLIP strength; on Resolution the popup showed a latent layout and
batch the node did not have. Reset now leaves those entries as they are,
and LoRA Loader gear edits enter undo history.
2026-09-24 00:06:01 -05:00
ausboss d782aeee95 Fix Save Image's name field, Browse popup and embed toggle
In legacy exact-name mode the name field wrote the filename prefix as
well as the exact name, so clearing the field to return to composed
names left the original prefix gone. The field now writes only the
widget it shows.

A second Browse left the first popup's outside-click listener behind,
which closed the new popup on the first click inside it. Each popup now
owns its listeners, Browse toggles it, and Esc closes it.

Embed workflow is an off | embed pill like every other boolean on the
pack's cards, instead of a small switch.
2026-09-24 00:04:51 -05:00
ausboss abd899afe2 Report copy results truthfully on Seed and Workflow Note
Over plain http (ComfyUI on a LAN address) the page has no Clipboard
API, and the optional-chained write resolved without copying anything,
so the buttons showed a success they had not earned. One shared helper
now falls back to the copy command and says whether the text really
reached the clipboard; the buttons show a failure mark when it did not.
2026-09-24 00:03:31 -05:00
ausboss 76e4f40d5c Stop the help badge click from dragging the node
chainCallback keeps the earlier handler's return value, so the badge's
"consumed" answer never reached LiteGraph and every click on "?" also
started a node drag. chainHandler reports a click as consumed when any
handler in the chain did. Run Timer, which has no title bar for the
badge, now opens the same card from an "About this node" menu item.
2026-09-24 00:01:54 -05:00
ausboss aef487375a Save to the output folder itself by default
Save Image and Save Video defaulted to an AusBoss subfolder, and every
example workflow saved into one. New nodes now save to ComfyUI's output
folder as image_... and video_..., and the examples keep their file
names without the folder. Saved workflows keep whatever prefix they
already have.
2026-09-24 00:00:43 -05:00
ausboss b336b6b3dc Restore trim values when a saved video is dropped on Load Video
Current frontends give a node with an upload button its own onDragDrop,
which shadowed the pack's prototype hook: the frontend uploaded the file
itself and the trim and sampling values stored in an AusBoss-saved video
were never restored. The instance handler is now wrapped so video files
come to the pack first and every other drop still reaches the frontend.
2026-09-23 23:58:47 -05:00
ausboss 44024352ce Show Save Video's CRF and Metadata rows only for formats that read them
When Save Video moved to a widget card, its rows lost the per-format
hiding: CRF showed for gif, ProRes and FFV1, and Metadata for gif and
WebP. The old code that toggled the classic widgets was still running
under the card and could bring them back. The card rows now follow the
format directly.
2026-09-23 23:56:58 -05:00
ausboss 70c15690e8 Keep the video transform panels visible when zoomed out
The frontend hides a DOM widget below a zoom threshold unless it opts
out. The video transform nodes mounted their panel through the one
addDOMWidget call without hideOnZoom: false, so zooming out left an
empty box. A guard test now checks every DOM widget in the pack.
2026-09-23 23:55:50 -05:00
ausboss af74774f21 Keep saved widget values when replacing missing nodes
Current frontends give a missing-type placeholder display widgets named
UNKNOWN, UNKNOWN_1, ... Replace with AusBoss nodes read those instead of
the saved widgets_values, so every replacement started from defaults.
Registered nodes still read their live widgets by name.
2026-09-23 23:54:58 -05:00
ausboss ff1b7ec8ca Start the Unreleased changelog section 2026-09-23 23:47:52 -05:00
ausboss c087ea8dd8 Keep the node module list alphabetical 2026-09-23 23:47:52 -05:00
ausboss 16518ea89b Add security and contributing guides
SECURITY.md states what the pack reads, writes and never does, and how
to report a problem privately. CONTRIBUTING.md covers useful bug reports
and the checks a pull request runs. Both stay out of the Registry archive.
2026-09-23 23:47:52 -05:00
ausboss f6ecb51d61 Fail the release preflight on shipped network code
Shipped backend code must not contain an HTTP or socket client, and the
frontend must not reintroduce the index-based connect call.
2026-09-23 23:47:52 -05:00
ausboss 37f4da69d8 Create graph links through connectSlots in one shared helper
Recreate node and Replace with AusBoss nodes each carried the same link
lookup, snapshot and rollback code, and wired links with the index-based
connect call that the Registry scan reads as a network socket. The shared
helper resolves the slots with the same checks and calls connectSlots,
which is what that call did internally.
2026-09-23 23:47:52 -05:00
ausboss 463bd89f7f Remove the Civitai lookup so the pack makes no network requests
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.
2026-09-23 23:47:52 -05:00
ausboss 3bc8ffa362 Drop the committed scratch note and prune stale ignore rules
todo2 was a finished checklist that rode along with the 2.1.0 release
commit. The ignore files listed personal note names one by one, including
several that no longer exist; local notes now follow one rule instead.
2026-09-23 23:35:26 -05:00
ausboss 4cb2af204a Release 2.1.0: Qwen workflows and polished image/video controls 2026-09-22 15:23:07 -05:00
ausboss cf332cedf7 Merge pull request #64 from ausboss/codex/outpaint-color-bands
Prevent flickering color bands in the LTX outpaint example
2026-09-11 14:27:22 -05:00
ausboss 7970503b97 Disable seam color matching in the LTX video outpaint example 2026-09-11 14:26:24 -05:00
ausboss da696d608d Merge pull request #63 from ausboss/codex/lora-insert-rows
Insert empty LoRA slots above or below a row
2026-09-11 14:24:04 -05:00
ausboss 7f2395c1de Add empty LoRA row insertion above and below 2026-09-11 14:23:08 -05:00
ausboss aabf2ca850 Merge pull request #62 from ausboss/codex/registry-review-report
Report pending Registry reviews without failing the status check
2026-09-10 10:08:59 -05:00
ausboss 9c4efa49fd Report pending Registry reviews without failing the status check 2026-09-10 10:08:21 -05:00
ausboss 1bb6f4cb7a Merge pull request #61 from ausboss/codex/release-2.0.2
Release 2.0.2: audited media containment and fixed Civitai requests
2026-09-10 09:56:47 -05:00
ausboss 410d33059f Release 2.0.2 2026-09-10 09:56:02 -05:00
ausboss 8e780832ae Merge pull request #60 from ausboss/codex/fixed-civitai-and-chime
Keep Civitai lookups fixed and generate the completion chime locally
2026-09-10 09:47:50 -05:00
ausboss 010f996bd0 Keep Civitai lookups fixed and generate the completion chime locally 2026-09-10 09:47:08 -05:00
ausboss 6d55bdd4bd Merge pull request #59 from ausboss/codex/contain-media-paths
Contain media reads and saves within ComfyUI folders
2026-09-10 09:46:55 -05:00
ausboss 3fd4d37f85 Contain media reads and saves within ComfyUI folders 2026-09-10 09:45:40 -05:00
ausboss 9cdaff9283 Merge pull request #57 from ausboss/feat/clip-timeline
Release 2.0.1: video timeline overhaul, outpaint-ready clip canvas, local path gate
2026-09-09 17:18:57 -05:00
ausboss 5c04bd6079 Release 2.0.1 2026-09-09 17:18:47 -05:00
ausboss 0be940f8dc Gate local path mode behind the operator's opt-in for queued runs too
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.
2026-09-09 17:08:18 -05:00
ausboss beb98ad6a8 Video timeline overhaul, outpaint-ready clip canvas, example styling
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.
2026-09-09 17:03:01 -05:00
ausboss 472b4403b8 Merge pull request #56 from ausboss/release-2.0.0 2026-09-09 17:01:33 -05:00
ausboss 699208dadc Prepare 2.0.0 release candidate 2026-09-07 19:56:23 -05:00
ausboss 528a864657 Merge pull request #55 from ausboss/release-1.3.0
Release 1.3.0
2026-08-31 11:32:41 -05:00
ausboss f00a1ca013 Release 1.3.0
The LoRA loader promotion (strength bars, absorb chain, moved-file
resolution, reconnect, master-toggle memory, slot-band bar), the Image
Crop + Rotate + Pad megapixel resize and quick row, the scrub-input
standard, and the dynamic README badge.
2026-08-31 11:32:33 -05:00
ausboss b85a380f44 Merge pull request #54 from ausboss/promote-lora-loader
Promote the lab LoRA loader feature set into the public node
2026-08-31 11:17:27 -05:00
ausboss e90be183a3 Absorb appends below the existing rows instead of on top
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.
2026-08-31 11:12:28 -05:00
ausboss 67d8512425 Promote the lab LoRA loader feature set into the public node
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.
2026-08-31 11:01:37 -05:00
ausboss 1649e01b38 Merge pull request #53 from ausboss/dynamic-release-badge
Make the README release badge dynamic
2026-08-30 23:11:17 -05:00
ausboss f2f69afc97 Make the README release badge dynamic instead of checked
shields.io's dynamic TOML badge reads the version straight out of
pyproject.toml on main at view time, so the badge cannot go stale by
construction - no third version string to bump. Preflight check 3 flips
from comparing a hardcoded number to guarding the mechanism: the dynamic
badge must be present and a hardcoded release-X.Y.Z badge fails the
check. AGENTS.md's release step 1 goes back to two version strings.
2026-08-30 23:08:56 -05:00
ausboss fa2808a451 Merge pull request #52 from ausboss/release-version-truth
Nail down release-version truth: badge preflight, publish-run and flagged-status release steps
2026-08-30 23:06:16 -05:00
ausboss e4738ef520 Nail down release-version truth: badge check, publish-run and flagged-status steps
The README badge advertised 1.0.0 through two releases, and the registry
side had two silent failure modes nobody was watching:

- release_preflight.py check 3: the README release badge must name the
  pyproject version, so the three places a version lives (pyproject,
  AUSBOSS_JS_VERSION, badge) can never disagree again. Badge bumped to
  1.2.0; README also names the real ComfyUI-floor key (requires-comfyui).
- AGENTS.md release step 4: the push-triggered publish run has failed
  before (1.2.0's run died; the version only shipped via a manual
  dispatch) - a red run is an unpublished release until re-run.
- AGENTS.md release step 5: verify the version AND its status on the
  registry. Every version this pack has published sits in
  NodeVersionStatusFlagged - the automated scan holding it for human
  review - and flagged versions are invisible in Manager's Select
  Version picker and set no last-update, which is exactly the empty
  picker on the live install. Ask the Comfy team for review; do not
  re-publish.
2026-08-30 22:27:09 -05:00
ausboss 8251fc1642 Merge pull request #51 from ausboss/lora-loader-polish
LoRA loader polish, scrub-number standard, and Crop+Rotate+Pad megapixel resize
2026-08-30 22:19:22 -05:00
ausboss dd118c7386 Image Crop + Rotate + Pad: megapixel resize, quick row, relocated size readout
- 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.
2026-08-30 22:14:39 -05:00
ausboss b2999a010e Scrubbable numbers become the pack standard
makeScrubInput in js/shared/scrub_input.mjs is the LoRA strength box's
interaction made reusable: drag the value to scrub (dead zone keeps
clicks clicks), click to type an exact value, chevron arrows step,
ArrowUp/Down step from the keyboard, Shift always means the fine step.
Values are quantized through one choke point, and edit mode re-asserts
itself a frame after the click because the graph canvas can steal focus
mid-sequence.

The gesture math is pure and tested (tests/scrub_input.test.mjs).
AGENTS.md and the brand skill record the convention - numeric fields in
pack panels use this control, never a bare input[type=number] - and
AGENTS.md also picks up the lab shared-code vendoring pull step.
2026-08-30 22:14:30 -05:00
ausboss da5649e3b2 LoRA loader and panel polish round
- 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.
2026-08-30 22:14:30 -05:00
ausboss 460177869d Merge pull request #50 from ausboss/docs/release-process
Document the release process in AGENTS.md
2026-08-27 13:59:33 -05:00
ausboss 440bddd4fc Document the release process in AGENTS.md
Landing a pyproject.toml change on main now publishes to the Comfy
Registry automatically, so the working rules must say so: a Releasing
section covers the version + AUSBOSS_JS_VERSION pairing (preflight
enforced), the changelog retitle, and keeping the registry description
naming the actual nodes since Manager search matches against it. The
hard rule against casual version bumps points at the consequence, and
the brand skill reminds that a new public node gets named in the
description at the next release.
2026-08-27 13:58:51 -05:00
ausboss 3f7e41d34f Merge pull request #49 from ausboss/crush-list-build-out
Save Image node + 1.2.0 release with registry auto-publish
2026-08-27 13:54:09 -05:00
ausboss 3b81a79c1e Add Save Image, fix the invented-pixels case, and cut the 1.2.0 release
- 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.
2026-08-27 13:50:47 -05:00
ausboss cb51a0a9c0 Merge pull request #48 from ausboss/crush-list-build-out
Crush-list build-out: seven new node files, replace button, video & LoRA polish
2026-08-27 13:32:09 -05:00
ausboss 3f715a4af8 Ship the crush-list build-out: 7 new node files, replace button, video and LoRA polish
- 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.
2026-08-27 13:30:35 -05:00
ausboss c12b94299e Merge pull request #47 from ausboss/codex/standard-civitai-sidecars
Use standard Civitai LoRA sidecars
2026-08-25 15:21:58 -05:00
ausboss 80ef06bb4b Use standard Civitai LoRA sidecars 2026-08-25 15:05:05 -05:00
ausboss aa08f41cc2 Merge pull request #46 from ausboss/fix/preview-panel-height
Let the result preview follow the node's height
2026-08-18 17:25:58 -05:00
ausboss fb40d4288c Let the result preview follow the node's height
Mask Refine, LaMa Inpaint and Select Frame drew their picture into a
container pinned at 140px, so dragging any of them taller only added dead
space underneath - the same complaint already fixed for Load Video, Image
Compare A/B and Save Video.

The panel is shared, and it was pinned deliberately: while it showed a small
thumbnail of the node's INPUT, a constant height was right, and the guard in
panel_guards.test.mjs listed it under fixedByDesign for exactly that reason.
Making it show the node's own RESULT turned it into a viewport onto a
picture, which is what the other set is for, and that reclassification was
missed - so the guard kept passing while the panel it described no longer
existed.

It goes through fillNodeHeight now, with a 140px floor and no ceiling, and
declares a minimum width to both layout paths because it carries the
AUTO/MORE buttons and can no longer be exempt from that. The guard moves it
to mustGrow and says out loud that this classification is a decision to
review whenever what a panel DISPLAYS changes, not only when one is added -
verified by reintroducing the pin and watching both guards name it.
2026-08-18 17:14:51 -05:00
ausboss a044b0fc84 removing half baked workflows 2026-08-18 17:07:49 -05:00
ausboss a9da3ff558 Merge pull request #45 from ausboss/fix/testing-round-2
Preview each node's own result, and rework Save Video
2026-08-18 17:02:39 -05:00
ausboss c8099952e5 Clear the compare panel, and make the example workflow shippable
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.
2026-08-18 16:59:44 -05:00
ausboss 0e4749db6b Preview each node's own result, and rework Save Video
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.
2026-08-18 16:31:44 -05:00
ausboss 6d5d790ddc Merge pull request #43 from ausboss/chore/demote-frame-chooser
Move Frame Chooser to the lab
2026-08-17 23:43:54 -05:00
ausboss 5a93c3c44a Move Frame Chooser to the lab
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.
2026-08-17 23:40:20 -05:00
ausboss 3da1681a3b Merge pull request #44 from ausboss/fix/transform-panel-resize
Fix the stretched transform preview; rename Video Crop + Rotate + Pad to "→ Frame"
2026-08-17 23:38:54 -05:00
ausboss 48a58448a4 Rename Video Crop + Rotate + Pad to "-> Frame"
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.
2026-08-17 23:35:17 -05:00
ausboss 591f8505d2 Redraw the transform panel when its element resizes
Video Crop + Rotate + Pad drew its preview stretched after a resize. The
canvas is CSS-stretched to whatever box the panel is given, but
prepareCanvas() sizes the backing store from the element at DRAW time, so
a box that changes shape without a redraw shows the last frame at the
wrong aspect ratio.

The panel's ResizeObserver was created inside a `kind === "image"` branch,
grouped with the interactive drag handlers because the comment there
reasons about hit-testing. Hit-testing is only half of what it does. The
image node redrew on resize and looked right; the video node - a passive
preview with no other reason to redraw - did not.

Latent until panels started taking the node's leftover height: before
that the box could not change shape without a width change, and a width
change forced a relayout that redrew anyway.

The observer now runs for both kinds; only the drag handlers stay
image-only. Two tests hold the line - one fails if any canvas-painting
panel ships without a resize observer, one fails if this observer is
gated on node kind again. Verified by reintroducing the bug: the second
test fails and names the offending condition.
2026-08-17 23:18:22 -05:00
ausboss 19bcc213ab Merge pull request #42 from ausboss/feat/promote-krea2-outpaint
Warn when an outpaint is padded on both axes
2026-08-17 22:53:16 -05:00
ausboss 8e2775da74 Warn when an outpaint is padded on both axes
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.
2026-08-17 22:49:18 -05:00
ausboss 38aeec6d09 Merge pull request #41 from ausboss/feat/promote-krea2-outpaint
Promote the Krea 2 outpaint pair from the lab
2026-08-17 22:26:08 -05:00
ausboss bf87fd92a6 Merge pull request #40 from ausboss/fix/panels-follow-node-height
Panels follow the node's height; rename Compare to Image Compare A/B
2026-08-17 22:25:45 -05:00
ausboss ec50d3acfe Promote the Krea 2 outpaint pair from the lab
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.
2026-08-17 22:17:19 -05:00
ausboss 4b5d6429d8 Rename Compare to Image Compare A/B
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.
2026-08-17 22:15:16 -05:00
ausboss 9262c515e5 Let every node panel follow the node's height
Dragging a node taller only ever widened its stage. The frontend
arranges widgets in one pass:

  if (w.computeSize)            -> fixed height, kept OUT of the split
  else if (w.computeLayoutSize) -> joins distributeSpace(freeSpace, ...)
  else                          -> one standard widget row

It is an else-if, so a widget declaring both is pinned by computeSize
and its computeLayoutSize is never called. Every stage, player and
filmstrip here derived that fixed height from the node's WIDTH, so the
extra height became dead space underneath. Compare was fixed by hand;
the other six had the same defect.

Adds js/shared/panel_layout.mjs with one fillNodeHeight() that deletes
computeSize and declares a floor with no ceiling - distributeSpace reads
a missing maxSize as Infinity, so the panel takes whatever is left.
Applied to Compare, Save Video, Load Video, Frame Chooser, Load Image +
Pad, Image Crop+Rotate+Pad, and the shared transform panel that Video
Crop+Rotate+Pad mounts. Removing computeSize also removed where those
nodes got their opening height, so Save Video, Load Video and Load Image
+ Pad now state a default; a saved size still wins through onConfigure.

Measured in the browser: all seven grow 1:1 with the node and clamp at
their own floor.

The panel audit now enforces it - a DOM panel is either a stage that
calls fillNodeHeight and never assigns computeSize, or is listed as
fixed by design - so a new panel cannot quietly reintroduce this.
2026-08-17 22:15:16 -05:00
ausboss 2364d60338 Merge pull request #39 from ausboss/chore/prune-redundant-nodes
Remove Drop Shadow and Pad Image
2026-08-17 22:08:44 -05:00
ausboss 772f78d031 Remove Pad Image
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.
2026-08-17 21:59:07 -05:00
ausboss 10382f1643 Remove Drop Shadow
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.
2026-08-17 21:59:07 -05:00
ausboss 9ad24d7314 Merge pull request #38 from ausboss/fix/node-polish-round
Fix LoRA mismatch detection, LM Studio reasoning, Compare height; add pad stitcher
2026-08-17 21:03:08 -05:00
ausboss 139082a9d9 Fix LoRA mismatch detection, LM Studio reasoning, Compare height; add pad stitcher
Six fixes found while testing every node against real workflows.

LoRA Loader: the "wrong base model" warning never fired on a modern stack.
It matched comfy's model class name against a table covering 8 of the 81
classes comfy ships, so Krea 2, Qwen, WAN, LTXV, Z-Image and friends read as
"unknown" and skipped the check entirely - an SD 1.5 LoRA on Krea 2 patched
nothing, changed nothing, and said nothing. It now measures the applied-patch
count across model and CLIP before and after each row and warns when a row
patched nothing, which needs no table, no LoRA metadata, and works on every
model family. base_model_family also stopped mining ss_sd_model_name: that is
the trainer's source filename, and reading substrings out of it labelled any
..._v1.safetensors as SD1.5 and anything containing xl as SDXL.

LM Studio Chat: reasoning returned in the server's own reasoning_content field
now reaches the thinking output. Only inline <think> tags were read, so with
gemma/qwen-style hybrids the whole reply landed in a field the node never
looked at and text came back blank. An empty answer now explains itself,
naming the max_tokens budget when the model ran out of room mid-thought.

Compare: the stage follows the node's height, not only its width. Its panel
declared a computeSize, and the widget layout gives any widget that defines one
a fixed height and leaves it out of the leftover-space split, so dragging the
node taller only added dead space under the image.

Pad Image / Load Image + Pad: a stitcher output, so an outpaint can put the
source back exactly. Padding now builds the same stitcher shape Crop For
Inpaint emits - the crop is simply the whole canvas - so the existing Stitch
Inpaint node serves both and no second stitch node has to exist. Appended
last, so saved workflows keep their links.

Settings menus: editing one row no longer reverts the others. Each save handed
back a value set rebuilt from stored defaults, silently replacing every other
row on a node whose workflow differed from those defaults.

LoRA templates popover: Save was clipped off the panel edge and each saved
row's delete x was stretched across the whole row - one blanket
.ausboss-lora-menu button rule outranked the specialized buttons inside it.
2026-08-17 20:47:27 -05:00
521 changed files with 138022 additions and 9409 deletions
-123
View File
@@ -1,123 +0,0 @@
---
name: ausboss-node-brand
description: Brand identity and design language for ComfyUI-AusBoss nodes — naming, visual language, settings conventions, frontend interaction etiquette, and the proof checklist. Load when building, porting, or restyling an AusBoss node or any pack-wide frontend feature.
---
# AusBoss node brand and design language
This skill layers the pack's *design language* on top of the hard rules in
[AGENTS.md](../../../AGENTS.md) and the admission checklist in
[docs/adding_a_node.md](../../../docs/adding_a_node.md). Read those for what is
allowed; read this for what makes a node feel like an AusBoss node.
## Identity
- Mapping key `AUSBOSS_NODES_<Purpose>` — permanent API, never renamed, and
always written as a string literal in both mapping dicts so registry
scanners (which parse, not import) can discover it.
- Display name `<Name> 🆎`; category `🆎 AusBoss/<Group>`. The category, the
`AUSBOSS_NODES_` id prefix, and the "ausboss" `SEARCH_ALIASES` entry keep
the pack searchable by name.
- Every node ships `DESCRIPTION`, input `tooltip`s, `OUTPUT_TOOLTIPS`, and
`SEARCH_ALIASES` with intent words the display name lacks ("trim", "cut",
"outpaint", "watermark").
- Namespace everything frontend-visible with `ausboss`: routes, extension
names, events, CSS classes, DOM markers, caches.
**This identity is for the public pack only.** Experiments live in the private
ComfyUI-AusBoss-Lab repo and wear a parallel grammar so the two never collide
and a lab node is obvious on the canvas:
| | public pack | lab |
|---|---|---|
| mapping key | `AUSBOSS_NODES_<Purpose>` | `AUSBOSS_LAB_<Purpose>` |
| display name | `<Name> 🆎` | `<Name> 🧪` |
| category | `🆎 AusBoss/<Group>` | `🧪 AusBoss Lab/<Group>` |
Do not apply this skill's polish budget to a lab node. Lab nodes deliberately
skip example workflows, thumbnails, frontend work and changelog entries —
those are release-gate items, and adding them early is how a workshop turns
into a second product. They also fall outside `js/appearance/`, which keys on
the `AUSBOSS_NODES_` prefix, so they stay uncolored on purpose.
## Visual language
- Editor handle grammar (keep it consistent everywhere): **cyan squares** =
crop, **orange diamonds** = padding, **green knob** = rotate. Section
legends reuse the same markers so the colors teach themselves.
- Node colors come from the scheme table in
[js/shared/appearance.mjs](../../../js/shared/appearance.mjs) — the single
source of truth. Adding a scheme means adding one table row; the tests in
`tests/appearance.test.mjs` validate it automatically. Title is always
darker than body in the muted rows; the flagship "AusBoss" row (the
pack-wide default) pairs the teal title with a near-black body. Values are
tuned against the dark canvas.
- Coloring is automatic: `js/appearance/index.js` targets every class whose
`comfyClass` starts with `AUSBOSS_NODES_` (plus the `SimpleWatermarkRemover`
alias). New nodes need zero per-node color work.
- Node faces stay compact. Big interactions get a fullscreen editor or an
inline mode — never a node with a huge fixed minimum size.
## Settings conventions
- Ids are `AusBoss.<Area>.<Name>`; register declaratively via the
`settings: [...]` array on `app.registerExtension`.
- `category: ["🆎 AusBoss", "<Area>", "<Leaf>"]` with a **distinct leaf per
setting** — settings sharing a leaf collapse into one panel row.
- Tooltips explain the *consequence* of the setting, in prose.
- `onChange(value)` must use the passed value (on some frontends it fires
before the store write) and must guard against no-op changes. Seed the
current value in `setup()` — `onChange` does not fire for the stored value
on load on all versions.
- Every non-obvious behavior gets an off switch; surprising behaviors default
to off.
## Frontend patterns
- Entries in `js/<feature>/index.js` auto-load; shared logic lives in
import-only `js/shared/*.mjs`. Pure decision logic and geometry go in the
`.mjs` modules with `node:test` coverage in `tests/*.test.mjs` — DOM wiring
stays thin in the entry file.
- **Widgets are the single source of truth.** Rich UI drives hidden standard
widgets (`serialize: false` on DOM widgets); no custom serialization, so
save/load, undo, and API format work through LiteGraph's default path.
- Chain lifecycle hooks with `chainCallback` from `js/shared/index.mjs`;
never assign prototype callbacks directly.
- Build API URLs through the `api` module helpers — never write root-relative
strings like `/view?...`, which break behind proxies and hosted frontends.
## Interaction etiquette
- A click on empty panel space must fall through (no `preventDefault`) so the
node stays draggable from its body.
- Wheel and middle-click belong to the graph (zoom/pan) unless the widget is
actively using them for its own view — capture them only in an explicit
edit mode.
- Set LiteGraph node properties with `node.color = undefined`, never
`delete node.color` — deleting breaks reactivity under the Nodes 2.0
renderer.
- Distinguish "workflow is loading" from "user changed the source" before
resetting state; restored values must never be wiped by a load-path
callback.
- Drawn glyphs stay small; hit zones stay generous (larger than the glyph).
Corners are hit-tested before edges.
## Backend patterns
- Shared compute in `nodes/_<topic>_helpers.py`; node files are thin V1
wrappers. IMAGE is BHWC float, MASK is BHW, returns are always tuples.
- Import-time console output stays ASCII (Windows cp1252 consoles); the 🆎
emoji is safe only in categories and display names.
- Treat every route parameter and widget value as attacker-controlled: path
containment checks live in shared helpers, never re-rolled per call site.
## Prove it
```bash
python scripts/validate_nodes.py
node --test tests/*.test.mjs
```
Then the full checklist in [docs/adding_a_node.md](../../../docs/adding_a_node.md):
restart ComfyUI, watch the banner, check `/object_info`, queue an API graph,
load the example workflow, and hard-refresh the browser after JS changes.
+40
View File
@@ -0,0 +1,40 @@
# Files comfy-cli leaves OUT of the Registry archive (gitignore syntax; the
# packer zips git-tracked files minus these, so untracked local files need
# no entry). Runtime code, node help, example workflows, README, LICENSE and
# pyproject stay in. Everything below is development-only or README media:
# it adds nothing for a user.
#
# scripts/release_preflight.py lists the archive the same way and fails when
# a path outside its runtime allowlist ships, when a runtime file is dropped
# (js/docs/ is node help and must ship), or when the archive outgrows its
# size budget.
# tests and offline tooling
tests/
scripts/
# CI and repository automation
.github/
# developer docs and tool settings (docs/ at the root only)
AGENTS.md
CLAUDE.md
CONTRIBUTING.md
SECURITY.md
.claude/
/docs/
# README media: GitHub shows it on the repository page, nothing in the
# pack loads it
/assets/
# the changelog: the Registry shows each version's notes, made from it when
# the version is published (scripts/changelog_notes.py), and the full list
# stays in the repository; nothing in the pack reads it
CHANGELOG.md
# repository files
.gitignore
.comfyignore
.gitattributes
.editorconfig
+19
View File
@@ -0,0 +1,19 @@
# Matches what the repository holds: UTF-8, LF, a final newline, spaces.
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
trim_trailing_whitespace = true
[*.py]
indent_size = 4
[*.{js,mjs,json,yml,yaml,md}]
indent_size = 2
# Two trailing spaces are a Markdown line break.
[*.md]
trim_trailing_whitespace = false
+2
View File
@@ -0,0 +1,2 @@
# Text files are stored and checked out with LF line endings on every OS.
* text=auto eol=lf
+1
View File
@@ -0,0 +1 @@
ko_fi: ausboss
+64
View File
@@ -0,0 +1,64 @@
name: Bug report
description: A node errors, gives a wrong result, or looks broken.
labels: [bug]
body:
- type: markdown
attributes:
value: >-
Security problems go through the
[security policy](https://github.com/ausboss/ComfyUI-AusBoss/security/policy),
not a public issue.
- type: input
id: comfyui-version
attributes:
label: ComfyUI and frontend versions
description: Both are under Settings → About.
placeholder: ComfyUI v0.x.y, frontend v1.x.y
validations:
required: true
- type: input
id: pack-version
attributes:
label: AusBoss version
description: The console banner prints it when ComfyUI starts, as "AusBoss vX.Y.Z".
placeholder: AusBoss v2.1.0
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installed with
options:
- ComfyUI-Manager or the Comfy Registry
- git clone
- type: input
id: nodes
attributes:
label: Node(s)
placeholder: Save Image 🆎
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1. Load the attached workflow
2. Set ...
3. Queue it
- type: textarea
id: expected
attributes:
label: What you expected
- type: textarea
id: actual
attributes:
label: What happened instead
description: Include any error from the ComfyUI console or the browser console.
validations:
required: true
- type: textarea
id: attachments
attributes:
label: Workflow or screenshot
description: Drag the smallest workflow .json that shows the problem, or a screenshot, into this box.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Security problem
url: https://github.com/ausboss/ComfyUI-AusBoss/security/policy
about: Report it privately instead of in a public issue.
+28
View File
@@ -0,0 +1,28 @@
name: Workflow idea
description: A workflow problem the pack could solve.
labels: [enhancement]
body:
- type: markdown
attributes:
value: >-
The pack admits nodes deliberately: each one solves a problem that
comes up again and again, in a compact node
([how a node gets added](https://github.com/ausboss/ComfyUI-AusBoss/blob/main/docs/adding_a_node.md)).
Start with the problem; the node can come later.
- type: textarea
id: problem
attributes:
label: The problem
description: What do you keep doing by hand, or with a pile of nodes?
validations:
required: true
- type: textarea
id: today
attributes:
label: How you handle it today
description: The nodes or steps you use now. A workflow .json or a screenshot helps.
- type: textarea
id: idea
attributes:
label: What would help
description: Optional. A new node, or a change to an existing one.
+7
View File
@@ -0,0 +1,7 @@
<!-- What does this change, and why? -->
- [ ] Offline checks run (the commands in CONTRIBUTING.md)
- [ ] No version bump: a `pyproject.toml` version change on `main` publishes a release
- [ ] No node inputs or outputs renamed or reordered: saved workflows depend on them
- [ ] Screenshots attached for UI changes
+160
View File
@@ -0,0 +1,160 @@
name: ComfyUI updates
# Every night the pack is run against the newest ComfyUI: its newest release
# and its development head. Nothing here needs a GPU or a model file.
#
# What runs, on a clean machine with ComfyUI installed for the CPU:
# 1. tests/test_comfy_core_contract.py: everything the pack takes from
# ComfyUI is still there (modules, functions, ComfyUI's own nodes the
# pack calls and the inputs it passes them).
# 2. All Python tests, with this ComfyUI on the path.
# 3. ComfyUI is started with the pack in it. scripts/compat_boot.py asks it
# which nodes it offers: every public node of the pack, and every node
# and required input the example workflows use.
# 4. scripts/dev/face_smoke.mjs puts every node on a graph in headless
# Chrome, in the classic node view and in Nodes 2.0: its face comes up
# and nothing throws.
#
# A failure on the release opens an issue (or adds to the open one) and fails
# the run. The development head breaks and mends itself often, so that job
# never fails the run, and it only opens an issue after two nights in a row.
#
# It also runs on a pull request that changes one of these checks.
on:
schedule:
- cron: "17 9 * * *"
workflow_dispatch:
pull_request:
branches: [main]
paths:
- ".github/workflows/comfyui_compat.yml"
- "tests/test_comfy_core_contract.py"
- "scripts/compat_boot.py"
- "scripts/dev/face_smoke.mjs"
permissions:
contents: read
concurrency:
group: comfyui-updates-${{ github.ref }}
cancel-in-progress: true
jobs:
compat:
name: ComfyUI ${{ matrix.comfyui }}
if: ${{ github.repository == 'ausboss/ComfyUI-AusBoss' }}
runs-on: ubuntu-latest
timeout-minutes: 45
continue-on-error: ${{ matrix.comfyui == 'master' }}
permissions:
contents: read
issues: write
actions: read
strategy:
fail-fast: false
matrix:
comfyui: [release, master]
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
steps:
- name: Check out the pack
uses: actions/checkout@v7
with:
path: ComfyUI-AusBoss
persist-credentials: false
- name: Which ComfyUI
id: which
env:
GH_TOKEN: ${{ github.token }}
WANTED: ${{ matrix.comfyui }}
run: |
if [ "$WANTED" = "release" ]; then
ref=$(gh api repos/Comfy-Org/ComfyUI/releases/latest --jq .tag_name)
else
ref=master
fi
test -n "$ref"
echo "ref=$ref" >> "$GITHUB_OUTPUT"
echo "ComfyUI at $ref"
- name: Check out ComfyUI
uses: actions/checkout@v7
with:
repository: Comfy-Org/ComfyUI
ref: ${{ steps.which.outputs.ref }}
path: ComfyUI
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ComfyUI/requirements.txt
- uses: actions/setup-node@v7
with:
node-version: "24"
- name: Install ComfyUI for the CPU
run: |
python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
python -m pip install -r ComfyUI/requirements.txt
git -C ComfyUI log -1 --format='ComfyUI %h %cd %s' --date=short
python -m pip list 2>/dev/null | grep -i -E "^(comfyui|torch) " || true
# Which CPU ran it: float results can differ by a last bit between CPUs.
lscpu | grep -E "^Model name" || true
- name: Everything the pack takes from ComfyUI is there
working-directory: ComfyUI-AusBoss
run: AUSBOSS_COMFY_ROOT="$GITHUB_WORKSPACE/ComfyUI" python tests/test_comfy_core_contract.py
- name: Python tests on this ComfyUI
working-directory: ComfyUI-AusBoss
run: AUSBOSS_COMFY_ROOT="$GITHUB_WORKSPACE/ComfyUI" python scripts/run_python_tests.py
- name: Start ComfyUI with the pack in it
run: |
ln -s "$GITHUB_WORKSPACE/ComfyUI-AusBoss" ComfyUI/custom_nodes/ComfyUI-AusBoss
cd ComfyUI
nohup python main.py --cpu --port 8188 --disable-auto-launch > "$GITHUB_WORKSPACE/comfyui.log" 2>&1 &
for i in $(seq 1 90); do
if curl -s -m 2 http://127.0.0.1:8188/system_stats > /dev/null; then echo "up after $((i * 2)) s"; exit 0; fi
sleep 2
done
echo "ComfyUI did not come up"; tail -60 "$GITHUB_WORKSPACE/comfyui.log"; exit 1
- name: Every node is offered and every example still fits
run: python ComfyUI-AusBoss/scripts/compat_boot.py --url http://127.0.0.1:8188 --log comfyui.log
- name: Every node's face comes up, classic and Nodes 2.0
run: |
google-chrome --headless=new --remote-debugging-port=9333 --no-sandbox --disable-gpu --disable-dev-shm-usage \
--user-data-dir="$RUNNER_TEMP/chrome" about:blank > "$GITHUB_WORKSPACE/chrome.log" 2>&1 &
for i in $(seq 1 30); do curl -s -m 2 http://127.0.0.1:9333/json/version > /dev/null && break; sleep 1; done
status=0
node ComfyUI-AusBoss/scripts/dev/face_smoke.mjs 9333 || status=1
node ComfyUI-AusBoss/scripts/dev/face_smoke.mjs 9333 --vue || status=1
exit "$status"
- name: ComfyUI's log
if: ${{ always() }}
run: tail -80 comfyui.log || true
# Only a nightly run tells anyone: a pull request shows its own result.
- name: Open or add to an issue
if: ${{ failure() && github.event_name == 'schedule' }}
env:
GH_TOKEN: ${{ github.token }}
WANTED: ${{ matrix.comfyui }}
REF: ${{ steps.which.outputs.ref }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
cd ComfyUI-AusBoss
if [ "$WANTED" = "master" ]; then
# The development head: only when last night's run failed on it too.
before=$(gh run list --workflow comfyui_compat.yml --event schedule --limit 2 --json databaseId --jq '.[1].databaseId // empty')
last=""
if [ -n "$before" ]; then
last=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$before/jobs" --jq '.jobs[] | select(.name == "ComfyUI master") | .steps[] | select(.conclusion == "failure") | .name' | head -1)
fi
if [ -z "$last" ]; then echo "First night it fails on the development head: no issue yet."; exit 0; fi
fi
title="ComfyUI $WANTED: the pack's nightly check fails"
commit=$(git -C ../ComfyUI log -1 --format='%h (%cd) %s' --date=short)
body=$(printf 'The nightly check of the pack against ComfyUI `%s` failed.\n\n- ComfyUI: %s\n- Run: %s\n\nThe failed step in that run names what broke: a module or function ComfyUI no longer has, a node or input an example uses, or a node whose face did not come up.' "$REF" "$commit" "$RUN_URL")
gh label create comfyui-update --description "A ComfyUI update broke something" --color 14424D 2>/dev/null || true
open=$(gh issue list --state open --label comfyui-update --search "in:title \"$title\"" --json number --jq '.[0].number // empty')
if [ -n "$open" ]; then
gh issue comment "$open" --body "$body"
else
gh issue create --title "$title" --label comfyui-update --body "$body"
fi
+119
View File
@@ -0,0 +1,119 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
# Publishes the pack to registry.comfy.org when a push to main changes the
# version in pyproject.toml, or on a manual run from main. Editing another
# pyproject field (description, keywords) without a new version publishes
# nothing, and the offline checks must pass before anything is uploaded.
# A successful upload also creates the version's tag and GitHub Release.
# Requires the repository secret REGISTRY_ACCESS_TOKEN, an API key created
# for the "ausboss" publisher at registry.comfy.org.
permissions:
contents: read
concurrency:
group: registry-publish
cancel-in-progress: false
jobs:
check:
name: Check version and validate
runs-on: ubuntu-latest
timeout-minutes: 10
if: ${{ github.repository == 'ausboss/ComfyUI-AusBoss' && github.ref == 'refs/heads/main' }}
outputs:
publish: ${{ steps.version.outputs.publish }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Did the version change?
id: version
env:
BEFORE: ${{ github.event.before }}
EVENT: ${{ github.event_name }}
run: |
read_version='import sys, tomllib; print(tomllib.loads(sys.stdin.read())["project"]["version"])'
current=$(python -c "$read_version" < pyproject.toml)
echo "version=$current" >> "$GITHUB_OUTPUT"
previous=""
if [ -n "$BEFORE" ] && git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
previous=$(git show "$BEFORE:pyproject.toml" | python -c "$read_version")
fi
if [ "$EVENT" = "workflow_dispatch" ] || [ "$current" != "$previous" ]; then
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "Publishing $current (was ${previous:-unknown})."
else
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "pyproject.toml changed but the version stayed $current; nothing to publish."
fi
- uses: actions/setup-node@v7
if: steps.version.outputs.publish == 'true'
with:
node-version: "24"
- name: Offline checks
if: steps.version.outputs.publish == 'true'
run: |
python scripts/validate_nodes.py
python scripts/release_preflight.py
node --test --test-isolation=none tests/*.test.mjs
publish-node:
name: Publish Custom Node to registry
needs: check
if: ${{ needs.check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
uses: actions/checkout@v7
# The Registry shows a version's notes under "More". comfy-cli takes
# them from COMFY_NODE_CHANGELOG, so this version's CHANGELOG entries
# are put there, one short line each, before the publish step runs.
- name: Notes for this version
run: python3 scripts/changelog_notes.py --github-env
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
github-release:
name: Create GitHub Release
needs: [check, publish-node]
if: ${{ needs.check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Publish release notes and tag
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
VERSION: ${{ needs.check.outputs.version }}
run: |
tag="v$VERSION"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists; leaving it unchanged."
exit 0
fi
python scripts/changelog_notes.py --version "$VERSION" --full > "$RUNNER_TEMP/release-notes.md"
gh release create "$tag" --target "$GITHUB_SHA" --title "$VERSION" \
--notes-file "$RUNNER_TEMP/release-notes.md" --latest
@@ -0,0 +1,82 @@
name: Publish Model Downloader addon
on:
workflow_dispatch:
inputs:
addon_commit:
description: Full commit SHA from ComfyUI-AusBoss-Model-Downloader main
required: true
type: string
changelog:
description: Release note for this version, shown in the Registry's Updates section
required: true
type: string
# The existing AusBoss publisher credential stays in this repository. This
# manual job publishes only the separately maintained Model Downloader addon.
# It does not release or change the main AusBoss node pack.
permissions:
contents: read
concurrency:
group: registry-publish-model-downloader
cancel-in-progress: false
jobs:
publish:
if: github.repository == 'ausboss/ComfyUI-AusBoss' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Require an exact addon commit
env:
ADDON_COMMIT: ${{ inputs.addon_commit }}
run: '[[ "$ADDON_COMMIT" =~ ^[0-9a-f]{40}$ ]]'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: ausboss/ComfyUI-AusBoss-Model-Downloader
ref: ${{ inputs.addon_commit }}
fetch-depth: 0
persist-credentials: false
- name: Require a commit merged into addon main
run: git merge-base --is-ancestor HEAD origin/main
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
with:
python-version: '3.12'
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: '24'
package-manager-cache: false
- name: Install validation and publishing tools
run: python -m pip install -r requirements-dev.txt comfy-cli==1.25.0
- name: Offline checks
run: |
python -m unittest discover -s tests -v
ruff check --select F,E9 __init__.py downloader tests scripts
node --test tests/*.test.mjs
node --check js/model_downloader/index.js
node --check js/shared/download_controls.mjs
- name: Build and inspect Registry package
run: |
comfy --skip-prompt --no-enable-telemetry node pack
python scripts/check_registry_package.py
- name: Refuse an existing immutable version
run: python scripts/registry_status.py --require-unpublished
- name: Publish addon
id: publish
env:
REGISTRY_ACCESS_TOKEN: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
COMFY_NODE_CHANGELOG: ${{ inputs.changelog }}
run: |
if [ -z "$REGISTRY_ACCESS_TOKEN" ]; then
echo '::error::REGISTRY_ACCESS_TOKEN is not configured.'
exit 1
fi
if [ -z "${COMFY_NODE_CHANGELOG// /}" ]; then
echo '::error::The changelog input is empty; the Registry shows it as the release note.'
exit 1
fi
comfy --skip-prompt --no-enable-telemetry node publish --token "$REGISTRY_ACCESS_TOKEN"
- name: Report Registry review status
if: always() && steps.publish.outcome != 'skipped'
run: python scripts/registry_status.py --report
+41
View File
@@ -0,0 +1,41 @@
name: Check Registry approval
on:
workflow_run:
workflows: ["Publish to Comfy registry"]
types: [completed]
workflow_dispatch:
inputs:
version:
description: "Version to check; blank uses pyproject.toml"
required: false
type: string
permissions:
contents: read
jobs:
registry-status:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
# Review happens after upload. Pending/Flagged are a successful status
# report, not approval; a ban, missing version or broken lookup still fails.
- name: Report exact published version's approval state
id: registry
env:
AUSBOSS_TARGET_VERSION: ${{ inputs.version }}
run: |
if [ -n "$AUSBOSS_TARGET_VERSION" ]; then
python scripts/registry_status.py --report --version "$AUSBOSS_TARGET_VERSION"
else
python scripts/registry_status.py --report
fi
+64
View File
@@ -0,0 +1,64 @@
name: Validate release candidate
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
offline-checks:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- uses: actions/setup-node@v7
with:
node-version: "24"
- run: python scripts/validate_nodes.py
- run: python scripts/release_preflight.py
# The test files that need only the standard library. The rest import
# torch, NumPy or PyAV and run in ComfyUI's own Python environment.
- name: Standard-library Python tests
run: >-
python scripts/run_python_tests.py
tests/test_changelog_notes.py
tests/test_execution_helpers.py
tests/test_layout_boxes_helpers.py
tests/test_lora_helpers.py
tests/test_math_helpers.py
tests/test_media_paths.py
tests/test_memory_helpers.py
tests/test_node_api.py
tests/test_registry_contract.py
tests/test_registry_status.py
tests/test_resolution.py
tests/test_status_helpers.py
tests/test_utility_nodes.py
tests/test_validate_inputs.py
tests/test_workflow_contract.py
tests/test_comfy_core_contract.py
# `node --check file.js` passes a broken file that uses import or
# export when no package.json marks it as a module, so each file goes
# in on stdin with the module type stated.
- name: JavaScript syntax
run: |
status=0
while IFS= read -r -d '' file; do
node --input-type=module --check < "$file" || { echo "::error file=$file::syntax error"; status=1; }
done < <(git ls-files -z -- 'js/*.js' '*.mjs')
exit "$status"
- run: node --test --test-isolation=none tests/*.test.mjs
- name: Ruff (pyflakes and syntax errors)
run: |
python -m pip install ruff==0.16.3
ruff check --select F,E9 .
+12 -5
View File
@@ -11,7 +11,7 @@ venv/
.DS_Store
Thumbs.db
# Local scratch — untracked experiments and node ports in progress
# Local scratch files
_scratch/
*.log
@@ -21,7 +21,14 @@ uv.lock
# User aspect-ratio presets - personal copy of ausboss_presets_example.json
ausboss_presets.json
# Local working notes
ausboss_test_checklist.md
ausboss_idea_backlog.md
.claude/skills/comfyui-node-api/SKILL.md
# Local notes at the repository root
/ausboss_*.md
# Local tool settings
/.agents/
# Local tool output: the archive `comfy node pack` writes, and per-machine
# worktrees and settings
node.zip
.claude/worktrees/
.claude/settings.local.json
+10 -132
View File
@@ -1,134 +1,12 @@
# ComfyUI-AusBoss — Agent Working Rules
# ComfyUI-AusBoss: start here
Instructions for any coding agent working in this repo. `CLAUDE.md` imports
this file, so keep everything here and leave that one as a pointer.
A pack of ComfyUI custom nodes by ausboss. Two kinds of agents read this.
A suite of polished ComfyUI custom nodes by ausboss. Public nodes must solve a
repeated workflow need, keep a compact graph footprint, and pass backend plus
browser acceptance before release.
## This repo is the showroom, not the workshop
Experiments live in the private **ComfyUI-AusBoss-Lab** repo, not here. Default
a new node to the lab and promote it later; adding it here is the deliberate
act of publishing it, and after a release its mapping key can never change.
A node belongs in the lab, not this repo, when any of these is true:
- it depends on a third-party pack or model most people will not have
- it encodes constants owned by someone else — a model card, an upstream
default — that go stale when they change them, leaving this pack shipping
wrong advice under ausboss's name
- it solved a need once rather than a repeated workflow need
- its inputs are still moving
The lab mirrors this repo's layout, so a promotion is a port, not a rewrite.
Its namespaces are held apart (`AUSBOSS_LAB_` ids, `🧪 AusBoss Lab/`
categories, `🧪` display suffix) so both packs install at once. The port steps
and the release gate are in the lab's `docs/promoting.md`.
`scripts/validate_nodes.py` enforces the boundary from this side: a mapping key
that is registered but absent from `PUBLIC_NODE_IDS` fails the build, so an
experiment cannot ride along into a release unnoticed.
## Hard rules
- Never modify `LICENSE`.
- Never bump `version` in `pyproject.toml` unless explicitly asked.
- Never add agent attribution to commits or PRs — no `Co-Authored-By`
trailers, no "generated with" footers. Commits are authored by ausboss alone.
- Do not read or analyze `__pycache__`, `.git`, or editor config directories.
- Ask before whole-repo sweeps or large refactors; propose a short plan first.
- Keep diffs minimal: touch only the lines the task needs.
- Be concise; skip pleasantries.
## Third-party independence
- Never copy third-party code, assets, fonts, icons, CSS, or documentation.
- Review ecosystem overlap before accepting a public node. Generic overlap is
fine, but implementation, naming, interaction design, and documentation must
be this repository's own work.
## Architecture
```text
__init__.py # NODE_MODULES list → importlib merge of all mappings.
# Fail-soft: a broken module logs and is skipped, the rest load.
nodes/
node_<name>.py # exactly one node (or one tight family) per file;
# exports NODE_CLASS_MAPPINGS + NODE_DISPLAY_NAME_MAPPINGS
_<topic>_helpers.py # shared backend logic, underscore prefix = not a node
js/
<name>/index.js # frontend entry per node or pack-wide feature, e.g.
# appearance/ (.js files auto-load)
shared/*.mjs # import-only shared modules (.mjs files do NOT auto-load)
docs/ # developer docs
scripts/ # offline checks, stdlib only. validate_nodes.py is the
# entry point; registry_contract.py holds the rules that
# keep nodes visible to registry scanners.
example_workflows/ # example workflows (regular workflow JSON, not API JSON)
```
## Conventions
- Public mapping keys use `AUSBOSS_NODES_<Purpose>`. The mapping key is the
workflow-compatibility contract and must never be renamed after release.
- Write those keys as **string literals** inside `NODE_CLASS_MAPPINGS` and
`NODE_DISPLAY_NAME_MAPPINGS` — never a `NODE_ID` variable. Registry scanners
(ComfyUI-Manager) AST-parse the source without importing it, so a variable
key makes every node invisible and "install missing custom nodes" stops
offering the pack. `scripts/validate_nodes.py` enforces this.
- Assign each mapping **once**, at module level, to a non-empty dict literal,
and never mention the name again — no `update()`, no `del`, no
`alias = NODE_CLASS_MAPPINGS`. A scanner reads that one literal and stops,
so anything done to the mapping afterwards is invisible to it. Both
mappings must carry exactly the same keys.
- Display name: `<Name> 🆎` — the emoji is the pack signature. Typing
"ausboss" still surfaces every node through the `🆎 AusBoss/<Group>`
category, the `AUSBOSS_NODES_` id prefix, and the "ausboss" entry every
node keeps in `SEARCH_ALIASES`.
- Category: `🆎 AusBoss/<Group>`. The emoji is safe here — categories reach
the frontend as JSON and are never printed to the console at import time.
- Every node gets `DESCRIPTION`, input `tooltip`s, and `OUTPUT_TOOLTIPS`.
- IMAGE tensors are BHWC float batches; MASK is BHW. Return tuples always,
even for one output: `(value,)`.
- Console output at import time must stay ASCII — ComfyUI on Windows often
runs a cp1252 console, and a UnicodeEncodeError there kills the whole pack.
- No new pip dependencies without an explicit decision; if truly optional,
use `[project.optional-dependencies]` and fail soft at runtime.
- Frontend JS never assigns prototype callbacks directly — use
`chainCallback` from `js/shared/index.mjs`.
- Frontend settings use `AusBoss.<Area>.<Name>` ids with
`category: ["🆎 AusBoss", "<Area>", "<Leaf>"]` and a distinct leaf per
setting. Node color schemes live in `js/shared/appearance.mjs`.
## Adding a node
Follow `docs/adding_a_node.md`. Short version: create `nodes/node_<name>.py`
from the template, add `"node_<name>"` to `NODE_MODULES` in `__init__.py`,
optionally add `js/<name>/index.js`, then validate. Brand and design-language
guidance (visual grammar, settings conventions, interaction etiquette) lives
in `.claude/skills/ausboss-node-brand/SKILL.md`.
## Validation
```bash
python scripts/validate_nodes.py
```
Then restart ComfyUI fully, watch the AusBoss banner for failed modules,
confirm the node appears in `GET http://127.0.0.1:8188/object_info`, queue a
tiny API graph, and load its example workflow. After JS changes, hard-refresh
the browser tab (Ctrl+Shift+R).
## Phase 2: porting an existing node
1. Drop the old file in `_scratch/` (gitignored) and read it fully first.
2. Rebuild the core compute in a clean `nodes/node_<name>.py` from the
template — port logic deliberately, don't paste wholesale.
3. Shared logic goes to `nodes/_<topic>_helpers.py`, not duplicated.
4. Frontend goes to `js/<name>/index.js`; reusable bits to `js/shared/`.
5. Keep the old class-name string as the mapping key only if existing saved
workflows must keep loading; otherwise use the `AUSBOSS_NODES_<Purpose>`
convention.
6. Run the validation steps above before calling it done.
- **Using the nodes in a workflow?** Read
[js/docs/agents/README.md](js/docs/agents/README.md): which node does which
job, where each node's help page is, and the example workflows. That folder
ships with the pack, so it is also in a ComfyUI install.
- **Changing the pack itself?** Read [CONTRIBUTING.md](CONTRIBUTING.md) in full
before the first edit. It holds the layout, the rules every node follows and
the checks to run. A new node follows
[docs/adding_a_node.md](docs/adding_a_node.md).
+2743
View File
File diff suppressed because it is too large Load Diff
+1 -8
View File
@@ -1,9 +1,2 @@
# CLAUDE.md
Working rules for this repo live in [AGENTS.md](AGENTS.md) — the tool-neutral
file every coding agent reads. Keep edits there, not here.
The line below is a Claude Code import: it pulls AGENTS.md into context
automatically, so nothing has to be duplicated between the two files.
@AGENTS.md
@CONTRIBUTING.md
+393
View File
@@ -0,0 +1,393 @@
# Contributing
Bug reports, workflow ideas and pull requests are welcome.
## Reporting a problem
The most useful report names:
- the ComfyUI and frontend versions (Settings → About),
- the pack version from the console banner (`AusBoss vX.Y.Z | N nodes loaded`),
- the node, what you expected, and what happened instead,
- the smallest workflow that shows it, plus a screenshot for anything visual.
Security problems go through [SECURITY.md](SECURITY.md) instead of a public
issue.
## Pull requests
- Read the contributor guide below first, and a new public node
follows [docs/adding_a_node.md](docs/adding_a_node.md). A new node is a
conversation before it is a pull request.
- Mapping keys, input names and order, and output order are a permanent API:
saved workflows depend on them. Append optional inputs and outputs instead
of renaming or reordering. `tests/test_node_api.py` checks this against a
snapshot; its docstring shows how to refresh the snapshot after a
compatible change.
- Leave `version` in `pyproject.toml` alone. A version change that reaches
`main` publishes to the Comfy Registry.
- Run the offline checks. None of them needs a GPU; the Python tests need
ComfyUI's Python environment.
```bash
python scripts/validate_nodes.py
python scripts/release_preflight.py
python scripts/run_python_tests.py
node --test tests/*.test.mjs
```
- Frontend changes are checked on a real canvas as well as in tests.
[docs/live_testing.md](docs/live_testing.md) describes the headless browser
harness and what to screenshot.
- A fix PR says what was wrong, adds a test that fails before the fix and
passes after it, and shows before/after results.
## Contributor guide
How the repo is laid out, the conventions every node follows, and the checks
to run. A suite of polished ComfyUI custom nodes by ausboss. Public nodes must
solve a repeated workflow need, keep a compact graph footprint, and pass
backend plus browser acceptance before release.
### READ FIRST: write for the people who use the nodes
Everything a user reads starts with the plainest version. That covers node
descriptions, tooltips, the `?` help pages (`js/docs/*.md`), the README,
example workflow notes, report and error text, and the CHANGELOG. Get more
technical later in the text, or in its own section.
- Open with what it does and when to use it, in everyday words and short
sentences. Someone new to ComfyUI should get it on the first read.
- A tooltip is one or two short sentences: what this is, and what to pick.
- Measurements, test results, edge cases and how it works go further down,
under a heading such as "Technical details".
- Keep jargon out of the opening (resample, warp, frame, affine,
estimator, canvas space). If a term is needed, say what it means the
first time.
- Put a number up top only when it helps someone decide what to do.
If a sentence needs a second read, rewrite it.
### Which nodes ship
A node ships only when its mapping key is listed in `PUBLIC_NODE_IDS` in
`scripts/validate_nodes.py`; the validator fails on any registered key that
is not listed there.
### Hard rules
- Never modify `LICENSE`.
- Never bump `version` in `pyproject.toml` — a version bump that lands on
main **publishes to the Comfy Registry automatically** (see Releasing).
- Keep diffs minimal: touch only the lines the task needs.
### Third-party independence
- Never copy third-party code, assets, fonts, icons, CSS, or documentation.
- Review ecosystem overlap before accepting a public node. Generic overlap is
fine, but implementation, naming, interaction design, and documentation must
be this repository's own work.
### Architecture
```text
__init__.py # NODE_MODULES list → importlib merge of all mappings.
# Fail-soft: a broken module logs and is skipped, the rest load.
nodes/
node_<name>.py # exactly one node (or one tight family) per file;
# exports NODE_CLASS_MAPPINGS + NODE_DISPLAY_NAME_MAPPINGS
_<topic>_helpers.py # shared backend logic, underscore prefix = not a node
js/
<name>/index.js # frontend entry per node or pack-wide feature, e.g.
# appearance/ (.js files auto-load)
shared/*.mjs # import-only shared modules (.mjs files do NOT auto-load)
docs/ # developer docs
scripts/ # offline checks in stdlib Python. validate_nodes.py is
# the entry point; registry_contract.py holds the rules
# that keep nodes visible to registry scanners;
# registry_status.py reads the Registry API; dev/ is the
# Node.js canvas harness (docs/live_testing.md).
example_workflows/ # example workflows (regular workflow JSON, not API JSON)
```
### Conventions
- Public mapping keys use `AUSBOSS_NODES_<Purpose>`. The mapping key is the
workflow-compatibility contract and must never be renamed after release.
- Inputs and outputs only grow after a release: append an input as
optional, append an output, and never rename, reorder, remove or newly
require one. Saved workflows keep widget values by position and links by
slot, and an API prompt must carry every required input.
`tests/test_node_api.py` holds the pack to `tests/fixtures/node_api.json`;
refresh that snapshot with the test's `--update` after a compatible change
or a new node.
- Appending an input to a node with a card or panel takes one more step.
Workflows saved by earlier releases end that node's values with an empty
value for each card and panel, and values come back by position, so the
new input opens holding that empty value. Give a widget input a
`resetUnknown` fallback in its card (`js/widget_cards/index.js`), or a
`RESET_UNKNOWN` beside `NODE_CLASS` in the node's own `js/<name>/index.js`
if it has no card (as LoRA Loader does). The fallback equals the node's
default, which is what an API prompt without the input runs with. Then
list the input in `tests/saved_widget_values.test.mjs`. Cards and
panels themselves are never saved: set `widget.serialize = false` right
after `addDOMWidget` (`options.serialize` only keeps a widget out of the
prompt).
- Write those keys as **string literals** inside `NODE_CLASS_MAPPINGS` and
`NODE_DISPLAY_NAME_MAPPINGS` — never a `NODE_ID` variable. Registry scanners
(ComfyUI-Manager) AST-parse the source without importing it, so a variable
key makes every node invisible and "install missing custom nodes" stops
offering the pack. `scripts/validate_nodes.py` enforces this.
- Assign each mapping **once**, at module level, to a non-empty dict literal,
and never mention the name again — no `update()`, no `del`, no
`alias = NODE_CLASS_MAPPINGS`. A scanner reads that one literal and stops,
so anything done to the mapping afterwards is invisible to it. Both
mappings must carry exactly the same keys.
- Display name: `<Name> 🆎` — the emoji is the pack signature. Typing
"ausboss" still surfaces every node through the `🆎 AusBoss/<Group>`
category, the `AUSBOSS_NODES_` id prefix, and the "ausboss" entry every
node keeps in `SEARCH_ALIASES`.
- Category: `🆎 AusBoss/<Group>`. The emoji is safe here — categories reach
the frontend as JSON and are never printed to the console at import time.
- Every node gets `DESCRIPTION`, input `tooltip`s, and `OUTPUT_TOOLTIPS`.
- IMAGE tensors are BHWC float batches; MASK is BHW. Return tuples always,
even for one output: `(value,)`.
- Console output at import time must stay ASCII — ComfyUI on Windows often
runs a cp1252 console, and a UnicodeEncodeError there kills the whole pack.
- Widget values and route parameters are attacker-controlled: ComfyUI's
`/prompt` and the pack's routes need no login. Media reads and writes stay
inside ComfyUI's input, output and temp folders - no opt-in switches, no
"any folder" settings, no folder pickers. A file is picked from a list and
named below one of those folders the way ComfyUI names it
(`clip.mp4 [output]`); no input opens a whole path as given.
A route answers with data it made (JSON, a small picture), never a file by
its path (`web.FileResponse`), and its error text goes through
`safe_route_error` (`nodes/_route_errors.py`). Besides those the pack only reads
its registered model folders (the one write there is LoRA Loader's Civitai
button: a `.civitai.info` file and a `.preview.jpg` picture, saved beside a
LoRA, never replacing a picture) and keeps its own
settings files in ComfyUI's user folder (`user/ausboss/`). Nothing is handed to a
subprocess. The Registry bans versions for exactly this.
- The pack's server code makes no network requests: no HTTP or socket client
in shipped Python (`release_preflight.py` fails on one). The one outbound
call is LoRA Loader's click-only Civitai lookup, made by the browser to a
fixed civitai.com address with a plain SHA256, plus the LoRA's picture
from `image.civitai.com` on the same click (`js/lora_loader/index.js`,
`js/shared/civitai_lookup.mjs`). `release_preflight.py` allows a raw
`fetch(` only there and in `js/cache_guard`, which only talks to the pack's
own server. Graph links are made with
`linkSlots` from `js/shared/graph_links.mjs`, never the index-based
`node.connect(...)`, which registry scans read as a network socket.
SECURITY.md states the model for users.
- No new pip dependencies without an explicit decision; if truly optional,
use `[project.optional-dependencies]` and fail soft at runtime.
- Frontend files import each other only with plain string literals
(`from "../shared/x.mjs"`, `import("../shared/x.mjs")`). The server adds a
version tag to those paths (`nodes/_frontend_cache.py`) so an update never
meets old copies from the browser's cache. A path built in code
(`new URL(...)`, `"../shared/" + name`) escapes the tag and loads a second,
stale copy; `tests/test_frontend_cache.py` fails on one.
- Frontend JS never assigns prototype callbacks directly — use
`chainCallback` from `js/shared/index.mjs`, or `chainHandler` where the
return value matters (a truthy `onMouseDown` result is what stops a node
drag). Messages to the user go through `showToast`, never `alert()`.
- The mouse wheel over a node zooms the graph, on every part of its face.
`js/canvas_passthrough/` hands the wheel on from the pack's panels, and
keeps it only for a box that can still scroll or a field being typed in,
so a panel adds no wheel listener of its own
(`tests/canvas_passthrough.test.mjs` lists the few that exist). After a
change to a node face, `scripts/dev/wheel_sweep.mjs` turns the wheel at
every point of every node and must find no dead spot, in both renderers.
- For compact panel actions and preview switches, use `compactAction` and
`panelSwitch` from `js/shared/panel_controls.mjs`. That module owns their
appearance, section caption styles, and common file and save labels. A
node styles its layout locally; leave the shared control's face to the
component. Its selectors only match explicitly named AusBoss controls.
- Every INT and FLOAT a public node exposes reaches the user as a scrub
control, Adobe-style: drag the value to scrub, click to type, chevron
arrows step, Shift is always the fine step. Use `makeScrubInput` from
`js/shared/scrub_input.mjs` — never a bare `<input type=number>`, and
never a classic canvas number widget on a finished node face. Units
(`px`, `MP`, `×`) ride inside the box in a fixed-width slot that every
single-field row reserves, so the numbers down a card share one centre
line. Known, deliberate exceptions: the Seed card's seed (typed) and Load
Video's trim timecodes (typed).
- **Node faces are widget cards.** A node whose face would be classic
LiteGraph widgets (full-width rows with an arrow at each end) gets an
entry in `js/widget_cards/index.js` instead: `mountWidgetCard` from
`js/shared/widget_card.mjs` hides the standard widgets and mirrors them
in one compact DOM card — numbers → scrub, ≤ 4 short choices → segmented
pill (else a select), booleans → an off | on pill as wide as the other
controls (never a small switch inside a card), strings → text field,
multiline → `kind: "textarea"` (`grow: true` takes the node's spare
height), hex colors → swatch. Rows can depend on other values (`when`)
and fold behind a disclosure (`group`). The widgets underneath stay the
single source of truth (save/load, undo, API, links).
- **One linkable widget per row.** The frontend (≥ 1.10.4, "Widget Input
Socket" RFC #9) gives every widget an input socket drawn at the widget's
own row, only while a link is dragged, hovered or connected; "convert to
input" no longer exists. The card lends each hidden widget its row's
position, so two widgets on one row would stack two sockets on one pixel
and nobody could aim. A `pair` row is therefore only allowed with
`top: true`, which lifts its sockets into the node's slot column under
the real inputs (Image Resize width/height); everything else is one
widget per row. A value that is meant to be wired rather than typed is a
backend `forceInput: True` socket (Math Expression `a`/`b`/`c`), and a
multi-type string such as `"FLOAT,INT"` accepts either kind of link.
- Preview-carrying nodes (Select Frame, Mask Refine, Mask by Name, LaMa
Inpaint, Save Image) share
`js/input_preview/`: a thin bar (node tools left, a small `preview`
switch right) above the picture, the picture gone and the node shorter
when the switch is off, backed by an optional `preview` BOOLEAN input
that also skips the temp file. A new node with a result picture reuses
it rather than growing its own.
- A DOM panel that shows a stage/preview claims the node's free height via
`fillNodeHeight` from `js/shared/panel_layout.mjs` — never a hand-rolled
`computeSize`, which pins the panel and leaves dead space when the node is
dragged taller. `tests/panel_guards.test.mjs` enforces this pack-wide: a
new panel entry must be added to its `mustGrow` set (or `fixedByDesign`
for genuinely constant-height rows), so the choice is always explicit.
A `fixedByDesign` card also calls `pinVuePanelHeight`: its own
`computeSize` only pins it in the classic renderer, and Nodes 2.0 would
stretch it with the node and squeeze a picture under it. Run Timer does
not: its readout fills the node on purpose.
- **A picture or clip in a stage sits out of flow.** Nodes 2.0 sizes a node
by its content, so an in-flow portrait clip drawn at full width held Load
Video at 965 px where classic goes to 584 px. Position the media
`absolute` in its stage, and the stage takes its size from the node
alone. A picture is centred at its own size (`js/input_preview/`); a
player fills the stage so its controls stay along the bottom edge
(`js/shared/video_ui.mjs`). Text that is fitted to its box follows the
same rule: the Callout's text is `absolute` in its card
(`js/callout/index.js`), or the text sizes the card it is fitted to.
`tests/panel_guards.test.mjs` holds all three.
- **A panel's declared minimum width is its node's real floor.** Pass
`exactMinWidth: true` to `fillNodeHeight`; a constant-height card that
keeps its own `computeSize` calls `holdNodeMinWidth` instead. Left alone,
the frontend adds 104 px to a growing panel's number and ignores a pinned
card's number altogether (the node falls to its 210 px default), so a
declared 320 meant 424 for one and 210 for the other. Prove each face on
a real canvas at that width in both renderers; a panel that clips there
gets a larger minimum, not a broken face. Nodes 2.0 never goes under
225 px whatever a panel declares.
- **A growing panel's declared minimum height holds in Nodes 2.0 too.**
Nodes 2.0 lays a node out by its content, not by the floor a widget
declares, so a panel whose content has no height of its own was dragged
flat there. `fillNodeHeight` also writes the floor to the panel's
element as a CSS `min-height`, less `WIDGET_FRAME`, while the panel sits
inside a Nodes 2.0 node. So declare `minHeight` as what the element needs
plus `WIDGET_FRAME`, and never assign a growing panel's
`computeLayoutSize` by hand. Prove the floor with a real corner drag up
in both renderers.
- Frontend settings use `AusBoss.<Area>.<Name>` ids with
`category: ["🆎 AusBoss", "<Area>", "<Leaf>"]` and a distinct leaf per
setting. Node color schemes live in `js/shared/appearance.mjs`.
### Adding a node
Follow `docs/adding_a_node.md`. Short version: create `nodes/node_<name>.py`
modelled on a small existing node such as `nodes/node_image_size.py`, add
`"node_<name>"` to `NODE_MODULES` in `__init__.py` and the key to
`PUBLIC_NODE_IDS` in `scripts/validate_nodes.py`, give it a help page at
`js/docs/<KEY>.md`, optionally add `js/<name>/index.js`, then validate.
### Validation
Run the offline checks before a pull request:
```bash
python scripts/validate_nodes.py
python scripts/release_preflight.py
python scripts/run_python_tests.py # with ComfyUI's Python
node --test tests/*.test.mjs
```
Then restart ComfyUI fully (a changed `INPUT_TYPES` is only served after a
restart), watch the AusBoss banner for failed modules, confirm the node
appears in `GET http://127.0.0.1:8188/object_info`, queue a tiny API graph,
and load its example workflow. After JS changes, hard-refresh the browser
tab (Ctrl+Shift+R) — the frontend caches `.mjs` modules aggressively.
Frontend work is proven on a real canvas, not by reading code: the
headless-Chrome harness in `scripts/dev/` (recipe, gotchas and the
screenshot convention in `docs/live_testing.md`) creates nodes, drives real
mouse drags, and screenshots the result. A link-drop test that only checks
which input got the link is not enough — look at the picture and ask whether
a person could have aimed there.
A ComfyUI update can break a node without a line of the pack changing. So
the pack is run against the newest ComfyUI every night
(`.github/workflows/comfyui_compat.yml`): its newest release and its
development head, on the CPU, with no model files. Three checks, which also
run by hand against any ComfyUI:
```bash
AUSBOSS_COMFY_ROOT=<ComfyUI> python tests/test_comfy_core_contract.py
python scripts/compat_boot.py --url http://127.0.0.1:8188
node scripts/dev/face_smoke.mjs <chrome port> # and again with --vue
```
- `test_comfy_core_contract.py` reads the pack's own source for everything
it takes from ComfyUI and looks each thing up in the real one. It finds
imports by itself. A call to one of ComfyUI's own nodes by name
(`SAM3_Detect`), or an internal read with a fallback, goes in the tables at
the top of that file; the test fails on a node call that is not listed.
- `compat_boot.py` asks a running ComfyUI which nodes it offers: every public
node of the pack, and every node and required input the examples use.
- `face_smoke.mjs` puts every node on a graph and fails when a face does not
come up or something throws.
A failure on the release opens an issue labelled `comfyui-update`. What these
cannot see is anything that needs a model: run the real thing on a GPU
before a release that follows a ComfyUI update.
### Releasing
A new `version` in `pyproject.toml` that lands on main publishes to the Comfy
Registry through the publish workflow (`.github/workflows/publish_action.yml`).
After a successful upload, the same workflow tags that commit and creates a
GitHub Release with the full changelog section. The README badge links to the
latest GitHub Release. If only that step fails, rerun the failed jobs to retry
it without uploading the Registry version again. Existing releases are left
unchanged on a rerun. Contributors never change `version`.
For an authorized release, update both `pyproject.toml` and
`AUSBOSS_JS_VERSION`, date the changelog section, then run
`python scripts/release_badge.py`. Preflight rejects a badge that does not
match the pack version. Its SVG lives in the repository so the release
number stays visible when an external badge service is unavailable.
The Registry shows notes with each version. They are that version's section
of `CHANGELOG.md`, one line per entry: its bold opening, and for a new node
the sentence that says what it does (`scripts/changelog_notes.py`). So open
every entry with a bold line that makes sense on its own.
### Example workflows
Examples are what most people run first, so each one must work for a
stranger with only the files its Workflow Note lists.
- Every model loader selects the official file name, the basename of its
download URL, carries that file in `properties.models`
(`name`, `url`, `directory`), and the Note's model row names the same
file. People who keep models in subfolders pick their copy.
- Media loaders (Load Image, Load Image + Pad, Load Video, Image Crop +
Rotate + Pad and the video nodes) are saved blank. A file name is one the
stranger doesn't have, and ComfyUI flags that node red when the workflow
loads. The Workflow Note tells people to load their own picture; the
sample files stay in `example_workflows/inputs/` for anyone who wants to
try one.
- Save nodes use a filename prefix without a folder. Seeds are fixed.
- Numbered groups hold every node, nothing overlaps (title bars included),
the saved zoom is at least 0.6 so widget text draws, the workflow `id` is
a real uuid4, and the `.jpg` beside the `.json` shows a real output.
- The frontend saves widget values twice, by position and by name
(`widgets_values_named`), and can restore by name; both copies must agree.
- `scripts/workflow_contract.py` checks links, groups, overlaps, the note,
the id, the zoom, the named copies, the download info and the save
prefixes. Whether the note tells the truth about the graph is review.
- Text-to-image examples have no source to describe. Edit and inpaint
examples keep an explicit change instruction: a description of the old
scene is not an edit. MiniMax H3 and Klein caption their sources with
Qwen3-VL 8B because their own text encoders returned only punctuation
when asked to describe an image.
+456 -151
View File
@@ -1,189 +1,494 @@
<div align="center">
<h1>ComfyUI-AusBoss</h1>
<p><strong>Polished nodes for the workflows I use most.</strong></p>
<p>ComfyUI nodes for image and video work.</p>
<p>
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License">
<img src="https://img.shields.io/badge/release-1.0.0-00b4aa?style=flat-square" alt="Release 1.0.0">
<!-- Generated by scripts/release_badge.py. Release preflight checks
the badge against pyproject.toml so it cannot silently go stale. -->
<a href="https://github.com/ausboss/ComfyUI-AusBoss/releases/latest"><img src="assets/readme/release.svg" alt="Latest release"></a>
<a href="https://www.youtube.com/@ausbossdev"><img src="https://img.shields.io/badge/YouTube-%40ausbossdev-00b4aa?style=flat-square&logo=youtube&logoColor=white" alt="YouTube @ausbossdev"></a>
<a href="https://discord.gg/RwVxY9Fuaj"><img src="https://img.shields.io/badge/Discord-AusBoss-00b4aa?style=flat-square&logo=discord&logoColor=white" alt="Discord: AusBoss"></a>
<a href="https://huggingface.co/ausboss"><img src="https://img.shields.io/badge/Hugging%20Face-ausboss-00b4aa?style=flat-square&logo=huggingface&logoColor=white" alt="Hugging Face: ausboss"></a>
<a href="https://x.com/Zanzibased"><img src="https://img.shields.io/badge/X-%40Zanzibased-00b4aa?style=flat-square&logo=x&logoColor=white" alt="X @Zanzibased"></a>
</p>
</div>
ComfyUI-AusBoss is a curated collection of polished nodes designed to streamline the ComfyUI workflows I use most. Each node replaces repetitive setup with focused controls, compact graph footprints, and only the outputs needed downstream. Larger visual tasks open into dedicated full-screen editors, while experiments and one-off utilities stay out of the public pack.
I made these nodes for my own ComfyUI workflows, mostly because I wanted features I couldn't find in other packs. I use them myself, and I keep adding new ones.
## Nodes
Most of them are for image and video work. ComfyUI's core nodes already cover the basics, like seeds, sizes and prompts, so use these where they add something. A few of the things they do:
### Image Crop + Rotate + Pad 🆎
- **[Mask by Touch](#mask-by-touch):** click a thing on the picture and get its mask. Hover shows what a click would take, and **Fine edges** follows hair and fur.
- **[Mask by Name](#mask-by-name):** type "the red jacket" and get its mask and a cut-out.
- **[Image Crop + Rotate + Pad](#image-crop--rotate--pad):** turn, crop and pad a picture by dragging handles on the node, for outpainting.
- **[Stitch Inpaint](#stitch-inpaint):** after an inpaint or outpaint, everything you didn't ask to change is your original pixels again.
- **[Video Crop + Rotate + Pad → Clip](#video-crop--rotate--pad--clip):** trim a clip on a timeline, and pad or crop it to a new shape.
- **[Tiled Upscale](#tiled-upscale):** upscale a picture in tiles to the megapixels you set.
- **[Image Folder](#image-folder):** tick pictures in a folder and run the workflow on each one.
- **[Layout Boxes](#layout-boxes):** draw boxes where things and words go in the picture, for MOSS-Image.
- **[Image Compare A/B](#image-compare-ab):** a before-and-after slider, and a brush to mix the two.
- **[LoRA Loader](#lora-loader):** a whole LoRA stack in one node, each row with its own switch and strength.
- **[Workflow Switches](#workflow-switches):** turn parts of a workflow off and on from one small card.
A Load Image-style node with a compact transformed preview and a full-screen editor for precise crop, rotation, padding, fill, mask feathering, and output-size alignment. The preview is itself a live stage — drag the crop handles, padding diamonds, and rotate knob directly on the node, and open the editor when you want zoom, pan, and the numeric sidebar.
### Video Crop + Rotate + Pad 🆎
Targets an exact video frame from an uploaded input file or local path. The full-screen editor adds a long timeline, playback, exact frame steps, and the same transform controls as the image node.
### Select Frame 🆎
Returns one unchanged frame from an `IMAGE` batch using a clear one-based frame number. Out-of-range requests report the valid range instead of silently selecting the wrong image.
### Frame Chooser 🆎
Pauses the graph and shows the incoming batch as a clickable filmstrip: pick the frames to keep, or Keep all / Cancel — with digits, `A`/`N`, `Enter` and `Escape` on the keyboard. A countdown can auto-answer with the policy you choose, a reload mid-pause restores the panel, and filling `pick_list` skips the pause entirely so a chosen take reruns headlessly (your interactive picks are written back there for you). Outputs the kept sub-batch, its count, and the one-based indices.
### LoRA Loader 🆎
A stacked multi-LoRA node. Each row has an on/off pill, a searchable picker (type to filter, arrow keys + Enter to pick; browse view groups by folder and hovering shows the preview image), and strengths you can **drag left/right to scrub** (Shift for fine steps) or click to type. A header pill toggles the whole stack. The per-row info card shows the LoRA's preview image, base model, trigger words from its file metadata, a one-click Civitai lookup, or your own saved words — click words to toggle them into the deduplicated `trigger_words` output — plus an optional suggested strength range that tints out-of-range values. The bar's ▤ button saves and applies named templates of the whole stack, and a LoRA trained for a different base model than the connected checkpoint logs a console warning. CLIP input is optional.
### Align Image 🆎
Snaps width and height to a clean multiple of a number you pick — 16, 32, whatever the model wants. Qwen image models, VAEs, and many samplers behave best on cleanly divisible sizes. `resize` rescales to the nearest multiple, `crop` center-crops down, `pad` replicate-pads up, and the new size comes out as INTs for wiring into latent nodes — plus `offset_x`/`offset_y` for cropping back after sampling.
### Image Size 🆎
Reads an image's dimensions as INTs — width, height, longest edge, shortest edge — so resizes and latents key off the actual image instead of retyped numbers that drift.
### LM Studio Chat 🆎
Sends a prompt (and optionally an image) to a local LM Studio server — or any OpenAI-compatible endpoint — and returns the reply as text. Empty model name means "whatever the server has loaded", reasoning-model `<think>` blocks come out on a separate output so the text stays clean for conditioning, and the seed doubles as the re-roll knob. Chain the `history` output into another chat node's `history` input for multi-turn conversations, or fill `json_schema` to force a structured JSON reply. Errors are actionable: a refused connection says how to start the server.
### Color Match 🆎
Harmonizes an image against a reference by transferring per-channel LAB mean and standard deviation — the fix for an inpainted or upscaled region that comes back slightly brighter or cooler than the plate it sits in. Optional mask restricts the correction to just that region, and `strength` blends it back toward the original. For video, `reference_mode: first_frame` locks every frame to the batch's own first frame — the one-node flicker fix.
### Pad Image 🆎
Pads an image with a solid color, replicated edges, replicated edge pixels, or a **pillarbox blur** (a blurred, dimmed copy of the frame behind the sharp original — the standard look for reframing video to a new aspect). Also returns a mask covering exactly the new padding, ready to wire straight into an outpaint. The stage on the node is the control: drag any edge of the final rect to set that side's padding over a live preview of the input (run once to fill it in).
### Load Image + Pad 🆎
A Load Image that opens straight into an outpaint canvas: drag any edge of the final rect drawn on the node to grow that side's padding — the whole edge is the handle, per-side pixel counts ride the bands, and the badge always shows the true output size. The mask covers exactly the padding with an optional **feather** ramped inward across the seam, the canvas rounds up to a clean multiple, and a **megapixel target** rescales the source *before* padding so the mask seam stays crisp at sampler-friendly sizes. Outputs the canvas, the mask, and the final width/height as INTs.
### Drop Shadow 🆎
Drops a soft shadow behind a masked subject with signed offset, grow, blur, color, and opacity — what sells a padded or reframed composition as deliberate. A multiply blend mode keeps the backdrop's texture, and the `shadow_mask` output hands the effective shadow alpha to downstream compositing.
### Frame Interpolate 🆎
Retimes a clip by **frames per second rather than a whole-number multiple**, so 24 to 30 works as naturally as doubling. Choose a fast `blend` crossfade or optical flow for real motion. Hard cuts are detected and held rather than interpolated across, avoiding the smeared morph other interpolators produce at a scene change. Memory is bounded by planning the work first and streaming results back to the CPU.
### LaMa Inpaint 🆎
Inpaints white mask regions with a TorchScript LaMa checkpoint, preserves pixels where the mask is zero, and processes large video batches one frame at a time to keep VRAM bounded. Place `big-lama.pt` in `ComfyUI/models/lama/`; the node never performs automatic downloads.
### Crop For Inpaint / Stitch Inpaint 🆎
Inpaint only where it matters: Crop For Inpaint grows the mask's bounding box by a context factor and emits the crop, the raw sampling mask, and a stitcher; Stitch Inpaint pastes the result back with a separately feathered blend so every pixel outside the blend region stays **bit-identical** to the original. The stitcher broadcasts across a video batch, so one crop serves a whole clip. An optional `fix_edge_halo` toggle removes the rim that appears when a feathered seam gets blended twice. `target_megapixels` rescales the crop to a sampler-friendly area with a selectable `rescale_algorithm`, and the `extend_*` inputs grow the frame itself for outpainting — the new bands become part of the stitched output.
### Load Video 🆎
Loads a video as frames plus audio with `frame_count`, `fps`, `width`, `height`, and `duration` outputs, plus a lazy core `VIDEO` handle so the clip chains straight into ComfyUI's own video nodes. Its single responsive player includes a two-handle trim timeline: drag **IN** and **OUT** (shown as `h:mm:ss.s` timecodes you can type into), preview exactly that window, and optionally loop it. Only the selected window is decoded, with a memory guard that reports oversized trims instead of exhausting RAM; audio is extracted lazily only when consumed. Optional custom width/height with aspect-preserving single-side mode; `every_nth` thins long clips (the fps output divides to match) and `max_frames` caps the decode outright.
### Refine Mask 🆎
Grows or shrinks a mask, fills enclosed holes, melts jagged edges with a `smooth` control that does not feather, feathers, and rescales the extremes with black/white points — returning both the refined mask and its inverse. Optional `guided filter` and `matting` tiers refine the edge against a guide image when you install the matching extra.
### Save Video 🆎
Encodes frames — or a connected core `VIDEO` — to an H.264/H.265 mp4 or VP9 webm (all CRF-controlled) tagged bt709 (so platforms don't shift your colors), with optional muxed audio and the workflow embedded, so a saved mp4 dragged back onto the canvas restores its workflow. The responsive in-node player previews the encoded result with loop and reload controls. Wire `fps` from Load Video to preserve source timing; a connected video brings its own rate and overrides the widget. Encoding runs off the executor thread with per-frame progress, so a long export never freezes the UI.
### Compare 🆎
A/B any two images: slide mode sweeps B over A under your pointer with a seam line, hold mode shows B while pressed. Passes image A through, so it sits mid-graph without rewiring.
The two transform nodes return exactly:
1. `image` — transformed ComfyUI `IMAGE`
2. `mask` — generated-area `MASK`, including source transparency, rotation corners, and padding
The operation order is always **rotate → crop → pad**. New sources start with zero rotation, full crop, zero padding, and a canvas multiple of `1`.
The **?** in a classic node's title bar opens a short card with its inputs and outputs. In Nodes 2.0, right-click the node and choose **About this node**. Every node also has a longer help page in ComfyUI's node help panel. I post the workflows I make with them on [Civitai](https://civitai.com/user/AusBoss) and [X @Zanzibased](https://x.com/Zanzibased), and videos of them on [YouTube @ausbossdev](https://www.youtube.com/@ausbossdev). Can't open Civitai? All my workflows are on [my Discord](https://discord.gg/RwVxY9Fuaj) too. If a node saves you time, a star on the repo helps other people find it.
## Install
In **ComfyUI-Manager**, open the Custom Nodes Manager, search for **AusBoss** and install it. From the command line, `comfy node install ausboss-nodes` does the same. To get the newest changes first, clone the repository instead:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/ausboss/ComfyUI-AusBoss.git
```
Restart ComfyUI, then search for `AusBoss`. There are no additional Python dependencies; the pack uses Pillow, NumPy, Torch, and PyAV already distributed with ComfyUI. LaMa Inpaint additionally needs a TorchScript `big-lama.pt` checkpoint in `ComfyUI/models/lama/`.
Restart ComfyUI and search the node library for **AusBoss**.
## Model Links
Install it one way, not both. Manager puts the pack in `custom_nodes/ausboss-nodes` and a clone in `custom_nodes/ComfyUI-AusBoss`. They are the same pack, so if both folders are there, delete one. The console says which copy it skipped.
**lama**
After an update, through Manager or `git pull`, restart ComfyUI and hard-refresh the browser with **Ctrl+Shift+R**. Otherwise the browser keeps the old JavaScript, and the pack warns you about it. Manager can install a version or two behind while the Comfy Registry reviews a new release. To get the newest one, pick it in Manager's version list, or update by hand: delete the pack's folder in `ComfyUI/custom_nodes` and clone the repository as shown above.
- [big-lama.pt](https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt)
The pack uses the Pillow, NumPy, Torch and PyAV that come with ComfyUI. Each example lists the models it needs on its Workflow Note. LaMa Inpaint also needs [`big-lama.pt`](https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt) in `ComfyUI/models/lama/`. The pack never downloads weights by itself.
## Model Storage Location
### Upgrading from 1.x
```text
📂 ComfyUI/
└── 📂 models/
└── 📂 lama/
└── big-lama.pt
2.0 removed **LM Studio Chat** (use **Text** for a fixed prompt) and Align Image's `offset_x` and `offset_y` outputs. Math Expression's `a`, `b` and `c` are sockets now instead of typed boxes, so put constants in the expression itself. 1.2 removed **Drop Shadow**, **Pad Image** (use **Load Image + Pad**) and **Frame Chooser**. Save Image writes only inside ComfyUI's output folder. The [changelog](CHANGELOG.md) has the details.
| Section | What's in it |
|---|---|
| [Image nodes](#image-nodes) | Rotate, crop and pad; resize, align and measure; upscale in tiles; run a whole folder; match colors; realign edits; compare; save |
| [Video nodes](#video-nodes) | Load and trim, pick a frame, transform a clip, change the frame rate, save |
| [Mask and inpaint nodes](#mask-and-inpaint-nodes) | Mask a thing by clicking it or typing its name, refine masks, remove objects with LaMa, crop around a mask and stitch the result back |
| [Models and conditioning](#models-and-conditioning) | LoRA stack, Krea 2 prompts and references |
| [Workflow utilities](#workflow-utilities) | Canvas size, seed, batches, math, text, memory, notes, callouts, switches, timer, step preview |
| [Example workflows](#example-workflows) | Ready-made workflows, each with a setup card |
| [For workflow creators](#for-workflow-creators) | Setup cards, before and after, repeatable seeds, measured run times |
| [Pack-wide tools](#pack-wide-tools) | Help cards, node colors, snap into line, recreate and replace, run status, completion sound |
## Image nodes
### Image Crop + Rotate + Pad
Rotate, crop and pad a picture by dragging handles on the node. Cyan squares crop, orange diamonds pad, and the green handle rotates. It rotates first, then crops, then pads. The full-screen editor adds exact sizes, zoom and pan.
Open **Crop, pad & resize** under the preview and tap a ratio to pad the picture to it, or to crop it with **Fit** set to crop. A lit ratio is the shape the canvas has right now: drag a handle to another shape and it goes dark, and the row says Custom. The padlock at the end of the row keeps the shape while you drag, and the button at the start turns the shape between portrait and landscape. **Reset crop** brings back the whole picture, and **Divisible by** makes the width and height divide evenly by a number such as 8, 16 or 32, for models that need it. Feather and output resizing are on the node and in the editor.
It returns the `image`, a `mask` of everything the model paints (padding, see-through parts of the source and the corners a rotation opens up), a `stitcher`, the untouched `original`, the output `width` and `height`, and a `prompt_image`. Wire the stitcher into **Stitch Inpaint** after the sampler to put your original back. See-through parts of a PNG, such as a cutout's missing background, are filled like the padding and painted; `prompt_image` and `original` show them as white. Give `prompt_image` to whatever writes your prompt, so it describes a real backdrop instead of the gray fill. **Load Image + Pad** has more fill choices and a separate reference output.
![The full-screen image editor with a rotated lake photo, cyan crop handles, orange padding handles, aspect ratios, and dimension controls.](assets/readme/image-editor.webp)
![A night photo of a woman leaning out of a car window is turned and padded, and Krea 2 fills the new space with more of the street.](assets/readme/rotate-outpaint.gif)
Krea 2 Rotate + Outpaint turns a photo, pads it and paints the new area in one pass. [Try the Krea 2 Rotate + Outpaint example →](example_workflows/Krea%202%20Rotate%20%2B%20Outpaint%20%28AusBoss%29.json)
### Load Image + Pad
Makes a canvas for outpainting: drag the edges out to add room around a picture. The Source list shows the picture under the pointer. The new area can be a solid color, the average color of the nearest edge, the edge pixels smeared outward, a blurred copy of the picture, or a mirror of it. The card also has a seam feather, a canvas multiple and a megapixel budget, and **Exact padding** opens one control per side. A budget of 0 keeps the picture's size, and anything higher resizes the picture before padding. A picture wired into `source_image` is padded instead of the chosen file.
It returns the padded `image`, a `mask` of the padding, `width`, `height`, a `stitcher`, and a smaller unpadded `reference` for the model. Wire the stitcher into **Stitch Inpaint** after the sampler, and everything outside the seam stays your original picture.
The Klein 9B Outpaint example pads its picture with Load Image + Pad. [Open the Klein 9B Outpaint workflow →](example_workflows/Klein%209B%20Outpaint%20%28AusBoss%29.json)
### Image Folder
Pick a folder, tick the pictures you want in a thumbnail gallery, and run the workflow on each one. **All in one run** does every picked picture with one press of Run. **One per run** loads one picture each run and steps to the next by itself, so a picture that fails only skips itself. After the last picture it stops with a message, or starts over at the first and keeps going. It gives each picture's mask, its file name for saving the result under the same name, its number and how many there are.
**Browse** lists the folders inside ComfyUI's input or output folder. A folder from anywhere else on your computer is brought in: drop it on the node, or choose it in your file explorer, and its pictures are copied into the input folder. A really big folder asks first. The node reads nothing outside those two folders.
### Image Resize
Resizes to a width and height, a longest or shortest edge, a megapixel count, or a scale factor. It can fit inside the size, stretch to it, fill it and crop the rest, or pad with a color you pick. New nodes start in megapixels mode, and saved workflows keep their mode. The card shows only the controls for the chosen mode, and width and height each take their own link.
An optional mask is resized the same way. It returns `image`, `mask`, `width` and `height`. Padding shows up white in the mask, and with no mask wired in, the rest of the mask is black. `divisible_by` rounds the size to a multiple, and `interpolation` picks the resize filter.
### Tiled Upscale
Sets the size of the finished picture in megapixels and cuts your picture into tiles a model can redraw one at a time. A picture that fits in one tile stays whole, and a small picture is not redrawn far past the detail it has. Send the tiles through your sampler, then into Tiled Upscale Stitch with the stitcher.
Megapixels is the one control to set. Under More are the tile size, the overlap, the grid multiple and the redraw limit, and an upscale model can be plugged in. It returns `tiles`, `stitcher`, `before` (your picture enlarged the plain way to the finished size), `width`, `height` and a `report` that says what was done.
### Tiled Upscale Stitch
Puts the redrawn tiles from Tiled Upscale back together, blends the joins and brings the picture to the size you asked for. Keep colors matches each tile to the same tile of your picture first, so the tiles agree with each other and grain in an old photo does not push the colors.
The Qwen Image 2.1 Upscale + Restore example redraws a small or worn photo sharp at the megapixels you set, in tiles above about 2. [Try the Qwen Image 2.1 Upscale + Restore example →](example_workflows/Qwen%20Image%202.1%20Upscale%20%2B%20Restore%20%28AusBoss%29.json)
### Align Image
Rounds a picture's size to a multiple you choose, by resizing, cropping or padding. Use it when the next model needs sizes that divide evenly and you don't want to type a target size. Crop and pad have anchors for where to cut or add, and padding can repeat the edge pixels or use a solid color. It returns the picture with its width and height.
### Image Size
Reads a picture's `width`, `height`, `longest_edge`, `shortest_edge` and batch `count` as whole numbers. Wire them into a resize, a latent, a frame count or Math Expression, so the rest of the graph follows the source's size. After a run the node shows each number beside its output.
### Color Match
Matches a picture's colors to a reference, with LAB, RGB, MKL or histogram transfer. Strength sets how much, and an optional mask (with invert) limits where. With `reference_mode` set to first frame, every frame of a batch matches the batch's first frame. That can cut down color drift in a clip, but it does not steady motion.
### Realign to Source (EXPERIMENTAL 🧪)
Lines an edited picture back up with the original. Qwen Image 2.1 often draws an edit, especially a style change, a little bigger or shifted: up to about 12% taller, by a different amount every seed. Wire in the edit and the picture it was made from. The node measures how far the edit moved and moves it back, at the original's size. It fixes the zoom and shift of the whole picture, not shapes the edit redrew in a new place, so the result lines up closely rather than pixel for pixel.
For the best result, pad the picture with Load Image + Pad's `mirror` fill before the edit and wire its `stitcher` in. The edit gets room to move, and you get back just your picture's area with real picture at the edges. Without that margin, an edit made at the picture's own size loses a thin strip the model pushed out of view. `empty_mask` marks that strip so you can crop or inpaint it, and **Empty fill** picks what it shows in the meantime.
**Fit** is `zoom + shift` (how Qwen edits drift) or `affine`, which also fixes a slight tilt. When the drift is a whole number of pixels, the node cuts the picture out as it is, with no resizing. `report` gives the measured zoom and shift, and the margin a side needed when an edit ran out of picture. A picture it can't measure passes through at the source's size, and the report says why. [Try the Qwen Image 2.1 Edit + Realign example →](example_workflows/Qwen%20Image%202.1%20Edit%20%2B%20Realign%20%28AusBoss%29.json)
![A Qwen Image 2.1 watercolor edit of a woman in front of a wall of old TVs, flipped against the original: without Realign the waistband and a shelf edge sit lower (61 px off), with Realign to Source they line up (0.6 px off).](assets/readme/realign.gif)
The same watercolor edit without and with Realign to Source: 61 px off before, 0.6 px after.
![Chart titled CRT wall, watercolor: Qwen draws it 5.6% taller, showing the original, the edit as Qwen drew it (61 px off) and the realigned edit (0.6 px off), with close-ups of the waistband.](assets/readme/realign-chart.webp)
Qwen drew this watercolor 5.6% taller, and Realign to Source lined it back up with the original.
### Image Compare A/B
Compares two pictures with a slider, or flips between them whole. Nothing covers the picture, and its size shows underneath. The panel grows with the node, and images A and B pass through, so you can put it anywhere on an image wire. **Erase** mixes the two by hand: rub parts of one picture away with a brush so the other shows through, then save the mix as a PNG under a name you type, with a layered PSD for Photoshop or GIMP if you want it.
### Save Image
Saves PNG, lossless WebP or lossless JPEG XL, with the workflow embedded if you want it. Pick a folder inside ComfyUI's output folder or browse its subfolders, edit the file name, and the path preview shows where the file will go. Chips add a counter, the date, the time, the size or a batch number to the name. Name tokens work as in ComfyUI's own Save Image: `%date:yyyy-MM-dd%`, `%width%`, `%KSampler.seed%`.
Link `filename` to keep a name from upstream, or `caption_text` to write a matching `.txt` next to each picture. The counter picks a name that isn't taken yet. Turn it off to reuse the same path on purpose. JPEG XL needs the optional `pillow-jxl-plugin` extra. The [help page](js/docs/AUSBOSS_NODES_SaveImage.md) has the full naming and compatibility notes.
Turn on **steps** to watch the picture form in the node while it is being made, with a **STOP** button for a run that is going wrong. It is off by default.
## Video nodes
![A square clip of a woman under a blue sky grows into a tall 9:16 frame as LTX 2.3 fills in more sky above her and the wall below.](assets/readme/video-outpaint.gif)
Here LTX 2.3 Video Outpaint takes a square clip to 9:16 and stitches the original frames back in.
The example keeps the clip's audio when there is any. The outpaint LoRA needs a pure black fill, since gray or white bands come back flat. With a black canvas it works at any aspect ratio and on any side. [Open the LTX 2.3 Video Outpaint workflow →](example_workflows/LTX%202.3%20Video%20Outpaint%20%28AusBoss%29.json)
### Load Video
Upload a video, or pick one from the Source list, which plays the clip under the pointer. Trim it with IN and OUT handles or typed timecodes. The player plays the part you kept and can loop it. Width and height resize while decoding, and setting only one keeps the shape. `every_nth` skips frames and lowers the reported fps so the timing stays right, and `max_frames` caps how many frames load.
It returns the frames, audio, frame count, fps, width, height, duration and a core `VIDEO`. Only the part you kept is decoded, and it checks memory before a large load. When you save the frames, wire its fps into the save node.
### Load Audio
Upload a sound file, or pick one from the Source list, and the node shows it as a wave. Drag the IN and OUT handles to keep only the part you want, or type the times. Press play to hear just that part. Set **Max length** to the longest sound the next step can take, such as a video's length: anything past it turns red and is cut off, so you see what comes out before you run. Click the red length next to OUT to move OUT to the cut.
It returns the `audio` you kept and its length in `seconds`. Only that part is decoded. [Help page](js/docs/AUSBOSS_NODES_LoadAudio.md).
### Video Crop + Rotate + Pad → Frame
Picks one frame from a video, then gives you the same rotate, crop and pad controls as the image node, plus an optional megapixel resize. Scrub to the frame with the playhead on the node; the editor adds frame-by-frame stepping and playback. It returns the transformed `image`, a `mask` of the added area, a `stitcher`, the chosen `original` frame, and the output `width` and `height`. For a whole clip, use the Clip version below.
### Video Crop + Rotate + Pad → Clip
Applies one rotate, crop and pad to every frame of a trimmed clip. It has the source picker, a timeline with a playhead and frame-accurate IN and OUT handles, frame skipping, a frame limit, the transform handles and output resizing. While you drag a trim handle, the node shows the exact first or last frame the run keeps. The bright part of the selection is what gets output.
Fill, feather and the resize budget are in **Crop, pad & resize** below the timeline, and a new clip keeps them. That matters for video outpainting, because the black, hard-edged canvas the model needs survives a source swap. A fresh Clip node starts with that canvas, and the editor's **Reset transform** goes back to it. **Frames for** trims the end to the frame count the next video model takes: LTX (8n+1) or Wan (4n+1). Tap a ratio to pad the clip to it; a new clip is padded to the lit ratio too. The padlock keeps the shape while you drag a handle.
It returns the frames, mask, audio, frame count, fps, size, duration, a `stitcher`, and the chosen `original` frames before the transform. **Inpaint & Stitch** in the editor sets the protected source area and previews the blend. After a video outpaint, wire the stitcher straight into **Stitch Inpaint**, with no Crop For Inpaint in between. It works in chunks to keep memory down.
**Fixed frames** sets an exact output length: 120 frames at 24 fps is five seconds. Drag either handle, or drag the highlighted band to move the whole window. Type the count, or link a frame count into `fixed_frames`. 0 turns the fixed length off. The frame limit still caps the length.
![The full-screen video editor with a portrait clip padded to landscape, an Inpaint and Stitch panel, and a timeline with IN and OUT handles.](assets/readme/video-editor.webp)
The Clip node's editor, from the video outpaint example.
### Select Frame
Picks one frame from a batch, unchanged: `1` is the first, `-1` the last and `-2` the one before it. 0, or a number past the end, stops with an error that gives the valid range. The preview switch shows the frame, or hides it and skips writing the preview file.
### Frame Interpolate
Changes a clip's frame rate by making in-between frames. You set the source and target fps, so 24 to 30 works as well as doubling. The blend method crossfades frames, and optical flow uses RAFT weights you put in place yourself. At a hard cut it holds the frame instead of blending two unrelated shots. It works in chunks to keep memory down.
It returns the new frames and their fps. It doesn't touch audio, so carry the source audio to Save Video separately. The [help page](js/docs/AUSBOSS_NODES_FrameInterpolate.md) covers the weights and timing.
### Save Video
Saves a frame batch or a core `VIDEO` as MP4 (H.264 or H.265), WebM (VP9 or AV1), ProRes MOV, lossless FFV1 MKV, GIF or WebP. Which encoders you get depends on your FFmpeg and PyAV build, and H.264 and H.265 come in CPU and NVENC versions. The videos carry color tags, and optionally audio and the workflow.
**Ping-pong** plays forward and then back without repeating the turnaround frames. A wired `VIDEO` brings its own frame rate; otherwise wire fps from Load Video or Frame Interpolate. The player on the node shows the saved file, and **Preview steps** shows the clip's first frame while it is being made, with a **STOP** button. Drop a saved video back on the canvas to load the workflow inside it, for the formats that can hold one. The [help page](js/docs/AUSBOSS_NODES_SaveVideo.md) has the format and audio details.
## Mask and inpaint nodes
![A woman in a pink fur coat under neon signs; a wipe turns the coat into a black leather jacket and leaves the rest of the picture as it was.](assets/readme/inpaint-masked.gif)
Krea 2 Inpaint Masked turns the painted fur coat into a leather jacket, using Mask Refine, Crop For Inpaint and Stitch Inpaint. [Open the Krea 2 Inpaint Masked workflow →](example_workflows/Krea%202%20Inpaint%20Masked%20%28AusBoss%29.json)
### Mask by Name
Type what to find in a picture, like "the dog" or "the red jacket", and get its mask. Nothing to paint. The node shows what it found, tinted on the picture, with how sure it is, and **FIND** runs only this node so you can check the mask before the rest of the workflow runs. When it finds nothing it stops the run with a plain message, so nothing after it runs on an empty mask; a setting passes an empty mask on instead.
It also returns the thing cut out on a see-through background, and how many things matched. Under **More**: how sure SAM 3 must be, fill the holes in the mask, and free SAM 3 from VRAM after the search. If you build a workflow that loads a big model after this node, turn **Free VRAM** on. The search is ComfyUI's own SAM 3: wire `model` and `clip` from a Load Checkpoint that loads the SAM 3 file. Needs ComfyUI 0.20 or newer, which has SAM 3 built in.
The Mask by Name example finds a thing by its name and saves its cut-out and its mask, with a Workflow Switches card to skip either one. [Open the Mask by Name workflow →](example_workflows/Mask%20by%20Name%20%28AusBoss%29.json)
### Mask by Touch
Click a thing on the picture and get its mask. No name to type and no MaskEditor: the picture is on the node. Move the pointer over it and the thing a click would take lights up, with its name beside the pointer: the jacket, not the whole person. Click to pick it, and click more things to add them. What is lit in teal is what the mask will be.
![Mask by Touch on a photo of a woman in a pink fur coat under neon signs: pointing lights up her face, hair and coat, clicks pick the coat and the hair, the eye shows the cut-out, and Subject with Fine edges cuts out the whole person with every strand of hair.](assets/readme/mask-by-touch.gif)
When a click could mean more than one thing, the node shows the pick with two arrows, like **‹ jacket · 1 of 3 ›**: an arrow swaps it for the next thing at that spot. A stroke across two things, like both shoes, takes both. Ctrl-click a thing to take it away again.
To fix the mask by hand, switch **Pick | Paint | Erase** (or press W, B or E, as in Photoshop). The brush has a size and a soft edge, set with **[** and **]** or by holding Alt and the right mouse button and dragging, and it shows under the pointer as it will paint. Hold **Ctrl** for the opposite: erase while you paint, paint while you erase.
Every area has a chip under the picture: a thing you picked, or an area you painted. Paint that touches an area joins its chip, and erasing makes none. A chip's cross takes that area away, a click selects it so what you paint joins it, and **all 3** on a named pick takes every thing of that name. The field at the end of the row takes a name you type, for things that did not light up.
**Subject** picks the picture's main subject in one click, and **Background** everything else. **Fine edges** is for hair and fur: switched on, the outline of the subject is followed strand by strand, on light and dark backgrounds, and the cut-out loses the colour of the old background. All three use ComfyUI's own background-removal model (`birefnet.safetensors`, ComfyUI 0.21 or newer); the help page says where to get it.
**Grow** and **Soften** show on the picture while you drag them. Under **More**: Smooth, Fill small holes, Invert, what to do when nothing is picked, and Free VRAM. The eye beside Undo and Clear hides everything else to show the result, as a cut-out or as the mask.
It also returns the picked area cut out on a see-through background, and the names of what was picked as text. The search is ComfyUI's own SAM 3: wire `model` and `clip` from a Load Checkpoint that loads the SAM 3 file. Needs ComfyUI 0.20 or newer, which has SAM 3 built in. The [help page](js/docs/AUSBOSS_NODES_MaskByTouch.md) has the keys and the details.
The Mask by Touch example loads a picture, lets you click what you want, and saves its cut-out and its mask, with Fine edges on. [Open the Mask by Touch workflow →](example_workflows/Mask%20by%20Touch%20%28AusBoss%29.json)
The Qwen Image 2.1 Inpaint example uses the same click to change one thing: click it, say what it should become, and only that part is repainted. [Open the Qwen Image 2.1 Inpaint workflow →](example_workflows/Qwen%20Image%202.1%20Inpaint%20%28AusBoss%29.json) · [Watch it on YouTube →](https://youtu.be/iMgaiXoB0v4)
### Mask Refine
Grows or shrinks a mask, blurs it, fills holes, smooths jagged edges, and sets black and white points. The card shows Expand and Blur, and **More** opens the rest. **AUTO** picks expand and blur values from the mask's size. The optional preview shows the result.
It returns the refined mask and its inverse. The `guided filter` and `matting` edge modes use a guide picture and each need an optional extra (see [Optional extras](#optional-extras)). The default, `off`, needs nothing extra.
### LaMa Inpaint
Removes what is under the white part of a mask and fills the hole from the surroundings, using a local LaMa model. Pixels under black stay as they are. One mask can cover a whole batch, and video frames go through one at a time to keep VRAM use down. The preview can show frames as they finish, or be switched off.
Put `big-lama.pt` in `ComfyUI/models/lama/`. It works one frame at a time, so a hard texture in a video may need more cleanup across frames. The old `SimpleWatermarkRemover` node id still loads, so older workflows keep working. [Try the Simple Video Watermark Remover example →](example_workflows/Simple%20Video%20Watermark%20Remover%20%28AusBoss%29.json)
### Crop For Inpaint
Crops the masked area plus some of what's around it, and can resize the crop to a target size or megapixel budget. It returns the cropped `image`, the `mask` the model paints in, and a `stitcher`. The card shows the usual mask and blend settings, and target size and canvas extension fold behind a disclosure.
The mask and the blend are separate. Growing or blurring the mask changes what the model can repaint, and the blend margin changes how the result joins the original. Raise **Context** or **Extra context** when the model needs to see more around the area. **Stay in picture**, on by default, stops the crop at the picture's edges. The `extend_*` controls grow the canvas for outpainting.
### Stitch Inpaint
Puts a generated crop or outpaint back into the original, using the stitcher from Crop For Inpaint, Load Image + Pad or any Crop + Rotate + Pad node. Everything outside the blend area keeps the stitcher's original pixels exactly. A video stitcher carries every original frame.
For a turned picture, or an outpaint with Tone match off, set **Seam** to **blend in** in the card's gear menu. It fades the model's picture into yours with no tone shift. **Tone match** evens out color steps at the seam. **Fix edge halo** cleans up a rim that got blended twice. It needs the optional matting extra, and without it the node warns and stitches normally. The [help page](js/docs/AUSBOSS_NODES_StitchInpaint.md) explains the stitcher and batches.
## Models and conditioning
### LoRA Loader
A whole LoRA stack in one node. Each row has an on switch, a searchable picker for the file and a strength you scrub. The strength bars share one scale, so the rows are easy to compare, and switching off the strongest row rescales the rest. Model and CLIP strengths can be split, and the CLIP input is optional.
The toolbar has the stack toggle, saved templates, reconnect and settings. The toggle turns the whole stack off, and back on to the rows you had on. **Absorb chain LoRAs** in the settings menu moves a chain of other LoRA loaders into this stack, in the order they applied, repeats included. It bypasses the old loaders only when every MODEL, CLIP and other connection can be kept. A shared branch or linked stack settings leave the chain alone.
![LoRA Loader absorbs a connected LoRA chain, shows strength changes while scrubbing, and restores the previous toggle selection.](assets/readme/lora-chain-demo.gif)
Each LoRA gets an info card from the file's metadata, a `.civitai.info` file next to it, and words you save yourself. The card's **Fetch Civitai info** button looks the file up on Civitai (your browser asks civitai.com, only when you click) and saves the answer as that `.civitai.info` file. Files Civitai doesn't know, such as unpublished or hidden models, come back as not found. The trigger words you pick come out of the `triggers` output. The same click saves the LoRA's picture from Civitai next to the file, so hovering the LoRA shows it (PG-13 or milder unless you allow more in the gear menu; a picture you already have is never replaced).
In the **+ LoRA** list, tick several files to add them at once. Rows can be gathered into groups, like layer groups in an image editor: each group folds away, and has its own switch that turns all its LoRAs off while each one keeps its own setting. A Workflow Switch can turn a whole group on and off.
When a file has moved, the node finds it by name if only one file matches. A missing LoRA that is switched on stops the run by default. Turn off **Stop on missing LoRA** to warn and skip it instead. A LoRA that changes nothing in the model gets a warning with its file name.
### Krea 2 Encode
Krea 2's positive and negative prompts in one card. Connect a VAE and reference pictures, and it adds them as references too, resized for the model first. **VLM reference** sets whether the text encoder also looks at them. Leave the references empty for plain text to image. For an outpaint, connect the pad node's mask too: the model then sees the new area in your picture's own colour instead of gray, so it paints it rather than keeping a gray frame.
The prompt box takes several lines and a linked text. The [help page](js/docs/AUSBOSS_NODES_Krea2Encode.md) covers preparing references. With an outpaint LoRA, copy the wiring from the matching example.
### Krea 2 Outpaint Model Patch
Tells Krea 2 where a reference picture sits on the canvas. It's made for Krea 2 outpaint LoRAs, and the two that exist want different placements. Put it after the LoRAs and before the sampler, and connect a stitcher and the reference conditioning from Krea 2 Encode.
The card has **Reference** placement and **KV cache**. The placement has to match the LoRA:
- **Whole canvas + AnyPaint:** the padded picture is the reference, VLM reference is on, the pad mask goes into Krea 2 Encode's `mask`, and a masked starting latent keeps the known pixels. Several sides can extend in one pass. The Krea 2 Outpaint example works this way.
- **Source rectangle + Registered Outpaint:** use the unpadded reference with VLM reference off. The source has to span the whole canvas in one direction, so extend left and right, or top and bottom, in each pass, including any padding added for rounding.
![A 16:9 photo of a woman at a waterfront railing is padded to 4:5, and Krea 2 fills in more sky above and the railing below.](assets/readme/krea2-outpaint.gif)
Here Krea 2 Outpaint takes a 16:9 photo to 4:5 in one pass. [Open the Krea 2 Outpaint workflow →](example_workflows/Krea%202%20Outpaint%20%28AusBoss%29.json)
The patch relies on ComfyUI's internal attention code. Its [help page](js/docs/AUSBOSS_NODES_Krea2OutpaintModelPatch.md) explains both setups and what happens without reference conditioning.
## Workflow utilities
Core ComfyUI has its own nodes for most of these jobs, and they work fine. These versions add the extras described under each one.
### Latent Size
Sets a canvas size. Pick landscape or portrait, a ratio and a megapixel budget, or drag the canvas handles. Orientation stays separate from the ratio, even on square. Typed sizes stay exact, and how drags snap is adjustable. It was called Resolution Master before 2.4.0.
It returns `width`, `height` and an empty image latent. The gear picks the latent layout (4, 16 or 128 channels) and the batch size. The latent's size rounds down to whole latent cells, so pick sizes that suit the layout when you wire the latent to a sampler. For video, use `width` and `height` with the model's own video latent node.
![Latent Size with a 1344 by 768 landscape canvas, its ratio chips, and the size and megapixel controls.](assets/readme/controls-showcase.webp)
### Layout Boxes
Draw boxes on a canvas and say what goes in each one: a thing, or words to write in the picture. Wire `elements` to MOSS-Image Layout Prompt and the picture puts each thing where its box is. It does what ComfyUI's own Create Bounding Boxes does, with the same inputs and the same outputs from the same boxes, and its canvas also works in the classic node look.
Drag to draw a box, drag it to move it and its handles to size it; under the canvas each box gets **object** or **text**, its words and up to five colors. A picture on `background` shows under the boxes and sets the canvas size, `width` and `height` can come from Latent Size, and boxes on the `bboxes` wire fill the canvas when the workflow runs. **Grid** snaps the boxes as you drag them, starts off over a picture and takes its size from the gear, **Layouts** starts from a ready arrangement, the arrow keys move the chosen box one pixel at a time (ten with Shift), and Delete and Ctrl + D remove and copy it.
The `mask` output is white inside every box: load a picture, draw a box over the words on a sign, and an inpaint chain can write new words there while the rest of the picture stays as it was. For words at a slant, drag the round handle over a box to turn it: the mask turns with it, and a turned text box tells the model its slant.
![Layout Boxes over a photo of a woman in front of a neon ramen sign: a text box is drawn over the sign, AUSBOSS and red neon letters are typed in, and MOSS-Image's result wipes in with the sign reading AUSBOSS.](assets/readme/layout-boxes.gif)
### Seed
One seed for several samplers. **Random**, **Fixed** and **Step** set what happens after each run. Switching to Random, or from Fixed to Step, changes the seed right away, so the next run is a new one. **New seed** rolls a value and keeps it. **Use last run** brings back the seed the last run actually used. The history menu keeps the last eight seeds and saves them with the workflow.
### Select Every Nth
Keeps every nth picture of a batch, after an offset that counts from 0. With nth 2, offset 0 keeps frames 1, 3 and 5, and offset 1 keeps 2, 4 and 6. The order stays the same. Wire the batch's fps in, and the `fps` output is divided by nth, so the thinned clip still lasts as long.
### Split Batch
Splits a batch after a frame number, counting from 1. Output `a` ends with that frame, and `b` gets the rest. Each side needs at least one frame, so a split point that breaks that stops with an error that gives the valid range.
### Merge Batches
Puts batch `b` after `a`. When the sizes match, nothing is resized. When they differ, it resizes to a's size or b's size, or stops with an error, whichever you pick. Both need the same number of channels.
### Text
A text box that grows with the node and sends its text, unchanged, down a `STRING` wire. Use it for a prompt, caption, path or other text that several nodes share. Spaces and line breaks are kept. Ctrl + Up and Ctrl + Down change the weight of the selected words, as in ComfyUI's own prompt boxes.
### H3 Prompt Writer
Type what happens, one line for each moment, and leave a blank line to cut to a new place. Connect up to four pictures and give each one a job: a person, a place, an outfit, a thing or a style. Copy the moves from a video, keep a clip going, or add a voice or a song. A timeline on the node shows when each moment plays before you run it. It needs no extra model and you never see H3's prompt format. Connect a small text model and it fills in a one-line idea.
**Change something** edits the named part of a video while keeping its person and sound. For a person swap, an optional `@1 looks like ...` line describes the connected replacement. Jobs that keep the source clip's length show **From the clip**; the `keeps_clip_sound` output helps a workflow choose the source audio.
### Integer
One whole number (`INT`) on a scrub control, for sharing a size, count or other whole number across nodes. Like every number box in the pack, it takes a typed sum: `1024*1.5`.
### Float
One decimal number (`FLOAT`) on a scrub control, for sharing a strength, scale, frame rate or duration across nodes.
### Math Expression
Works out a number from linked `a`, `b` and `c` inputs, each an integer or a decimal. An input left unwired counts as 0. It handles arithmetic plus `min`, `max`, `abs`, `round`, `floor`, `ceil` and `sqrt`, for example `a * 2` or `ceil(a / 32) * 32`. It returns a float and a rounded integer, and halves round away from zero. The expression is parsed, never run as Python code.
### Show Text
Shows what arrives on a wire, in a panel you can select from and resize: text, a number, or any other value. Text is passed on unchanged, and a number comes out as text. The last text shown saves with the workflow. Very long text may be cut short on screen, but the output keeps all of it. It runs as an output node, so nothing has to be connected after it.
### Free Memory
Passes any value through and, on the way, unloads ComfyUI's cached models, runs Python's garbage collection and clears the CUDA cache where there is one. Put it between two heavy stages when the second one needs the room. It runs only when its input changes, like any cached node, and later stages may take longer while models load again.
### Workflow Note
A setup card for a shared workflow: a title, instructions in Markdown, each model's download link and folder, the node packs it needs, and your links. It checks the models and packs against the install it's opened in. **Edit** changes the card, and **Banner** turns it into a small section title. The note never runs, and its text is never shown as raw HTML.
### Callout
A note for the canvas whose text grows to fill it, so it reads without zooming in. Double-click to type; a blank line starts a new point and `**two stars**` stress a word. Type an arrow emoji such as ⬆️ and it turns into a teal arrow in the middle of the line. Click a node while you edit, then press an arrow, and the arrow is linked to that node: click it later and the canvas brings the node into view. It never runs and has no wires.
### Compact note and link tiles
**Callout Tile** shows a compact title or colored YouTube/Discord link button. **Workflow Note Tile** opens a full help note from a compact button. They reuse the note editors and save as distinct node types, so an older install reports a missing node instead of drawing an incompatible plain note. Install or update ComfyUI-AusBoss, restart ComfyUI and reload when either tile is missing. Existing ordinary notes and callouts keep their original types.
### Run Timer
A stopwatch for the whole run, with no wires. It starts when the queue starts running, holds the total when the run ends, and saves the last time with the workflow. Recent times are in its right-click menu. You can resize it. It only shows time and doesn't change how the graph runs.
### Step Preview
Shows the picture while it is being made, one step at a time, so you can stop a run that is going wrong after the first few steps. It has no wires: put it anywhere and it shows whichever sampler is working. **STOP** stops this run; **STOP ALL** also empties the queue. When the picture is saved, the finished picture replaces the last step. It turns on ComfyUI's step pictures for its own runs, unless you chose a preview method yourself. The [help page](js/docs/AUSBOSS_NODES_StepPreview.md) covers sharper step pictures.
### Workflow Switches
Turns parts of a workflow off and on from one small card. Select the nodes of one part, such as a box and its Save Image, press **+ Switch**, and they get one **off | on** switch with a name you choose. To pick between settings instead, set a node and press **+ Choice** for each way: the card becomes one pill. Every group can have a switch of its own too. Off bypasses the nodes, so a step in the middle of a chain can be skipped and the picture still reaches the steps after it; a setting mutes them instead. A switch can change settings instead: save what chosen nodes hold for on and for off, and one click sets Steps to 8 and turns a LoRA row on. **Done** shrinks the card to the labels and their switches. A row whose nodes are only partly on reads **mixed**. The node never runs and has no wires. An AI agent can set one up for you: point it at the [agent guide](js/docs/agents/workflow_switches.md).
## Example workflows
These are the workflows where the nodes do something core ComfyUI doesn't do by itself: outpainting and inpainting that put your original picture back, lining an edit back up with its source, upscaling a picture in tiles, masking a thing by its name or with a click, and removing a watermark from a clip. Open a JSON file from [`example_workflows/`](example_workflows), or find them under this pack in ComfyUI's template browser. Each one has a thumbnail, numbered groups and a Workflow Note with the setup steps and model downloads.
![The Krea 2 Rotate + Outpaint workflow: a setup note on the left, then groups for your input, the two boxes and the model loaders, and the result with a before-and-after slider and Save Image.](assets/readme/workflow-layout.webp)
The examples use this layout: named groups for your input, the models and the result, with the plumbing inside two boxes you can double-click to open.
The workflows open with their image and video loaders empty, so you load your own picture or clip. To try one with a sample first, copy the pier photo or the short pier clip from [`example_workflows/inputs/`](example_workflows/inputs) into `ComfyUI/input/`. If you keep models in subfolders, pick your copy in each loader before you run. A green model check on the note only means the file exists; it doesn't mean the loader has that file selected. The models' licenses and any download terms are their authors'.
| Workflow | What it does | What you need |
|---|---|---|
| [Krea 2 Outpaint](example_workflows/Krea%202%20Outpaint%20%28AusBoss%29.json) | Pick a ratio or drag the orange handles out, and Krea 2 paints the new area | Krea 2 Turbo and the AnyPaint LoRA; its text encoder also writes the caption |
| [Krea 2 Inpaint Masked](example_workflows/Krea%202%20Inpaint%20Masked%20%28AusBoss%29.json) | Paint over part of a picture, say what goes there, and Krea 2 repaints only that | Krea 2 Turbo and the AnyPaint LoRA |
| [Krea 2 Rotate + Outpaint](example_workflows/Krea%202%20Rotate%20%2B%20Outpaint%20%28AusBoss%29.json) | Turn, crop and pad a picture, and Krea 2 paints the new area | Krea 2 Turbo and the AnyPaint LoRA |
| [Klein 9B Outpaint](example_workflows/Klein%209B%20Outpaint%20%28AusBoss%29.json) | Extend a picture with the PixaOutpaint LoRA, then stitch the original back | Distilled Klein 9B, its Qwen encoder and the Flux 2 VAE, the PixaOutpaint LoRA, and Qwen3-VL 8B INT8 for the caption |
| [LTX 2.3 Video Outpaint](example_workflows/LTX%202.3%20Video%20Outpaint%20%28AusBoss%29.json) | Widen a vertical clip, keeping its audio and original pixels | LTX 2.3, the distilled LoRA and the outpaint IC-LoRA |
| [Qwen Image 2.1 Edit + Realign](example_workflows/Qwen%20Image%202.1%20Edit%20%2B%20Realign%20%28AusBoss%29.json) | Type a short edit, and Realign to Source lines Qwen's result back up with your picture | Qwen Image 2.1 INT8, Qwen3-VL 8B INT8 and the 2.1 VAE |
| [Qwen Image 2.1 Upscale + Restore](example_workflows/Qwen%20Image%202.1%20Upscale%20%2B%20Restore%20%28AusBoss%29.json) | Load a small or worn photo, set the megapixels, and Qwen redraws it sharp, in tiles above about 2 | Qwen Image 2.1 INT8, Qwen3-VL 8B INT8, the 2.1 VAE, the Restore LoRA and, for Fast, the Viggle Turbo LoRA |
| [Qwen Image 2.1 Inpaint](example_workflows/Qwen%20Image%202.1%20Inpaint%20%28AusBoss%29.json) | Click a thing in your picture, say what to change, and Qwen repaints only that part | Qwen Image 2.1 INT8, Qwen3-VL 8B INT8, the 2.1 VAE, ComfyUI's own SAM 3.1 and, for Fast, the Viggle Turbo LoRA |
| [Mask by Name](example_workflows/Mask%20by%20Name%20%28AusBoss%29.json) | Type what to find in a picture, and get its mask and a cut-out with a see-through background | ComfyUI's own SAM 3.1 (ComfyUI 0.20 or newer) |
| [Mask by Touch](example_workflows/Mask%20by%20Touch%20%28AusBoss%29.json) | Click a thing in a picture, or its whole subject, and get its mask and a cut-out with hair-fine edges | ComfyUI's own SAM 3.1 and its background-removal model (ComfyUI 0.21 or newer) |
| [Simple Video Watermark Remover](example_workflows/Simple%20Video%20Watermark%20Remover%20%28AusBoss%29.json) | Find an overlay in a clip and remove it, with a one-frame comparison branch | ComfyUI's own SAM 3.1 (ComfyUI 0.20 or newer), plus `big-lama.pt` |
Every example uses only core nodes and this pack. Speed and memory use depend on the models, the sizes, the frame count and whatever else is using the GPU.
## For workflow creators
If you share workflows, a few of these nodes help the people who download them:
- **Workflow Note** puts a setup card on the canvas with each model's download link, folder and size, the node packs it needs and your links, and checks them against the reader's install.
- **Callout** is a note with big readable text and teal arrows you type with an emoji.
- **Image Compare A/B** shows the result against its source with a slider.
- **Seed** keeps runs repeatable, and **Use last run** brings back the seed the last run used.
- **Run Timer** shows how long the whole run took, so a speed note is measured.
- **Save Image** embeds the workflow in PNG, lossless WebP or JPEG XL files, with names you control.
- **Workflow Switches** gives each optional part of your workflow one off | on switch, so people skip what they don't need without breaking the chain.
- **Free Memory** between two heavy stages, and **Free VRAM** on Mask by Name and Mask by Touch, give the next model its room on a smaller graphics card.
People who download your workflow get the nodes through ComfyUI-Manager's missing-node install, like any other pack.
## Pack-wide tools
- **Help cards:** the **?** in a classic node's title bar opens its description, inputs and outputs. Nodes 2.0 and titleless nodes such as Run Timer have **About this node** in their right-click menu.
- **Recreate node 🆎** (right-click an AusBoss node) rebuilds a node saved by an older version from the current definition. It keeps the values, links, position and colors.
- **Replace with AusBoss nodes 🆎** (canvas right-click or the command palette) finds third-party nodes this pack can stand in for, such as VideoHelperSuite's Load Video and Video Combine or KJNodes' Color Match. It previews each swap and replaces the ones you leave ticked.
- **Snap into line:** a node, group or selection you drag lines up with the nodes and groups around it, edge to edge or middle to middle, and a teal line shows what it lines up with. Pulling a corner to resize lines up the same way. Hold Shift while you drag to skip it, or turn it off in Settings → 🆎 AusBoss → Canvas.
- **Line up:** select two or more nodes or groups and the selection toolbar gets a Line up button: line up edges or middles, match widths or heights, or space them evenly. With two or more nodes selected, pulling the edge or corner of one resizes them all: a column gets one width, a row one height, a grid both (turn it off in Settings → 🆎 AusBoss → Canvas → Resize together).
- **Node colors:** Settings → 🆎 AusBoss → Appearance sets the scheme for every AusBoss node (AusBoss, Graphite, Slate, Teal, Moss, Plum, Rust, Navy, Custom or Theme default). A node's right-click **AusBoss color** menu recolors just that node, and nodes you colored by hand keep their colors.
- **Run status:** under Settings → 🆎 AusBoss → Chrome, the queue status can show in the browser tab's title and icon, running nodes get live progress badges, and each node's run time can be shown.
- **Completion sound:** Settings → 🆎 AusBoss → Notifications plays a short chime when the queue finishes.
## Editor controls and settings
- Drag a number box to scrub it, or click it to type. Hold Shift for fine steps.
- Hold Shift while you turn the green rotation handle to snap to 15° steps.
- In the full-screen editor, the mouse wheel zooms, and the middle mouse button or Alt-drag pans. On the node itself, the wheel and middle mouse button still move the graph.
- **Alt+E** opens the selected node's transform editor. Change the key in Settings → Keybindings.
- The video editors have frame stepping, playback and exact timeline seeking, on the node face as well as in the editor. The Clip node's IN and OUT handles sit on the source's own frames, so the frame shown for IN is the first one decoded and the frame shown for OUT is the last.
- Your own aspect presets go in `ausboss_presets.json` beside the pack. Copy [`ausboss_presets_example.json`](ausboss_presets_example.json) to start. Your file survives updates.
LoRA Loader and Latent Size have their own gear menus for per-node preferences.
The video transform nodes read a video you pick from a list of ComfyUI's input folder (Uploads) or output folder (Outputs), never one named by a whole path, and Save Image writes only inside the output folder. No widget can point the pack anywhere else on the disk. See [the video transform help](js/docs/AUSBOSS_NODES_VideoCropRotatePad.md).
## Optional extras
Three features need an extra Python package. Install it with the Python that runs ComfyUI. For the Windows portable build, that is `python_embeded\python.exe -m pip install ...` from the portable folder.
```bash
python -m pip install opencv-contrib-python # Mask Refine: guided filter
python -m pip install "pymatting>=1.1" # Mask Refine: matting; Stitch Inpaint: edge halo
python -m pip install pillow-jxl-plugin # Save Image: lossless JPEG XL
```
After updating frontend files, hard-refresh the browser with `Ctrl+Shift+R`.
The default mask settings, ordinary stitching, PNG and lossless WebP don't need them. Optical flow needs RAFT weights on disk, and the node's help page explains the setup. Which video codecs you have depends on your ComfyUI environment.
## Quick start
## Compatibility and development
1. Add either transform node.
2. Pick or upload a source. For very large videos, switch the video node to `local path` mode.
Queued workflows always read local paths, but the editor's live preview of paths outside
ComfyUI's own folders is opt-in: start ComfyUI with `AUSBOSS_TRANSFORM_LOCAL_PREVIEW=1`
to enable it. This keeps the preview routes from exposing arbitrary files to anything
that can reach your ComfyUI server (for example on `--listen`).
3. Click **Open editor**.
4. Drag cyan crop handles, orange padding handles, or the green rotation handle.
5. Close the editor and connect `image` and `mask` downstream.
The pack needs ComfyUI **0.27.1** or newer. An example for a newer model needs a ComfyUI core that supports that model, and updating only this pack can't add it. Every night it is checked against the newest ComfyUI release and ComfyUI's development version. It works on the classic canvas and in Nodes 2.0, and running a workflow doesn't need an editor open.
Use the matching examples in [`example_workflows`](example_workflows) as small starting graphs.
## Editor controls
- Eight crop handles with generous hit targets
- Four visually distinct padding handles
- Rotation handle with alignment grid and `Shift` snapping to 15 degrees
- Free or ratio-locked crop
- Mouse-wheel zoom; middle-mouse or `Alt`-drag pan
- Reset view, reset padding, and reset all
- Video first/last, ±1, ±25, ±50, ±100, playback, and timeline scrubbing
## Settings
Under **Settings → 🆎 AusBoss** you can pick an **AusBoss node color** scheme
(Graphite, Slate, Teal, Moss, Plum, Rust, Navy, or **Custom** with your own
picked tint). It recolors every AusBoss
node in the open workflow immediately and applies to nodes you add later.
Nodes you have colored by hand keep their own colors, and **Theme default**
returns everything to the stock look. Right-click any AusBoss node for a
per-node **AusBoss color** override, and press **Alt+E** (rebindable in
Settings → Keybindings) to open the selected transform node's editor.
Custom crop aspect-ratio presets live in an optional `ausboss_presets.json`
next to the pack — copy `ausboss_presets_example.json` to start; your file is
gitignored and survives updates.
Under **Chrome**: the browser tab's icon and title can show rendering state
and queue depth (on by default), **live status badges** report progress on the
node itself while it works — `frame 43/120` through a video inpaint — (on by
default), and optional **node runtime badges** stamp each node with its seconds
after a run (off by default).
## Compatibility
- Classic V1 node definitions for broad ComfyUI compatibility
- Classic canvas and Nodes 2.0 frontend support
- API-mode execution does not require the editor to be open
- Transform editors validated on ComfyUI `0.27.1`; the pack scaffold was also validated on `0.28.0`
The declared minimum is ComfyUI `0.27.1` (`supported_comfyui_version` in `pyproject.toml`, which ComfyUI-Manager enforces at install time). If a frontend update changes custom-widget behavior, please include your ComfyUI and frontend versions in the issue.
## Development
Backend changes need a full ComfyUI restart, and frontend changes need a hard refresh. Run the backend tests with ComfyUI's Python:
```bash
python scripts/validate_nodes.py
python scripts/release_preflight.py
python -m unittest discover -s tests -p "test_*.py" -v
python scripts/run_python_tests.py
node --test tests/*.test.mjs
```
See [`AGENTS.md`](AGENTS.md) for compatibility rules and [`docs/adding_a_node.md`](docs/adding_a_node.md) for the node checklist.
The backend tests run in separate processes, because their offline ComfyUI stubs must not leak between files. [CONTRIBUTING.md](CONTRIBUTING.md) covers bug reports and pull requests. [`docs/live_testing.md`](docs/live_testing.md) covers canvas, mouse-drag, save and reload, and workflow checks, and [`docs/adding_a_node.md`](docs/adding_a_node.md) lists what a new public node needs.
## Feedback
Bug reports and workflow ideas are welcome in [issues](https://github.com/ausboss/ComfyUI-AusBoss/issues). Security problems go through [SECURITY.md](SECURITY.md). Every release is in the [changelog](CHANGELOG.md). If a node saves you time, a star on GitHub helps other people find it. There is also a [Ko-fi page](https://ko-fi.com/ausboss) if you want to leave a tip.
## License
+70
View File
@@ -0,0 +1,70 @@
# Security
ComfyUI answers `/prompt` and custom-node routes without a login, so anyone
who can reach a ComfyUI server can queue a workflow or call a node's route.
This pack treats every widget value and route parameter as untrusted and
follows a few fixed rules.
## What the pack does and does not do
- **Files stay inside ComfyUI's folders.** Save Image writes only to
ComfyUI's output folder or a subfolder of it, and Save Video checks its
prefix and final file the same way. Image Compare's Erase save and Align
never take a file to read from the browser: they work on the two previews
that node made in its last run, and Align writes its lined-up copy of B
beside them in ComfyUI's temp folder. The name typed for the new files is a plain name
with no folder in it, and they go straight into the output folder. Load Video, Load Image + Pad
and Image Crop + Rotate + Pad pick from the input folder, and take a file ComfyUI made by
the name it gives it (`photo.png [output]`), as core Load Image does; the video transform
nodes read a video picked from a list of the
input or the output folder, by the name ComfyUI gives it there
(`clip.mp4 [output]`), and their uploader streams into the input folder.
No input opens a whole path as given. Absolute paths, `..`, `~`, Windows drive and
alternate-stream syntax, UNC paths and symlinks that lead outside those
folders are refused.
- **Routes answer with data, not files.** The pack's routes send JSON and
small pictures made for the request (a video frame, a gallery tile, a
LoRA's preview). The only files sent as they are on the disk are the
pack's own scripts. An error message never names a place on the server's
disk.
- **The server makes no network requests.** The pack's Python code never
contacts a host. The one exception is a button you click: LoRA Loader's
**Fetch Civitai info** has your browser ask civitai.com about that one
LoRA, by its SHA256 hash, and the answer is saved beside the LoRA as
`<model>.civitai.info` (inside ComfyUI's LoRA folders). The pack checks
that the answer describes the same file before saving it. Nothing is sent
until you click, and the button can be turned off in the node's settings.
The same click also has your browser download the LoRA's first picture
from Civitai's picture server (a small copy, without cookies, rated PG-13
or milder unless you allow more). The pack checks it is a picture and
saves it beside the LoRA as `<model>.preview.jpg`, never replacing a
picture that is there. The gear menu's **Civitai picture** option turns
this off.
- **No code execution.** Math Expression parses its text into an
allowlisted syntax tree of arithmetic and a few math functions; it never
runs Python source. The pack starts no subprocesses and installs nothing
at runtime.
- **No automatic downloads.** You place model weights (LaMa, RAFT optical
flow) yourself; the pack never fetches them.
- **LaMa opens only known files.** A LaMa model file can run code when it
opens, so LaMa Inpaint checks a file's SHA-256 first and opens only known
copies of `big-lama.pt`.
- **No raw HTML from workflows.** Workflow Note renders its Markdown into
text nodes, and its links open only `http(s)` addresses.
`scripts/release_preflight.py` fails a release whose shipped code contains an
HTTP or socket client.
These rules protect a ComfyUI server from the requests it receives. They do
not defend against someone who can already change files on the ComfyUI
machine, and exposing ComfyUI beyond a trusted network is still a risk of
its own.
## Reporting a vulnerability
Report privately through this repository's **Security** tab →
**Report a vulnerability**. If that option is not offered, open an issue
asking for a private contact and leave the details out of it.
Fixes land on `main` and ship in the next release; the
[CHANGELOG](CHANGELOG.md) records security-relevant changes.
+69 -13
View File
@@ -7,35 +7,70 @@ name to NODE_MODULES below — nothing else to wire up.
A module that fails to import is skipped with a console warning instead
of taking the whole pack down, so one broken node never hides the rest.
Only one copy of the pack loads. People install it from ComfyUI-Manager
(custom_nodes/ausboss-nodes) and from GitHub (custom_nodes/ComfyUI-AusBoss),
and two copies used to register the same server routes, which stopped
ComfyUI at startup. A second copy now loads nothing and says which folder
to delete.
"""
import importlib
import os
import re
import sys
import tomllib
import traceback
import types
# One entry per file in nodes/. Keep alphabetical-ish; order only affects
# nothing functional.
_HERE = os.path.dirname(os.path.abspath(__file__))
_FIRST_COPY = sys.modules.setdefault(
"_ausboss_nodes_first_copy", types.SimpleNamespace(folder=_HERE)
)
_IS_SECOND_COPY = _FIRST_COPY.folder != _HERE
# One entry per file in nodes/, kept alphabetical. The order has no
# functional effect.
NODE_MODULES = [
"node_align_image",
"node_batch_utils",
"node_callout",
"node_color_match",
"node_compare",
"node_drop_shadow",
"node_frame_chooser",
"node_frame_interpolate",
"node_free_memory",
"node_h3_prompt_writer",
"node_image_crop_rotate_pad",
"node_image_folder",
"node_image_resize",
"node_image_size",
"node_inpaint_crop_stitch",
"node_krea2_encode",
"node_krea2_model_patch",
"node_lama_inpaint",
"node_lmstudio_chat",
"node_layout_boxes",
"node_literals",
"node_load_image_pad",
"node_load_audio",
"node_load_video",
"node_lora_loader",
"node_pad_image",
"node_mask_by_name",
"node_mask_by_touch",
"node_math",
"node_realign_to_source",
"node_refine_mask",
"node_resolution",
"node_run_timer",
"node_save_image",
"node_save_video",
"node_seed",
"node_select_frame",
"node_show_text",
"node_step_preview",
"node_tiled_upscale",
"node_video_crop_rotate_pad",
"node_video_crop_rotate_pad_clip",
"node_workflow_note",
"node_workflow_switches",
]
NODE_CLASS_MAPPINGS = {}
@@ -43,6 +78,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
_failed_modules = []
for _module_name in NODE_MODULES:
if _IS_SECOND_COPY:
break
try:
_module = importlib.import_module(f".nodes.{_module_name}", __name__)
NODE_CLASS_MAPPINGS.update(getattr(_module, "NODE_CLASS_MAPPINGS", {}))
@@ -55,18 +92,18 @@ for _module_name in NODE_MODULES:
# Folder ComfyUI serves to the browser; every .js file in it is loaded as a
# frontend extension. Shared modules use .mjs so they are import-only.
WEB_DIRECTORY = "./js"
WEB_DIRECTORY = None if _IS_SECOND_COPY else "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
def _read_version():
def _read_version(folder=None):
"""Version straight from pyproject.toml so it is never duplicated."""
try:
pyproject = os.path.join(os.path.dirname(__file__), "pyproject.toml")
with open(pyproject, "r", encoding="utf-8") as f:
match = re.search(r'^version\s*=\s*"([^"]+)"', f.read(), re.MULTILINE)
return match.group(1) if match else "unknown"
pyproject = os.path.join(folder or os.path.dirname(__file__), "pyproject.toml")
with open(pyproject, "rb") as f:
version = tomllib.load(f).get("project", {}).get("version")
return version if isinstance(version, str) and version.strip() else "unknown"
except Exception:
return "unknown"
@@ -81,6 +118,17 @@ def _print_banner():
bar = f"{teal}{'-' * 70}{reset}"
print(bar)
if _IS_SECOND_COPY:
def where(folder):
text = f"{os.path.basename(folder)} (v{_read_version(folder)})"
return text.encode("ascii", "replace").decode("ascii")
print(f" {red}AusBoss is installed twice, so this copy is not loaded.{reset}")
print(f" In use: custom_nodes/{where(_FIRST_COPY.folder)}")
print(f" Skipped: custom_nodes/{where(_HERE)}")
print(" Delete one of the two folders and restart ComfyUI.")
print(bar)
return
print(
f" {teal}AusBoss{reset} v{_read_version()} | "
f"{len(NODE_CLASS_MAPPINGS)} nodes loaded"
@@ -91,4 +139,12 @@ def _print_banner():
print(bar)
try:
if not _IS_SECOND_COPY:
from .nodes._frontend_cache import register_frontend_cache_guard
register_frontend_cache_guard()
except Exception:
traceback.print_exc()
_print_banner()
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.23.0">
<title>release: v2.23.0</title>
<path fill="#555" d="M0 0h48v20H0z"/>
<path fill="#00b4aa" d="M48 0h57v20H48z"/>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text x="24" y="14">release</text>
<text x="76.5" y="14">v2.23.0</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+11 -7
View File
@@ -20,22 +20,26 @@ Every visible field and output must earn its place. Published IDs, input order,
1. Put independently testable processing in an underscore-prefixed helper under `nodes/`.
2. Add one V1 wrapper in `nodes/node_<purpose>.py` with `INPUT_TYPES`, `RETURN_TYPES`, `FUNCTION`, `CATEGORY`, and both mapping dictionaries — keys as string literals, never a `NODE_ID` variable, or registry scanners cannot see the node.
3. Add the module to `NODE_MODULES` in `__init__.py`.
4. Add frontend JavaScript only when the normal schema cannot provide the required interaction.
3. Add the module to `NODE_MODULES` in `__init__.py` and the key to `PUBLIC_NODE_IDS` in `scripts/validate_nodes.py`; the validator fails on a registered key it does not know. A key kept only so old workflows load goes in `LEGACY_NODE_IDS` instead.
4. Give the node its face. Any INT, FLOAT, BOOLEAN, COMBO or STRING widget on a public node is shown through a widget card entry in `js/widget_cards/index.js` (see CONTRIBUTING.md, Conventions: numbers scrub, booleans are off | on pills, one linkable widget per row, `top: true` or `forceInput` for values people wire). Write more frontend JavaScript only when the card grammar cannot provide the interaction (a stage, a player, an editor).
5. Namespace every route, extension, event, DOM marker, CSS class, cache, and browser state with `ausboss`.
6. Chain lifecycle hooks through `chainCallback`; never replace core or third-party prototypes directly.
7. Keep normal workflow and API execution independent from the custom frontend.
8. Ship user-facing toggles as ComfyUI settings under the `AusBoss.*` namespace (see `.claude/skills/ausboss-node-brand/SKILL.md`). Node coloring is automatic for `AUSBOSS_NODES_*` classes via `js/appearance/` — no per-node work.
8. Ship user-facing toggles as ComfyUI settings under the `AusBoss.*` namespace. Node coloring is automatic for `AUSBOSS_NODES_*` classes via `js/appearance/` — no per-node work.
9. A `VALIDATE_INPUTS` names only the inputs it reads, never `**kwargs`. ComfyUI skips its own range and list checks for every input a validator names (naming `input_types` also switches off its link checks), and repeats a failed check once per named input. Checks that need several values can run in the node itself. `tests/test_validate_inputs.py` pins what each validator names.
## Document and prove
- Add rich help at `js/docs/<exact-mapping-key>.md`.
- Add a minimal workflow and matching JPEG under `example_workflows/`.
- Add rich help at `js/docs/<exact-mapping-key>.md` (the validator checks it exists) and a section in `README.md`. Name card controls the way the node face shows them, with the widget name alongside: **Tone match** (`color_match`).
- Show the node in an example under `example_workflows/`, a new one or an existing one it fits: a Workflow Note, numbered groups holding every node, no overlaps, and a matching `.jpg` of a real output. CONTRIBUTING.md, Example workflows, lists the rest.
- A DOM panel goes into `tests/panel_guards.test.mjs` as `mustGrow` or `fixedByDesign`.
- At the next release, name the node in the pyproject `description`: Registry and Manager search read it.
- Extend `scripts/validate_nodes.py` when the new contract needs a permanent assertion.
- Add the node to the API snapshot: `AUSBOSS_COMFY_ROOT=<ComfyUI> python tests/test_node_api.py --update` with ComfyUI's Python. From then on its inputs and outputs may only be appended.
- Add pure Python and dependency-free JavaScript tests.
- Compile with ComfyUI's embedded Python.
- Verify `/object_info/<mapping-key>`, ownership, served assets, routes, API execution, and queued dimensions/masks.
- Test the actual canvas in Classic and Nodes 2.0, including save/reload, duplication, graph zoom, source replacement, and teardown.
- Scan the diff for paths, hosts, secrets, obsolete branding, placeholders, agent attribution, and non-ASCII import output.
- Test the actual canvas in Classic and Nodes 2.0, including save/reload, duplication, graph zoom, source replacement, and teardown. Drag a real link onto every card row and socket and look at where it lands (`docs/live_testing.md`).
- Scan the diff for paths, hosts, secrets, placeholders and non-ASCII import output.
Do not release a node that only works in the editor, only works for one source, or returns debugging outputs users do not need.
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+95
View File
@@ -0,0 +1,95 @@
# Live frontend testing
Frontend work in this pack is proven on a real canvas: a node is created in a
real ComfyUI tab, real mouse drags are dispatched, and the result is a
screenshot a person looks at. Reading the code, or a test that only checks
which input received a link, has let broken designs through before (three
sockets stacked on one pixel passed every automated check). This is the
recipe.
## The pieces
- **ComfyUI** on `http://127.0.0.1:8188` (set `COMFY_URL` to point the
scripts elsewhere), with this repository installed or linked into
`custom_nodes`. A changed `INPUT_TYPES` is served only after a full
restart. Stop the test instance, then start it from the ComfyUI checkout
using that installation's Python environment:
```bash
python main.py --listen 127.0.0.1
```
From this repository, run `python scripts/run_python_tests.py` with the
same environment to isolate ComfyUI stubs and keep CPU thread counts
bounded. Set `AUSBOSS_COMFY_ROOT` to the ComfyUI checkout to enable the
optional core video integration checks.
- **Headless Chrome or Chromium** on an unused CDP port; these examples use
**9334**. Use a separate test profile and the installed browser executable:
```bash
chromium \
--headless=new --remote-debugging-port=9334 --user-data-dir=/tmp/chrome-ausboss \
--window-size=1800,1100 --no-first-run --no-default-browser-check about:blank &
```
- **`scripts/dev/`** — small Node scripts that talk CDP directly, with no
package dependencies. Use a Node version with built-in `fetch` and `WebSocket`:
| script | does |
|---|---|
| `cdp.mjs <port> <workflow.json\|-> <out.png\|-> [eval.js] [waitMs]` | opens/reuses the Comfy tab, optionally loads a workflow, evaluates a JS file, screenshots (a returned `__clip` rect clips the shot), prints the eval result as JSON |
| `cdp_drag.mjs <port> <setup.js> <mid.png\|-> <after.png\|-> [check.js]` | `setup.js` returns `{from, to, __clip}` in client pixels; presses at `from`, moves to `to`, screenshots mid-drag, releases, evaluates `check.js`, screenshots again |
| `audit_workflows.mjs <port> <output-dir> [workflow.json ...]` | loads every example (or selected files), exports API and loaded graphs, captures screenshots, and fails on overlaps, missing nodes, browser exceptions, or exposed AusBoss number widgets |
| `wheel_sweep.mjs <port> [--vue] [--only=Type,Type] [--workflow=file.json] [--shots=dir]` | turns the mouse wheel at every point of every node of the pack (or of the nodes in one workflow) and fails on any spot where the graph does not zoom, naming the element that took the wheel; `--real` sends real wheel turns, slower and on a coarser grid |
| `face_smoke.mjs <port> [--vue] [--only=Type,Type]` | puts one of each node of the pack on an empty graph and fails when a face does not come up: no node, a panel that is not on the page or has no size, an error thrown, a file of the pack that did not load. Needs no model, so it also runs in the nightly ComfyUI check |
| `cdp_nav_debug.mjs <port>` | navigates the tab in place and logs every CDP event, for when a tab stops answering |
| `cdp_hang.mjs <port>` | pre-arms the debugger on a fresh tab, loads ComfyUI, and pauses to print the stack if the page stops answering |
`CDP_RELOAD=1` closes the Comfy tab and opens a fresh one with the cache
cleared — required after editing any `.mjs`, which the frontend caches
hard.
## Screenshot convention
An eval creates the node at `pos [0, 0]`, sets `app.canvas.ds.scale = 1` and
`ds.offset = [160, 230]`, and returns
`__clip: { x: r.left + 152, y: r.top + 192, width: node.size[0] + 16, height: node.size[1] + 46 }`
(`r` is the canvas element's bounding rect). A drag gives two shots: `_mid`,
taken during the drag, and `_after`, the result.
## Gotchas learned the hard way
- **Clearing the graph reuses node ids**, and the frontend's widget store
hands a new node the previous node's widget values (a toggled `preview`
came back `false`). Bump `app.graph.last_node_id += 50` after
`app.graph.clear()`, or set the value you want explicitly.
- **A real mouse drag marks the workflow modified.** The next in-place
navigation raises ComfyUI's `beforeunload` dialog and the whole tab stops
answering every CDP call, even `1+1`; it looks exactly like a renderer
hang. The scripts auto-accept dialogs, and `CDP_RELOAD=1` makes `cdp.mjs`
reload into a fresh tab rather than navigate the old one.
- **Reset a half-finished link drag** (`app.canvas.linkConnector.reset()`)
before clearing the graph in an eval.
- The mid-drag screenshot shows the socket lit and the tooltip, not the
greyed field: the field greys once the link exists, in the `_after` shot.
- Widget input sockets are drawn only while a link is dragged, hovered, or
connected, so a "no socket visible" screenshot at rest is normal; the
mid-drag shot is the one that shows where a link can land.
- `cdp.mjs` and `cdp_drag.mjs` requests time out after 25 s; the workflow
audit allows 45 s.
Inspect the browser's dialogs and logs when a request stops responding.
- Keep test renders and logs out of the repository; only deliberately
shipped sample media belongs in `example_workflows/inputs/`.
## What to verify for a node face
1. The face at rest (values, alignment, no clipped panel).
2. Every group disclosure open.
3. A real link dropped on every row and socket, `_mid` and `_after`.
4. `app.graphToPrompt()` sends the link for a linked input and the typed
value otherwise.
5. Save, reload (fresh tab), and confirm links, values and hidden state.
6. The preview switch on and off where the node has one.
7. The wheel zooms the graph over every part of the face:
`wheel_sweep.mjs`, once plain and once with `--vue`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

@@ -1,40 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "AUSBOSS_NODES_ImageCropRotatePad",
"pos": [120, 180],
"size": [360, 390],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{"name": "image", "type": "IMAGE", "links": [1], "slot_index": 0},
{"name": "mask", "type": "MASK", "links": [], "slot_index": 1}
],
"properties": {"Node name for S&R": "AUSBOSS_NODES_ImageCropRotatePad"},
"widgets_values": ["", 0, "free", 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, "#808080"]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [560, 180],
"size": [360, 400],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [{"name": "images", "type": "IMAGE", "link": 1}],
"outputs": [],
"properties": {"Node name for S&R": "PreviewImage"}
}
],
"links": [[1, 1, 0, 2, 0, "IMAGE"]],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

@@ -1,40 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "AUSBOSS_NODES_VideoCropRotatePad",
"pos": [120, 150],
"size": [360, 455],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{"name": "image", "type": "IMAGE", "links": [1], "slot_index": 0},
{"name": "mask", "type": "MASK", "links": [], "slot_index": 1}
],
"properties": {"Node name for S&R": "AUSBOSS_NODES_VideoCropRotatePad"},
"widgets_values": ["", "input folder", "", "frame index", 0, 0, 0, "free", 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, "#808080"]
},
{
"id": 2,
"type": "PreviewImage",
"pos": [560, 150],
"size": [360, 400],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [{"name": "images", "type": "IMAGE", "link": 1}],
"outputs": [],
"properties": {"Node name for S&R": "PreviewImage"}
}
],
"links": [[1, 1, 0, 2, 0, "IMAGE"]],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
-60
View File
@@ -1,60 +0,0 @@
import { app } from "/scripts/app.js";
import { chainCallback } from "../shared/index.mjs";
import { setWidgetVisible } from "../shared/widget_visibility.mjs";
const NODE_CLASS = "AUSBOSS_NODES_AlignImage";
// Each anchor/fill widget only means something in its own mode, so the node
// face stays as compact as before the options existed:
// crop_position -> mode == "crop"
// pad_position, pad_fill -> mode == "pad"
// pad_color -> mode == "pad" AND pad_fill == "color"
function syncOptionWidgets(node) {
const widget = (name) => node.widgets?.find((item) => item.name === name);
const mode = widget("mode");
const padFill = widget("pad_fill");
if (!mode) return;
const wants = {
crop_position: mode.value === "crop",
pad_position: mode.value === "pad",
pad_fill: mode.value === "pad",
pad_color: mode.value === "pad" && padFill?.value === "color",
};
let changed = false;
for (const [name, visible] of Object.entries(wants)) {
const target = widget(name);
if (target && setWidgetVisible(target, visible)) changed = true;
}
if (!changed) return;
const width = node.size?.[0] ?? 0;
node.setSize([Math.max(width, node.computeSize()[0]), node.computeSize()[1]]);
node.graph?.setDirtyCanvas?.(true, true);
}
function watchWidget(node, name) {
const widget = node.widgets?.find((item) => item.name === name);
if (!widget) return;
const previous = widget.callback;
widget.callback = function (...args) {
const result = previous?.apply(this, args);
syncOptionWidgets(node);
return result;
};
}
app.registerExtension({
name: "AusBoss.AlignImage",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_CLASS) return;
chainCallback(nodeType.prototype, "onNodeCreated", function () {
watchWidget(this, "mode");
watchWidget(this, "pad_fill");
syncOptionWidgets(this);
});
chainCallback(nodeType.prototype, "onConfigure", function () {
// Restored widget values land after creation; re-sync to the loaded
// mode without disturbing the size the workflow saved.
queueMicrotask(() => syncOptionWidgets(this));
});
},
});
+55 -69
View File
@@ -1,5 +1,4 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { app } from "../../../scripts/app.js";
import {
CUSTOM_SCHEME,
DEFAULT_SCHEME,
@@ -10,8 +9,9 @@ import {
schemeColors,
shouldRecolor,
titleInk,
wearsLegacyScheme,
} from "../shared/appearance.mjs";
import { AUSBOSS_JS_VERSION, chainCallback } from "../shared/index.mjs";
import { chainCallback, chainHandler, isAusbossNode } from "../shared/index.mjs";
import {
BADGE_RADIUS,
badgeCenter,
@@ -23,6 +23,7 @@ import { openInfoCard } from "../shared/settings_menu.mjs";
const SETTING_ID = "AusBoss.Appearance.NodeColor";
const CUSTOM_SETTING_ID = "AusBoss.Appearance.CustomColor";
const COLOR_OVERRIDE = "ausbossColor";
// Stored bare (no "#") to match the color setting's storage contract.
const DEFAULT_CUSTOM_COLOR = "00b4aa";
@@ -40,42 +41,11 @@ let activeCustomColor = DEFAULT_CUSTOM_COLOR;
let activeCustomTitle = normalizeHexColor(DEFAULT_CUSTOM_COLOR);
function setActiveCustomColor(value) {
if (!normalizeHexColor(value)) return;
activeCustomColor = value;
activeCustomTitle = normalizeHexColor(value);
}
// Surface the stale-cache warning where the user actually looks. On current
// frontends app.extensionManager is the workspace store, whose `toast` is the
// toast store: add({ severity, summary, detail, life }) queues a PrimeVue
// toast. Older frontends without it fall back to the console, and nothing in
// here may ever throw — this is advice, not a feature.
function warnStaleJs(serverVersion) {
const detail =
`Installed pack is v${serverVersion} but this tab is running ` +
`v${AUSBOSS_JS_VERSION} JavaScript from the browser cache. Hard-refresh ` +
"the tab (Ctrl+Shift+R) to load the updated frontend.";
try {
const toast = app.extensionManager?.toast;
if (typeof toast?.add === "function") {
toast.add({
severity: "warn",
summary: "AusBoss frontend is stale",
detail,
life: 15000,
});
return;
}
} catch (_error) {
// Toast store missing or incompatible: the console still works.
}
console.warn(`[AusBoss] ${detail}`);
}
function isAusbossNode(node) {
const comfyClass = node?.comfyClass || "";
return comfyClass.startsWith("AUSBOSS_NODES_") || comfyClass === "SimpleWatermarkRemover";
}
function applyScheme(node, colors) {
// Assigning undefined (never `delete`) keeps the property reactive under
// the Vue-based Nodes 2.0 renderer while still restoring theme defaults.
@@ -88,6 +58,7 @@ function applyScheme(node, colors) {
function repaintAll(next, previous) {
for (const node of collectGraphNodes(app.graph)) {
if (!isAusbossNode(node)) continue;
if (Object.hasOwn(node.properties ?? {}, COLOR_OVERRIDE)) continue;
if (!shouldRecolor(node, previous)) continue;
applyScheme(node, next);
}
@@ -178,23 +149,15 @@ app.registerExtension({
const storedCustom = app.ui?.settings?.getSettingValue?.(CUSTOM_SETTING_ID);
if (normalizeHexColor(storedCustom)) setActiveCustomColor(storedCustom);
installTitleInk();
// Stale-cache probe: an updated pack served to a browser still running
// old cached JavaScript fails in confusing ways, so say so once. Any
// network or route failure stays silent — this is advice, not a feature.
try {
const response = await api.fetchApi("/ausboss/pack_version");
const payload = await response.json();
const server = payload?.version;
if (response.ok && server && server !== "unknown" && server !== AUSBOSS_JS_VERSION) {
warnStaleJs(server);
}
} catch (_error) {
// Old backend without the route, offline, or a non-JSON reply: silent.
}
},
getNodeMenuItems(node) {
if (!isAusbossNode(node)) return [];
// Vue nodes skip the canvas badge callbacks; title-less nodes have no bar.
const about = isTitleless(node) || globalThis.LiteGraph?.vueNodesMode === true
? [{ content: "About this node", callback: () => openHelpCard(node, null) }]
: [];
return [
...about,
{
content: "AusBoss color",
has_submenu: true,
@@ -202,12 +165,14 @@ app.registerExtension({
options: SCHEME_NAMES.map((name) => ({
content: name,
callback: () => {
// A menu pick writes the colors directly; shouldRecolor then
// treats them as the user's choice during setting sweeps.
// Keep an explicit choice, including no colors, through saves
// and setting changes. Custom stores the pair picked today.
const colors = schemeColors(name, activeCustomColor);
// Custom with an unusable stored color resolves to null, which
// would wrongly read as "Theme default" — do nothing instead.
if (name === CUSTOM_SCHEME && !colors) return;
node.properties ??= {};
node.properties[COLOR_OVERRIDE] = colors ? { ...colors } : null;
applyScheme(node, colors);
app.graph?.setDirtyCanvas?.(true, true);
},
@@ -219,20 +184,48 @@ app.registerExtension({
nodeCreated(node) {
if (!isAusbossNode(node)) return;
installHelpBadge(node);
// Colors restored from a saved workflow (and manual picks) land before
// this hook runs — an already-colored node is left alone.
if (node.color || node.bgcolor) return;
const colors = schemeColors(activeScheme, activeCustomColor);
if (colors) applyScheme(node, colors);
restoreAppearance(node);
// Construction precedes saved fields; reload, paste and undo restore
// their colors and properties before this callback runs.
chainCallback(node, "onConfigure", function (saved) {
restoreAppearance(this, saved);
});
},
});
// Every AusBoss node gets a quiet "?" in the title bar; clicking it opens a
// card built from the node's own DESCRIPTION and tooltips, so the docs on
// screen are exactly the docs in the source.
function restoreAppearance(node, saved) {
if (Object.hasOwn(node.properties ?? {}, COLOR_OVERRIDE)) {
// A later pick in ComfyUI's own color menu is saved in the native fields.
if (saved?.color || saved?.bgcolor) return;
applyScheme(node, node.properties[COLOR_OVERRIDE]);
return;
}
if ((node.color || node.bgcolor) && !wearsLegacyScheme(node)) return;
applyScheme(node, schemeColors(activeScheme, activeCustomColor));
}
function isTitleless(node) {
return (node.title_mode ?? node.constructor?.title_mode) === (globalThis.LiteGraph?.NO_TITLE ?? 1);
}
function openHelpCard(node, event) {
const nodeData = node.constructor?.nodeData;
const clientX = event?.clientX ?? window.innerWidth / 2;
const clientY = event?.clientY ?? 80;
openInfoCard({
anchor: { left: clientX - 150, top: clientY, bottom: clientY + 6 },
title: node.title || nodeData?.display_name || "About this node",
sections: helpSections(nodeData),
});
}
// Classic nodes get a quiet "?" in the title bar. Vue nodes open the same
// card from their context menu. Its text comes from DESCRIPTION and tooltips.
function installHelpBadge(node) {
// A title-less node (Run Timer) has no bar to hang the badge on; its
// card opens from the node's context menu instead.
chainCallback(node, "onDrawForeground", function (ctx) {
if (!showBadge(this.size?.[0] ?? 0, this.flags?.collapsed)) return;
if (isTitleless(this) || !showBadge(this.size?.[0] ?? 0, this.flags?.collapsed)) return;
const [x, y] = badgeCenter(this.size[0]);
ctx.save();
ctx.beginPath();
@@ -247,17 +240,10 @@ function installHelpBadge(node) {
ctx.fillText("?", x, y + 0.5);
ctx.restore();
});
chainCallback(node, "onMouseDown", function (event, pos) {
if (!showBadge(this.size?.[0] ?? 0, this.flags?.collapsed)) return false;
chainHandler(node, "onMouseDown", function (event, pos) {
if (isTitleless(this) || !showBadge(this.size?.[0] ?? 0, this.flags?.collapsed)) return false;
if (!pos || !hitsBadge(pos, this.size[0])) return false;
const nodeData = this.constructor?.nodeData;
const clientX = event?.clientX ?? window.innerWidth / 2;
const clientY = event?.clientY ?? 80;
openInfoCard({
anchor: { left: clientX - 150, top: clientY, bottom: clientY + 6 },
title: this.title || nodeData?.display_name || "About this node",
sections: helpSections(nodeData),
});
openHelpCard(this, event);
return true; // consume the click so it does not start a drag
});
}
+80
View File
@@ -0,0 +1,80 @@
// Notices when this tab is still running old AusBoss files after an update,
// refreshes them, and asks for a reload. A reload alone is not always enough:
// browsers may keep reusing the old shared modules (.mjs) for hours.
//
// Imports nothing from ../shared on purpose. Old cached copies of those files
// are the problem this entry has to survive.
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
const PACK_URL = new URL("../", import.meta.url).href;
const PARALLEL = 6;
async function runningVersion() {
try {
return (await import("../shared/index.mjs")).AUSBOSS_JS_VERSION;
} catch (_error) {
return null;
}
}
// Fetching with cache "reload" replaces the browser's stored copy with a
// fresh one, so the next page load uses the new files.
async function refreshModules(paths) {
const queue = [...paths];
const worker = async () => {
while (queue.length) {
const path = queue.pop();
try {
await fetch(PACK_URL + path, { cache: "reload" });
} catch (_error) {
// One file failing to refresh must not stop the rest.
}
}
};
await Promise.all(Array.from({ length: PARALLEL }, worker));
}
function askForReload(installed) {
const detail =
`AusBoss v${installed} is installed, but this page is still running an older copy. ` +
"Press F5 to reload the page.";
const toast = app.extensionManager?.toast;
if (typeof toast?.add === "function") {
// No `life`: the message stays until it is closed. The page keeps running
// the old files until it is reloaded, however long that takes.
toast.add({ severity: "warn", summary: "AusBoss was updated", detail });
} else {
console.warn(`[AusBoss] ${detail}`);
}
}
// The installed version this page already asked a reload for. A server that
// drops and comes back several times must not stack the same message.
let asked = null;
async function check() {
// Advice, not a feature: any network or route failure stays silent.
try {
const installed = (await (await api.fetchApi("/ausboss/pack_version")).json())?.version;
const running = await runningVersion();
if (!installed || installed === "unknown" || !running || installed === running || installed === asked) return;
asked = installed;
const listing = await (await api.fetchApi("/ausboss/pack_modules")).json();
await refreshModules(Array.isArray(listing?.modules) ? listing.modules : []);
askForReload(installed);
} catch (_error) {
// Old backend without the routes, offline, or a non-JSON reply.
}
}
app.registerExtension({
name: "ausboss.cache_guard",
async setup() {
await check();
// An update from the Manager restarts the server and reloads the workflow,
// not the page, so an open tab keeps the scripts it started with. Look
// again every time the server comes back.
api.addEventListener("reconnected", check);
},
});
+724
View File
@@ -0,0 +1,724 @@
// Callout 🆎: a note whose text grows to fill it. An arrow typed into the text
// (an arrow emoji such as ⬆️, a plain arrow, a pointing hand, or -> and <-)
// shows as a teal arrow sitting in the line.
//
// The text lives in the node's one STRING widget (hidden, so save/load, undo
// and copy ride the default path); the panel only shows it. Decisions live in
// js/shared/callout.mjs under node:test.
import { app } from "../../../scripts/app.js";
import { setTileFace, TILE_FACE_CSS } from "../shared/tile_face.mjs";
import { CALLOUT_TYPES, serializeNoteIdentity } from "../shared/note_identity.mjs";
import { BRAND, chainCallback, keepDomWidgetWidthAuto } from "../shared/index.mjs";
import {
ARROW_ANGLE,
ARROW_CHOICES,
DOUBLE_ARROWS,
arrowText,
fitFontSize,
parseBlocks,
pastedLink,
repointLinks,
targetId,
toggleSmall,
tileLinkFields,
tileLinkText,
urlLabel,
wrapLink,
} from "../shared/callout.mjs";
import { onPaste } from "../shared/paste_ids.mjs";
import { keepKeyInField } from "../shared/canvas_passthrough.mjs";
import { WIDGET_FRAME, fillNodeHeight } from "../shared/panel_layout.mjs";
import { hideInputsInDef, hideWidget } from "../shared/widget_visibility.mjs";
import { commitWidgetValue } from "../shared/widget_card_math.mjs";
import { compactAction } from "../shared/panel_controls.mjs";
import { frameBounds } from "../shared/workflow_switches.mjs";
const CSS_ID = "ausboss-callout-css";
const WIDGET_NAME = "ausboss_callout_panel";
const DEFAULT_SIZE = [380, 160];
const PANEL_MIN_WIDTH = 200;
const PANEL_MIN_HEIGHT = 64;
const SVG_NS = "http://www.w3.org/2000/svg";
const ARROW_COLOR = "#1fd3c6";
const LINK_TILES = {
youtube: { label: "Watch Me on YouTube", url: "https://www.youtube.com/@ausbossdev" },
discord: { label: "Join for Help or More Workflows", url: "https://discord.gg/RwVxY9Fuaj" },
};
const FRAME_FILL = 0.8; // the share of the view a node brought into view takes, as in Workflow Switches
const FONT = '"IBM Plex Sans", "Inter", "Segoe UI", system-ui, -apple-system, sans-serif';
// The text sits out of flow, centred in the card by its auto margins. Nodes
// 2.0 sizes a node by its content, so text in normal flow set the note's
// height: a note could not be made shorter than its own text, and the text
// was then fitted to a card it had sized itself. Out of flow, the card takes
// its size from the node alone, in both renderers.
//
// The editor's bar is laid out for a note of ordinary width. On a narrow one
// it drops its "Arrow" label and its hint, and when the note is tall enough
// it goes onto two rows, so the buttons keep their size instead of being
// squeezed to slivers (the two @container rules).
function ensureCss() {
if (document.getElementById(CSS_ID)) return;
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
${TILE_FACE_CSS}
.ausboss-callout{box-sizing:border-box;width:100%;height:100%;padding:2px 4px 6px;overflow:hidden;pointer-events:none;}
.ausboss-callout-card{position:relative;box-sizing:border-box;width:100%;height:100%;overflow:hidden;border-radius:8px;background:linear-gradient(135deg,rgba(0,180,170,.07),rgba(0,0,0,.2));}
.ausboss-callout-inner{position:absolute;top:0;bottom:0;left:0;box-sizing:border-box;width:100%;height:fit-content;margin:auto 0;padding:.6em 1em .6em .85em;color:#d6e6e4;font-family:${FONT};font-weight:500;line-height:1.4;letter-spacing:.003em;overflow-wrap:anywhere;}
.ausboss-callout-point{margin:0 0 .6em;padding-left:.7em;border-left:2px solid rgba(0,180,170,.7);}
.ausboss-callout-point:last-child{margin-bottom:0;}
.ausboss-callout-point b{color:#6fe0d6;font-weight:650;}
.ausboss-callout-small{margin-top:.3em;color:#9fb5b2;font-size:max(11px,.62em);font-weight:400;line-height:1.4;}
.ausboss-callout-small b{color:#8fd6cf;font-weight:600;}
.ausboss-callout-card[data-tile=youtube]{background:#ae2639}
.ausboss-callout-card[data-tile=discord]{background:#5146aa}
.ausboss-callout-card[data-tile=title]{background:#090909;border-left:4px solid #21c7b9}
.ausboss-callout-card[data-tile] .ausboss-callout-inner{padding-block:.15em;}
.ausboss-callout-card[data-tile] .ausboss-callout-point{border:0;padding:0;margin:0;font-weight:650;color:#fff}
.ausboss-callout-card[data-tile] .ausboss-callout-url{color:white;text-decoration:none;display:flex;align-items:center;gap:12px}
.ausboss-callout-card[data-tile] .ausboss-callout-url svg{width:36px;height:28px;flex:none}
.ausboss-callout-card[data-tile] .ausboss-callout-out{margin-left:auto}
.ausboss-callout-card[data-tile] .ausboss-callout-tools{right:27px;}
.ausboss-callout-empty{color:#6f8987;font-size:14px;font-weight:400;}
.ausboss-callout-arrow{display:inline-block;width:1.2em;height:1.2em;margin:0 .1em;vertical-align:-.26em;filter:drop-shadow(0 0 .3em rgba(0,180,170,.55));}
.ausboss-callout-arrow svg{display:block;width:100%;height:100%;overflow:visible;}
.ausboss-callout-arrow.is-link{pointer-events:auto;cursor:pointer;border-radius:6px;filter:drop-shadow(0 0 .4em rgba(0,180,170,.8));}
.ausboss-callout-arrow.is-link:hover{background:rgba(0,180,170,.2);}
.ausboss-callout-arrow.is-gone{opacity:.4;}
.ausboss-callout-url,.ausboss-callout-jump{pointer-events:auto;cursor:pointer;border-radius:.2em;color:#6fe0d6;text-decoration:underline;text-decoration-thickness:.06em;text-underline-offset:.17em;}
.ausboss-callout-url:hover,.ausboss-callout-jump:hover{background:rgba(0,180,170,.2);color:#a5f1ea;}
.ausboss-callout-url b,.ausboss-callout-jump b{color:inherit;}
.ausboss-callout-out{display:inline-block;margin-left:.2em;font-size:.7em;font-weight:600;vertical-align:.14em;text-decoration:none;opacity:.85;}
.ausboss-callout-jump.is-gone{opacity:.45;cursor:default;text-decoration-style:dotted;}
.ausboss-callout-tools{position:absolute;top:5px;right:5px;display:flex;gap:4px;opacity:0;transition:opacity .15s;pointer-events:auto;}
.ausboss-callout-card:is(:hover,:focus-within) .ausboss-callout-tools,.ausboss-callout.is-selected .ausboss-callout-tools{opacity:.95;}
.ausboss-callout-tool{height:22px;padding:0 8px;border:1px solid rgba(0,180,170,.45);border-radius:5px;background:rgba(8,14,16,.86);color:#9fd8d3;font:600 11px ${FONT};letter-spacing:.02em;cursor:pointer;}
.ausboss-callout-tool:hover{color:#fff;border-color:${BRAND};}
.ausboss-callout-editor{position:absolute;inset:0;box-sizing:border-box;display:flex;flex-direction:column;gap:6px;padding:6px;border:1px solid ${BRAND};border-radius:8px;background:rgba(5,10,11,.95);pointer-events:auto;container-type:size;}
.ausboss-callout-edit{flex:1;min-height:0;box-sizing:border-box;width:100%;padding:6px 8px;resize:none;border:none;background:transparent;color:#e6f4f3;font:15px/1.45 ${FONT};outline:none;}
.ausboss-callout-arrowbar{flex:none;display:flex;align-items:center;gap:4px;}
.ausboss-callout-arrowbar span{margin-right:4px;color:#6f8987;font:11px ${FONT};}
.ausboss-callout-link{margin-left:auto;display:flex;align-items:center;gap:6px;max-width:55%;color:#6f8987;font:11px ${FONT};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ausboss-callout-link.has-node{color:#9fe8e1;}
.ausboss-callout-link button{height:20px;padding:0 6px;border:1px solid rgba(0,180,170,.35);border-radius:5px;background:rgba(8,14,16,.9);color:#9fd8d3;font:600 11px ${FONT};cursor:pointer;}
.ausboss-callout-pick{width:26px;height:24px;padding:3px;border:1px solid rgba(0,180,170,.35);border-radius:5px;background:rgba(8,14,16,.9);cursor:pointer;}
.ausboss-callout-pick:hover{border-color:${BRAND};background:rgba(0,180,170,.14);}
.ausboss-callout-pick svg{display:block;width:100%;height:100%;}
.ausboss-callout-size{flex:none;height:24px;margin-left:4px;padding:0 7px;border:1px solid rgba(0,180,170,.35);border-radius:5px;background:rgba(8,14,16,.9);color:#9fd8d3;font:600 11px ${FONT};cursor:pointer;}
.ausboss-callout-size:hover{border-color:${BRAND};background:rgba(0,180,170,.14);}
.ausboss-tile-editor{box-sizing:border-box;width:min(520px,94vw);max-height:86vh;overflow:auto;margin:auto;padding:24px;border:1px solid #365957;border-top:3px solid ${BRAND};border-radius:10px;background:#111b1c;color:#e6f4f3;box-shadow:0 18px 64px #0008;font:14px/1.5 ${FONT};}
.ausboss-tile-editor::backdrop{background:#06090ab8;}
.ausboss-tile-editor h2{margin:0 0 18px;font-size:21px;}
.ausboss-tile-editor label{display:flex;flex-direction:column;gap:7px;margin-bottom:16px;font-weight:600;}
.ausboss-tile-editor input,.ausboss-tile-editor textarea{box-sizing:border-box;width:100%;padding:10px 12px;border:1px solid #365957;border-radius:5px;background:#080e10;color:#e6f4f3;font:16px/1.5 ${FONT};user-select:text;}
.ausboss-tile-editor textarea{min-height:120px;resize:vertical;}
.ausboss-tile-editor :is(input,textarea):focus{outline:2px solid ${BRAND};outline-offset:1px;}
.ausboss-tile-editor footer{display:flex;justify-content:flex-end;align-items:center;gap:8px;}
.ausboss-tile-editor footer span{margin-right:auto;color:#99b0ad;font-size:12px;}
.ausboss-tile-editor .ausboss-compact-action{height:32px;padding:0 15px;font-size:13px;}
.ausboss-tile-editor .ausboss-tile-error{color:#ffc36d;margin:0 0 12px;}
@container (max-width:395px){.ausboss-callout-arrowbar>span:first-child,.ausboss-callout-link:not(.has-node){display:none;}}
@container (max-width:395px) and (min-height:112px){.ausboss-callout-arrowbar{flex-wrap:wrap;}.ausboss-callout-pick{flex:none;}}
`;
document.head.appendChild(style);
}
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = text;
return node;
}
function svg(tag, attrs) {
const node = document.createElementNS(SVG_NS, tag);
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
return node;
}
// One teal arrow, drawn right-pointing and turned to face `kind`.
function arrowIcon(kind) {
const root = svg("svg", { viewBox: "0 0 24 24", "aria-hidden": "true" });
const group = svg("g", { transform: `rotate(${ARROW_ANGLE[kind] ?? 0} 12 12)`, fill: ARROW_COLOR, stroke: ARROW_COLOR });
const double = DOUBLE_ARROWS.has(kind);
group.append(
svg("path", { d: `M${double ? 5 : 3} 12H${double ? 19 : 15.5}`, fill: "none", "stroke-width": "2.8", "stroke-linecap": "round" }),
svg("path", { d: "M13.5 5.8L22 12L13.5 18.2Z", "stroke-width": "1.6", "stroke-linejoin": "round" }),
);
if (double) {
group.append(svg("path", { d: "M10.5 5.8L2 12L10.5 18.2Z", "stroke-width": "1.6", "stroke-linejoin": "round" }));
}
root.append(group);
return root;
}
// The node a link names, looked up in the graph the note sits in.
function linkedNode(state, id) {
const graph = state.node.graph ?? app.graph;
return graph?.getNodeById?.(id) ?? graph?.getNodeById?.(Number(id)) ?? null;
}
function nodeName(node) {
return node?.getTitle?.() ?? node?.title ?? node?.type ?? "node";
}
// The group a link names (#g3), looked up in the graph the note sits in.
function linkedGroup(state, id) {
const graph = state.node.graph ?? app.graph;
return (graph?.groups ?? graph?._groups ?? []).find((group) => String(group?.id) === String(id)) ?? null;
}
const isGroup = (item) => Boolean(item) && (item instanceof (globalThis.LGraphGroup ?? globalThis.LiteGraph?.LGraphGroup ?? class {}) || (item.constructor?.name === "LGraphGroup"));
// What a piece of the text points at inside the workflow: a node or a group,
// with the words that say so. null for a piece that points at nothing here.
function placeOf(state, piece) {
if (piece.group !== undefined) {
const group = linkedGroup(state, piece.group);
return { item: group, title: group ? `Bring the group "${group.title ?? ""}" into view` : "The group this points at is not in the workflow" };
}
if (piece.link !== undefined) {
const node = linkedNode(state, piece.link);
return { item: node, title: node ? `Bring "${nodeName(node)}" into view` : "The node this points at is not in the workflow" };
}
return null;
}
function arrowSpan(state, kind, piece = {}) {
const span = el("span", "ausboss-callout-arrow");
span.append(arrowIcon(kind));
// Inside linked words (a run) the arrow is part of that link.
const place = piece.run === undefined ? placeOf(state, piece) : null;
if (!place) return span;
span.classList.add("is-link", place.item ? "is-live" : "is-gone");
span.title = place.title;
span.addEventListener("click", (event) => {
event.stopPropagation();
bringIntoView(place.item);
});
return span;
}
// The words of one link, as one element: an address opens in a new tab, a
// node or a group comes into view.
function linkRun(state, first) {
if (first.url !== undefined) {
const link = el("a", "ausboss-callout-url");
const tile = state.card.dataset.tile;
if (tile === "youtube" || tile === "discord") {
const icon = svg("svg", { viewBox:"0 0 28 20", "aria-hidden":"true" });
if (tile === "youtube") {
icon.append(svg("rect",{x:0,y:1,width:28,height:18,rx:5,fill:"white"}),
svg("path",{d:"M11 5.5 19 10 11 14.5Z",fill:"#ae2639"}));
} else {
icon.append(svg("path",{d:"M4 3 10 1l1 2a15 15 0 0 1 6 0l1-2 6 2c2 4 3 8 3 12l-6 4-2-3a17 17 0 0 1-10 0l-2 3-6-4c0-4 1-8 3-12Z",fill:"white"}));
for (const cx of [9,19]) icon.append(svg("ellipse",{cx,cy:10,rx:2,ry:2.6,fill:"#5146aa"}));
}
link.append(icon);
}
link.href = first.url;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.title = `Opens ${urlLabel(first.url)} in a new tab`;
// A press on a link must not become a node drag, and a double-click is one visit.
link.addEventListener("pointerdown", (event) => event.stopPropagation());
let last = 0;
link.addEventListener("click", (event) => {
event.stopPropagation();
if (event.timeStamp - last < 600) event.preventDefault();
last = event.timeStamp;
});
return link;
}
const place = placeOf(state, first);
const jump = el("span", "ausboss-callout-jump");
jump.setAttribute("role", "link");
jump.title = place.title;
if (!place.item) jump.classList.add("is-gone");
jump.addEventListener("pointerdown", (event) => event.stopPropagation());
jump.addEventListener("click", (event) => {
event.stopPropagation();
bringIntoView(place.item);
});
return jump;
}
// Bring a node or a group into view the way Workflow Switches does: a small
// one is shown at natural size, a big one fitted to most of the view.
function bringIntoView(target) {
const canvas = app.canvas;
if (!target || !canvas) return;
const title = isGroup(target) ? 0 : (globalThis.LiteGraph?.NODE_TITLE_HEIGHT ?? 30);
const found = [target.pos[0], target.pos[1] - title, target.size[0], target.size[1] + title];
const view = canvas.canvas?.getBoundingClientRect?.();
const rect = frameBounds(found, [view?.width, view?.height], FRAME_FILL);
if (typeof canvas.animateToBounds === "function") {
canvas.animateToBounds(rect, { zoom: FRAME_FILL });
} else if (typeof canvas.ds?.fitToBounds === "function") {
canvas.ds.fitToBounds(rect, { zoom: FRAME_FILL });
canvas.setDirty?.(true, true);
}
}
function renderText(state) {
const style = state.node.properties?.ausbossCalloutTile;
if (["youtube","discord","title"].includes(style)) state.card.dataset.tile = style;
else delete state.card.dataset.tile;
setTileFace(state.node, state.root, Boolean(state.card.dataset.tile), () => startEdit(state));
const blocks = parseBlocks(state.valueWidget.value);
state.inner.replaceChildren();
state.empty = blocks.length === 0;
if (state.empty) {
state.inner.append(el("div", "ausboss-callout-empty", "Double-click to write a note"));
}
for (const lines of blocks) {
const point = el("div", "ausboss-callout-point");
lines.forEach((line, index) => {
// Small print is a block of its own under the line above it. Normal
// lines are told apart by a break, as before; a block needs none.
const into = line.small ? el("div", "ausboss-callout-small") : point;
if (index && !line.small && !lines[index - 1].small) point.append(document.createElement("br"));
let run = null;
let wrap = null;
for (const piece of line) {
// The pieces of one link sit in one element.
if (piece.run !== run) {
if (wrap?.classList.contains("ausboss-callout-url")) wrap.append(el("span", "ausboss-callout-out", "↗"));
run = piece.run;
wrap = run === undefined ? null : linkRun(state, piece);
if (wrap) into.append(wrap);
}
const target = wrap ?? into;
if (piece.arrow) target.append(arrowSpan(state, piece.arrow, piece));
else target.append(piece.bold ? el("b", "", piece.text) : document.createTextNode(piece.text));
}
if (wrap?.classList.contains("ausboss-callout-url")) wrap.append(el("span", "ausboss-callout-out", "↗"));
if (line.small) point.append(into);
});
state.inner.append(point);
}
fit(state);
}
// The frontend's wrapper around a panel takes every click, so a press on the
// note's body would neither select nor drag it. The wrapper hands those
// presses to the canvas and opens the editor on a double-click; the Edit
// button and the editor take the pointer for themselves.
let forwarded = null;
function letClicksThrough(state) {
const wrapper = state.root.closest(".dom-widget");
if (!wrapper || wrapper.__ausbossCallout) return;
wrapper.__ausbossCallout = true;
// The presses are forwarded to the canvas, which can swallow the browser's
// own double-click, so a second press close to the first is read here.
let lastPress = null;
wrapper.addEventListener("pointerdown", (event) => {
const canvas = app.canvas;
if (event.target !== wrapper || event.button !== 0 || !canvas) return;
const again = lastPress
&& event.timeStamp - lastPress.time < 500
&& Math.hypot(event.clientX - lastPress.x, event.clientY - lastPress.y) < 8;
lastPress = { time: event.timeStamp, x: event.clientX, y: event.clientY };
if (again) {
lastPress = null;
event.preventDefault();
event.stopPropagation();
startEdit(state);
return;
}
forwarded = event.pointerId;
event.preventDefault();
event.stopPropagation();
canvas.processMouseDown(event);
}, true);
wrapper.addEventListener("contextmenu", (event) => {
if (event.target !== wrapper) return;
event.preventDefault();
app.canvas?.processContextMenu?.(state.node, event);
}, true);
window.addEventListener("pointermove", (event) => {
if (forwarded === event.pointerId) app.canvas?.processMouseMove(event);
}, true);
window.addEventListener("pointerup", (event) => {
if (forwarded !== event.pointerId) return;
forwarded = null;
app.canvas?.processMouseUp(event);
}, true);
}
// The text grows to the largest size that still sits inside the node.
function fit(state) {
const { card, inner } = state;
letClicksThrough(state);
if (state.empty) {
inner.style.fontSize = "";
return;
}
const height = card.clientHeight;
const width = card.clientWidth;
if (!height || !width) return;
const fits = (px) => {
inner.style.fontSize = `${px}px`;
return inner.offsetHeight <= height && inner.scrollWidth <= width + 1;
};
const size = fitFontSize(fits);
const social = card.dataset.tile === "youtube" || card.dataset.tile === "discord";
inner.style.fontSize = `${social ? Math.min(size, 16) : size}px`;
}
function markChanged() {
app.graph?.setDirtyCanvas?.(true, true);
try {
app.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState?.();
} catch {
// The change tracker is optional; the canvas still saves what it holds.
}
}
function startTileEdit(state) {
if (state.editor) return;
const original = String(state.valueWidget.value ?? "");
const social = ["youtube", "discord"].includes(state.card.dataset.tile);
const link = social ? tileLinkFields(original) : null;
const dialog = el("dialog", "ausboss-tile-editor");
const heading = social ? "Edit link tile" : "Edit title";
dialog.setAttribute("aria-label", heading);
dialog.append(el("h2", "", heading));
const label = el("label", "", link ? "Button text" : "Text");
const text = el(link ? "input" : "textarea");
text.value = link?.label ?? original;
label.append(text);
dialog.append(label);
let address;
if (link) {
const field = el("label", "", "Link address");
address = el("input");
address.type = "url";
address.value = link.url;
field.append(address);
dialog.append(field);
}
const error = el("p", "ausboss-tile-error");
error.setAttribute("role", "alert");
const foot = el("footer");
const cancel = compactAction("Cancel");
const save = compactAction("Save");
foot.append(el("span", "", "Ctrl + Enter to save"), cancel, save);
dialog.append(error, foot);
const commit = () => {
let value = text.value;
try {
if (address) value = tileLinkText(text.value, address.value);
} catch (problem) {
error.textContent = problem.message;
return;
}
if (value !== original) {
state.node.graph?.beforeChange?.();
commitWidgetValue(state.node, state.valueWidget, value, app.canvas);
renderText(state);
state.node.graph?.afterChange?.();
markChanged();
}
dialog.close();
};
save.addEventListener("click", commit);
cancel.addEventListener("click", () => dialog.close());
dialog.addEventListener("keydown", event => {
event.stopPropagation();
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
commit();
}
});
dialog.addEventListener("keyup", event => event.stopPropagation());
dialog.addEventListener("close", () => {
state.editor = null;
dialog.remove();
state.root.querySelector(".ausboss-callout-tool")?.focus();
}, { once: true });
state.editor = dialog;
document.body.append(dialog);
dialog.showModal();
text.focus();
text.select();
}
function startEdit(state) {
if (state.editor) return;
if (state.card.dataset.tile) return startTileEdit(state);
const wrap = el("div", "ausboss-callout-editor");
const area = el("textarea", "ausboss-callout-edit");
area.value = state.valueWidget.value ?? "";
area.placeholder = "One idea per paragraph. **Bold** a word to stress it. The buttons below add an arrow, make a line small or make words a link.";
area.spellcheck = true;
const bar = el("div", "ausboss-callout-arrowbar");
bar.append(el("span", "", "Arrow"));
// The node the next arrow will be linked to: click a node on the canvas
// while editing, then press an arrow.
let linkTarget = null;
const linkBox = el("div", "ausboss-callout-link");
const showLink = () => {
linkBox.replaceChildren();
linkBox.classList.toggle("has-node", Boolean(linkTarget));
if (!linkTarget) {
linkBox.append(el("span", "", "Click a node or a group to link to it"));
return;
}
const clear = el("button", "", "\u00D7");
clear.type = "button";
clear.title = "Do not link the next arrow or link";
clear.addEventListener("mousedown", (event) => event.preventDefault());
clear.addEventListener("click", () => {
linkTarget = null;
showLink();
area.focus();
});
linkBox.append(el("span", "", `Links to ${isGroup(linkTarget) ? `the group ${linkTarget.title ?? ""}` : nodeName(linkTarget)}`), clear);
};
// What stands after # for the picked node or group: "12", "g3", or nothing.
const pickedId = () => (linkTarget ? targetId(linkTarget, isGroup(linkTarget)) : undefined);
for (const [kind, emoji] of ARROW_CHOICES) {
const pick = el("button", "ausboss-callout-pick");
pick.type = "button";
pick.title = `Add an arrow pointing ${kind}. Click a node first to link it.`;
pick.append(arrowIcon(kind));
// The text box keeps focus, so clicking an arrow does not end the edit.
pick.addEventListener("mousedown", (event) => event.preventDefault());
pick.addEventListener("click", () => {
area.setRangeText(arrowText(emoji, pickedId()), area.selectionStart, area.selectionEnd, "end");
linkTarget = null;
showLink();
area.focus();
});
bar.append(pick);
}
// Small print for the line the caret is on, or every line of a selection.
const small = el("button", "ausboss-callout-size", "Small");
small.type = "button";
small.title = "Make this line small print, for the details under the line above. Press again to make it normal.";
small.addEventListener("mousedown", (event) => event.preventDefault());
small.addEventListener("click", () => {
const next = toggleSmall(area.value, area.selectionStart, area.selectionEnd);
area.setRangeText(next.insert, next.from, next.to, "preserve");
area.setSelectionRange(next.selectionStart, next.selectionEnd);
area.focus();
});
bar.append(small);
// The selected words become a link: to the picked node or group, or to an
// address, which comes back selected so it can be pasted over.
const link = el("button", "ausboss-callout-size", "Link");
link.type = "button";
link.title = "Make the selected words a link. Click a node or a group first to link to it; otherwise paste the address of a page.";
link.addEventListener("mousedown", (event) => event.preventDefault());
link.addEventListener("click", () => {
const id = pickedId();
const next = wrapLink(area.value, area.selectionStart, area.selectionEnd, id ? `#${id}` : "");
area.setRangeText(next.insert, next.from, next.to, "preserve");
area.setSelectionRange(next.selectionStart, next.selectionEnd);
linkTarget = null;
showLink();
area.focus();
});
bar.append(link);
bar.append(linkBox);
showLink();
wrap.append(area, bar);
let done = false;
const finish = (keep) => {
if (done) return;
done = true;
const value = area.value;
state.editor = null;
watch.abort();
wrap.remove();
if (keep && value !== state.valueWidget.value) {
commitWidgetValue(state.node, state.valueWidget, value, app.canvas);
}
renderText(state);
markChanged();
};
area.addEventListener("keydown", (event) => {
keepKeyInField(event);
if (event.key === "Escape") finish(false);
});
area.addEventListener("keyup", (event) => event.stopPropagation());
// An address pasted over selected words makes them a link to it.
area.addEventListener("paste", (event) => {
const selected = area.value.slice(area.selectionStart, area.selectionEnd);
const made = pastedLink(selected, event.clipboardData?.getData("text/plain"));
if (!made) return;
event.preventDefault();
area.setRangeText(made, area.selectionStart, area.selectionEnd, "end");
});
// A press on the editor's own bar keeps the keyboard in the text box.
wrap.addEventListener("mousedown", (event) => {
if (event.target !== area) event.preventDefault();
});
wrap.addEventListener("pointerdown", (event) => event.stopPropagation());
wrap.addEventListener("wheel", (event) => event.stopPropagation(), { passive: true });
// Clicking another node on the canvas takes the keyboard from the text box.
// That is how the link is picked, so the edit stays open then; a click on
// empty canvas, or on the note itself, ends it.
const watch = new AbortController();
let pointerDown = false;
window.addEventListener("pointerdown", () => { pointerDown = true; }, { capture: true, signal: watch.signal });
const settle = () => {
if (done) return;
const others = [...(app.canvas?.selectedItems ?? [])].filter((item) => item !== state.node && item?.pos && item?.size);
const stays = others.length === 1 && !app.canvas.selectedItems.has?.(state.node);
if (stays) {
linkTarget = others[0];
showLink();
area.focus();
} else {
finish(true);
}
};
window.addEventListener("pointerup", () => {
pointerDown = false;
if (waiting) {
waiting = false;
setTimeout(settle, 40);
}
}, { capture: true, signal: watch.signal });
let waiting = false;
area.addEventListener("blur", () => {
if (done) return;
if (pointerDown) waiting = true;
else finish(true);
});
state.editor = wrap;
state.card.append(wrap);
area.focus();
}
function buildPanel(node) {
if (node.__ausbossCallout) return node.__ausbossCallout;
const valueWidget = node.widgets?.find((item) => item.name === "text");
if (!valueWidget) return null;
ensureCss();
hideWidget(valueWidget);
const root = el("div", "ausboss-callout");
const card = el("div", "ausboss-callout-card");
const inner = el("div", "ausboss-callout-inner");
const tools = el("div", "ausboss-callout-tools");
const edit = el("button", "ausboss-callout-tool", "Edit");
edit.type = "button";
edit.title = "Edit the text. Double-clicking the note does the same.";
tools.append(edit);
card.append(inner, tools);
root.append(card);
tools.addEventListener("pointerdown", (event) => event.stopPropagation());
const panelHeight = () => (card.dataset.tile ? 36 : PANEL_MIN_HEIGHT) + WIDGET_FRAME;
const domWidget = node.addDOMWidget(WIDGET_NAME, "ausboss_callout", root, {
serialize: false,
hideOnZoom: false,
getMinHeight: panelHeight,
});
keepDomWidgetWidthAuto(domWidget);
domWidget.serialize = false;
fillNodeHeight(domWidget, {
minWidth: PANEL_MIN_WIDTH,
minHeight: panelHeight,
minNodeSize: [PANEL_MIN_WIDTH, PANEL_MIN_HEIGHT + 40],
exactMinWidth: true,
});
const state = { node, valueWidget, domWidget, root, card, inner, empty: true, editor: null, frame: 0 };
node.__ausbossCallout = state;
edit.addEventListener("click", () => startEdit(state));
card.addEventListener("dblclick", (event) => {
if (card.dataset.tile && !event.target.closest("a,button,textarea,.ausboss-callout-editor")) startEdit(state);
});
state.observer = new ResizeObserver(() => {
cancelAnimationFrame(state.frame);
state.frame = requestAnimationFrame(() => fit(state));
});
state.observer.observe(card);
node.setSize?.([
Math.max(node.size?.[0] ?? 0, DEFAULT_SIZE[0]),
Math.max(node.size?.[1] ?? 0, DEFAULT_SIZE[1]),
]);
renderText(state);
return state;
}
app.registerExtension({
name: "AusBoss.Callout",
setup() {
// A note copied with what its links name follows the copies.
onPaste(({ nodes, groups, created }) => {
for (const node of created) {
if (!CALLOUT_TYPES.has(node?.comfyClass) && !CALLOUT_TYPES.has(node?.type)) continue;
const state = buildPanel(node);
if (!state) continue;
const text = repointLinks(state.valueWidget.value, nodes, groups);
if (text === null) continue;
state.valueWidget.value = text;
renderText(state);
}
});
},
beforeRegisterNodeDef(nodeType, nodeData) {
if (!CALLOUT_TYPES.has(nodeData?.name)) return;
hideInputsInDef(nodeData, ["text"]);
chainCallback(nodeType.prototype, "onNodeCreated", function () {
if (nodeData.name === "AUSBOSS_NODES_CalloutTile") {
this.properties ??= {};
this.properties.ausbossCalloutTile ??= "title";
}
buildPanel(this);
// Keep new tiles compact without changing ordinary or saved callouts.
if (nodeData.name === "AUSBOSS_NODES_CalloutTile") this.setSize?.([380, 80]);
});
chainCallback(nodeType.prototype, "onSerialize", function (saved) {
serializeNoteIdentity(saved, ["youtube", "discord", "title"].includes(this.properties?.ausbossCalloutTile));
});
chainCallback(nodeType.prototype, "onConfigure", function (saved) {
// Saved values land after onNodeCreated; show them.
queueMicrotask(() => {
const state = buildPanel(this);
if (!state) return;
renderText(state);
if (state.card.dataset.tile && Number.isFinite(saved?.size?.[1])) {
this.setSize?.([this.size[0], saved.size[1]]);
}
});
});
chainCallback(nodeType.prototype, "onDblClick", function () {
const state = this.__ausbossCallout;
if (state) startEdit(state);
});
chainCallback(nodeType.prototype, "getExtraMenuOptions", function (_canvas, options) {
const state = this.__ausbossCallout;
if (!state || !Array.isArray(options)) return;
for (const [style, label] of [["plain","Plain callout"],["title","Title tile"],["youtube","Set YouTube link tile"],["discord","Set Discord link tile"]]) {
options.unshift({ content: label, callback: () => {
this.properties ??= {};
this.properties.ausbossCalloutTile = style;
const preset = LINK_TILES[style];
if (preset) commitWidgetValue(this, state.valueWidget, `[${preset.label}](${preset.url})`, app.canvas);
renderText(state);
if (style !== "plain") this.setSize?.([this.size[0], style === "title" ? 86 : 78]);
markChanged();
}});
}
options.unshift({ content: "Edit callout text", callback: () => startEdit(state) }, null);
});
chainCallback(nodeType.prototype, "onRemoved", function () {
const state = this.__ausbossCallout;
if (!state) return;
state.observer?.disconnect();
state.editor?.close?.();
cancelAnimationFrame(state.frame);
this.__ausbossCallout = null;
});
},
});
+142
View File
@@ -0,0 +1,142 @@
// Graph zoom and pan over the pack's panels, and files dropped on them.
//
// The mouse wheel and the Ctrl + Shift drag-zoom shortcut reach the graph only
// from the empty canvas; a panel that takes the mouse swallowed them, so
// zooming stopped over Compare, the crop previews, the video viewers and every
// control on a card. One listener here hands those gestures back, the same way
// ComfyUI does for its own text boxes. A panel that wants the wheel for itself
// (a scrolling list, a focused text field, an editor) still keeps it.
//
// A file dragged in from the desktop, or a picture's card from the Assets
// panel, has the same problem in the classic renderer: dropped on a node's
// title it loads into the node, dropped on the node's panel (the picture, the
// controls) it missed the node. The drop listeners here hand it to the node
// under the pointer.
import { app } from "../../../scripts/app.js";
import {
dragCarriesFiles, graphDragStarts, panelHost, panelKeepsWheel, panelNeedsDropHelp, panelRoot,
} from "../shared/canvas_passthrough.mjs";
import { showToast } from "../shared/index.mjs";
let forwardedPointer = null;
// The node a dragged file is over while the pointer is on its panel.
let dropNode = null;
function canvas() {
return app.canvas ?? null;
}
function onWheel(event) {
const target = event.target;
const root = panelRoot(target);
const lgCanvas = canvas();
if (!root || !lgCanvas || event.defaultPrevented) return;
if (panelKeepsWheel(target, event, { root, host: panelHost(target), styleOf: (el) => getComputedStyle(el), activeElement: document.activeElement })) return;
event.preventDefault();
lgCanvas.processMouseWheel(event);
}
function onPointerDown(event) {
const lgCanvas = canvas();
if (!lgCanvas || !panelRoot(event.target)) return;
if (!graphDragStarts(event, { dragZoomEnabled: lgCanvas.dragZoomEnabled })) return;
forwardedPointer = event.pointerId;
event.preventDefault();
event.stopPropagation();
lgCanvas.processMouseDown(event);
}
function onPointerMove(event) {
if (forwardedPointer !== event.pointerId) return;
canvas()?.processMouseMove(event);
}
function onPointerEnd(event) {
if (forwardedPointer !== event.pointerId) return;
forwardedPointer = null;
canvas()?.processMouseUp(event);
}
// The node a panel belongs to, found the way the canvas finds the node under
// a drag: by where the pointer is.
function nodeUnder(event) {
const lgCanvas = canvas();
const graph = lgCanvas?.graph;
if (!graph || typeof lgCanvas.adjustMouseEvent !== "function") return null;
lgCanvas.adjustMouseEvent(event);
if (!Number.isFinite(event.canvasX) || !Number.isFinite(event.canvasY)) return null;
return graph.getNodeOnPos?.(event.canvasX, event.canvasY) ?? null;
}
// The node that takes this dragged file, or null when the drag is not ours
// to help with: no files, not over one of the pack's classic panels, or a
// node that does not take files.
function takerOf(event) {
if (!dragCarriesFiles(event) || !panelNeedsDropHelp(event.target)) return null;
const node = nodeUnder(event);
if (typeof node?.onDragDrop !== "function" || !node.onDragOver?.(event)) return null;
return node;
}
// ComfyUI outlines the node a file is dragged over; keep that outline while
// the pointer is on the node's panel.
function markDropNode(node) {
if (dropNode === node) return;
dropNode = node;
if ("dragOverNode" in app) app.dragOverNode = node;
canvas()?.graph?.setDirtyCanvas?.(false, true);
}
function onDragOver(event) {
const node = takerOf(event);
if (!node) {
if (dropNode && panelRoot(event.target)) markDropNode(null);
return;
}
// Without this the browser refuses the drop.
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
markDropNode(node);
}
async function onDrop(event) {
const node = takerOf(event);
if (!node) return;
// The node gets the file. ComfyUI's own handler would add a Load Image
// node for it, or open it as a workflow.
event.preventDefault();
event.stopPropagation();
markDropNode(null);
const file = event.dataTransfer?.files?.[0] ?? null;
try {
if (await node.onDragDrop(event)) return;
// Not a file this node takes (a workflow dropped on a loader): let
// ComfyUI open it, as it does for a drop on the node's title.
if (file && file.type !== "image/bmp") await app.handleFile?.(file);
} catch (error) {
showToast({ severity: "error", detail: `That file could not be loaded: ${error?.message ?? error}`, life: 8000 });
}
}
function onDragEnd() {
if (dropNode) markDropNode(null);
}
app.registerExtension({
name: "AusBoss.CanvasPassthrough",
setup() {
// The wheel listener sits in the bubble phase so a panel's own handler
// (stopPropagation / preventDefault) always gets the first say.
window.addEventListener("wheel", onWheel, { passive: false });
window.addEventListener("pointerdown", onPointerDown, true);
window.addEventListener("pointermove", onPointerMove, true);
window.addEventListener("pointerup", onPointerEnd, true);
window.addEventListener("pointercancel", onPointerEnd, true);
// Capture phase: ComfyUI's own drop listener sits on the document and
// must not also act on a file the node took.
window.addEventListener("dragover", onDragOver, true);
window.addEventListener("drop", onDrop, true);
window.addEventListener("dragend", onDragEnd, true);
window.addEventListener("dragleave", (event) => { if (!event.relatedTarget) onDragEnd(); }, true);
},
});
+41 -10
View File
@@ -1,7 +1,8 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { AUSBOSS_JS_VERSION, BRAND, BRAND_BODY, chainCallback } from "../shared/index.mjs";
import { nodeByExecutionId } from "../shared/graph_ids.mjs";
import { isOwnRun, ownRuns } from "../shared/prompt_scope.mjs";
import {
advanceExecution,
applyStatus,
@@ -187,6 +188,7 @@ const runtime = {
enabled: false,
abort: null,
state: createRunState(),
kept: null,
};
function graphNodes() {
@@ -217,34 +219,62 @@ function stampUpdates(updates) {
if (updates.length) app.graph?.setDirtyCanvas?.(true, false);
}
// The badges a run cleared when it started, kept until it is clear that the
// run is one of the workflow's: a node's own run (Mask by Touch looking at
// its picture) is only known as one once the queue has answered.
function keepBadges(promptId) {
const badges = [];
for (const node of graphNodes()) {
if (node[RUNTIME_KEY] !== undefined) badges.push([node, node[RUNTIME_KEY]]);
}
runtime.kept = { promptId: promptId ? String(promptId) : null, badges };
}
function enableRuntime() {
if (runtime.abort) return;
runtime.abort = new AbortController();
const signal = runtime.abort.signal;
const closeRun = () => stampUpdates(advanceExecution(runtime.state, null, performance.now()));
// A run a node starts for itself leaves the last run's badges alone.
const workflowRun = (handler) => (event) => {
const detail = event?.detail;
if (!isOwnRun(detail && typeof detail === "object" ? detail.prompt_id : null)) handler(event);
};
const closeRun = workflowRun(() => stampUpdates(advanceExecution(runtime.state, null, performance.now())));
api.addEventListener(
"execution_start",
() => {
workflowRun((event) => {
keepBadges(event?.detail?.prompt_id);
runtime.state = createRunState();
clearBadges();
},
}),
{ signal },
);
api.addEventListener(
"executing",
(event) =>
stampUpdates(advanceExecution(runtime.state, executingNodeId(event?.detail), performance.now())),
workflowRun((event) =>
stampUpdates(advanceExecution(runtime.state, executingNodeId(event?.detail), performance.now()))),
{ signal },
);
api.addEventListener("execution_success", closeRun, { signal });
api.addEventListener("execution_error", closeRun, { signal });
api.addEventListener("execution_interrupted", closeRun, { signal });
const stop = ownRuns.subscribe((promptId) => {
const kept = runtime.kept;
if (!kept || kept.promptId !== promptId) return;
runtime.kept = null;
runtime.state = createRunState();
clearBadges();
for (const [node, seconds] of kept.badges) node[RUNTIME_KEY] = seconds;
app.graph?.setDirtyCanvas?.(true, false);
});
signal.addEventListener("abort", stop);
}
function disableRuntime() {
runtime.abort?.abort();
runtime.abort = null;
runtime.state = createRunState();
runtime.kept = null;
try {
clearBadges();
app.graph?.setDirtyCanvas?.(true, false);
@@ -334,9 +364,10 @@ function drawNodeBadge(node, ctx) {
const padX = 5;
const height = 15;
const width = Math.ceil(ctx.measureText(badge.text).width) + padX * 2;
// Top-right, floating just above the title bar so it never covers
// the node title or its widgets.
const x = node.size[0] - width;
// Top-left, floating just above the title bar so it never covers the
// node title or its widgets. The right side belongs to the frontend's
// own node-source badge, which sits at the same height.
const x = 0;
const y = -titleHeight - height - 4;
roundedRectPath(ctx, x, y, width, height, 4);
ctx.fillStyle = "rgba(8, 20, 19, 0.85)";
+364 -92
View File
@@ -1,24 +1,48 @@
import { api } from "/scripts/api.js";
import { app } from "/scripts/app.js";
import { BRAND, chainCallback, keepDomWidgetWidthAuto } from "../shared/index.mjs";
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { isForeignRun, outputKey } from "../shared/prompt_scope.mjs";
import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "../shared/index.mjs";
import { lookupLink } from "../shared/graph_links.mjs";
import { WIDGET_FRAME, fillNodeHeight, growToNodeFloor } from "../shared/panel_layout.mjs";
import { mediaViewQuery, responsivePreviewHeight } from "../shared/video_preview.mjs";
import { VIDEO_MIN_WIDTH, ensureVideoCss, makeToolButton } from "../shared/video_ui.mjs";
import { makeSlideSwitch } from "../shared/slide_switch.mjs";
import { makePictureStepper } from "../shared/picture_stepper.mjs";
import { keepIndex, stepIndex } from "../shared/picture_steps.mjs";
import { VIDEO_MIN_WIDTH, ensureVideoCss } from "../shared/video_ui.mjs";
import {
clipFraction,
slideFraction,
compareBadges,
compareClip,
findCompareImages,
compareResultSide,
compareSizeLabel,
compareSwitchValue,
findComparePairs,
normalizeCompareMode,
sizeAfterErase,
} from "../shared/compare.mjs";
import { CHECKER, backdropPadding, savedLinks } from "../shared/compare_erase.mjs";
import { ERASE_CSS, makeEraser } from "../shared/compare_erase_stage.mjs";
const NODE_NAME = "AUSBOSS_NODES_Compare";
const PANEL_WIDGET = "ausboss_compare_panel";
const PANEL_CHROME = 12;
const CSS_ID = "ausboss-compare-ui-v1";
const MODE_HINTS = {
slide: "Move across the panel to reveal B",
hold: "Press and hold to see B",
};
// The stage floor plus the caption row that now sits under it.
const CAPTION_HEIGHT = 24;
const PANEL_MIN_HEIGHT = 144 + CAPTION_HEIGHT;
// Height the node opens at. It used to fall out of computeSize; with the panel
// now free to grow, the default has to be stated somewhere, and a 16:9-ish
// stage is the shape most A/B pairs want.
const DEFAULT_NODE_SIZE = [
420,
responsivePreviewHeight(420, 132, 520) + PANEL_CHROME + CAPTION_HEIGHT + 60,
];
const CSS_ID = "ausboss-compare-ui-v6";
const ROOT_GAP = 6;
const FOOTER_GAP = 8;
const BADGE_WIDTH = 22;
const REST_SETTING = "AusBoss.Compare.RestOnResult";
const SIZE_BEFORE_ERASE = "ausboss_compare_size_before_erase";
let restOnResult = true;
function ensureCompareCss() {
ensureVideoCss(); // tool button styles are shared with the video panels
@@ -26,16 +50,26 @@ function ensureCompareCss() {
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
.ausboss-compare-root{box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:6px;padding:2px 6px 6px;color:#d8eeee;font:11px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden;}
.ausboss-compare-root{box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:${ROOT_GAP}px;padding:2px 6px 6px;color:#d8eeee;font:11px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow:hidden;}
.ausboss-compare-stage{position:relative;flex:1 1 auto;min-height:112px;overflow:hidden;border:1px solid rgba(0,180,170,.34);border-radius:6px;background:#000;box-shadow:inset 0 0 0 1px rgba(255,255,255,.025);touch-action:none;}
.ausboss-compare-stage img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;user-select:none;-webkit-user-drag:none;}
.ausboss-compare-stage.is-empty img{visibility:hidden;}
.ausboss-compare-stage img.has-alpha{box-sizing:border-box;background-image:repeating-conic-gradient(${CHECKER.light} 0 25%,${CHECKER.dark} 0 50%);background-size:${CHECKER.side * 2}px ${CHECKER.side * 2}px;background-origin:content-box;background-clip:content-box;}
.ausboss-compare-seam{position:absolute;top:0;bottom:0;z-index:2;width:1px;margin-left:-0.5px;background:${BRAND};box-shadow:0 0 4px rgba(0,180,170,.55);opacity:0;pointer-events:none;}
.ausboss-compare-status{position:absolute;left:7px;top:7px;z-index:3;max-width:calc(100% - 112px);padding:3px 6px;border-radius:4px;background:rgba(0,0,0,.7);color:#b8d3d1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none;backdrop-filter:blur(4px);}
/* Once the previews are up the status has nothing to say, and an empty chip
over the corner of the picture is just something in the way. */
.ausboss-compare-status:empty{display:none;}
.ausboss-compare-stage.is-empty .ausboss-compare-status{left:50%;top:50%;max-width:82%;transform:translate(-50%,-50%);color:#78908e;text-align:center;white-space:normal;}
.ausboss-compare-tools{position:absolute;right:6px;top:6px;z-index:4;display:flex;gap:4px;opacity:.9;}
.ausboss-compare-tools:hover{opacity:1;}
`;
.ausboss-compare-footer{display:flex;align-items:center;gap:${FOOTER_GAP}px;flex:none;height:24px;}
.ausboss-compare-identity{position:absolute;left:0;right:0;top:7px;height:22px;z-index:3;pointer-events:none;}
.ausboss-compare-identity span{position:absolute;top:0;width:${BADGE_WIDTH}px;height:22px;border-radius:4px;background:rgba(0,0,0,.7);color:#d8eeee;font-weight:600;text-align:center;line-height:22px;}
.ausboss-compare-identity span[hidden]{display:none;}
.ausboss-compare-stage.is-empty .ausboss-compare-identity{display:none;}
.ausboss-compare-caption{flex:1;min-width:0;height:${CAPTION_HEIGHT}px;overflow:hidden;color:#8ba3a1;font-size:10px;line-height:${CAPTION_HEIGHT}px;text-align:center;white-space:nowrap;text-overflow:ellipsis;}
.ausboss-compare-caption a{color:${BRAND};font-weight:700;text-decoration:none;cursor:pointer;}
.ausboss-compare-caption a:hover{text-decoration:underline;}
${ERASE_CSS}`;
document.head.appendChild(style);
}
@@ -47,42 +81,208 @@ function getMode(node) {
return node.properties.ausboss_compare_mode;
}
// One floor for every mode: Erase fits its rows into the node as it is, so
// picking it never moves the node's edge over its neighbours.
const PANEL_FLOOR = PANEL_MIN_HEIGHT + WIDGET_FRAME;
// A and B ride on either side of the split line (compareBadges), so each
// label sits on the picture it names wherever the line is.
function applyClip(state) {
const { clipPath, seamLeft, seamVisible } = compareClip(state.fraction);
state.imageB.style.clipPath = clipPath;
const { clipPath, seamLeft, seamVisible, showA, showB } = compareClip(state.fraction);
state.imageA.style.clipPath = clipPath;
// "" hands a picture back to the stylesheet, which hides both while the
// stage is empty.
state.imageA.style.visibility = showA ? "" : "hidden";
state.imageB.style.visibility = showB ? "" : "hidden";
state.seam.style.left = seamLeft;
state.seam.style.opacity = seamVisible ? "1" : "0";
const places = compareBadges(state.fraction, state.stage.clientWidth || 0, BADGE_WIDTH);
for (const [badge, left] of [[state.identityLeft, places.a], [state.identityRight, places.b]]) {
badge.hidden = left == null;
if (left != null) badge.style.left = `${left}px`;
}
state.identity.title = seamVisible ? "A on the left of the line · B on the right" : `Showing ${state.fraction === 0 ? "B" : "A"}`;
state.identity.setAttribute("aria-label", state.identity.title);
}
// A picture with see-through parts lies on a checkerboard. It is the
// picture's own background, held to the picture by padding the size of the
// bars beside it, so in Slide the cut that shows A hides B behind A's clear
// parts too.
function placeBackdrops(state) {
const { clientWidth, clientHeight } = state.stage;
for (const [image, ref] of [[state.imageA, state.refs?.a], [state.imageB, state.refs?.b]]) {
const clear = Boolean(ref?.alpha) && image.naturalWidth > 0;
image.classList.toggle("has-alpha", clear);
image.style.padding = clear ? backdropPadding(clientWidth, clientHeight, image.naturalWidth, image.naturalHeight) : "";
}
}
function setEmpty(state, text) {
state.stage.classList.add("is-empty");
state.status.textContent = text;
state.caption.textContent = "";
}
function setReady(state) {
state.stage.classList.remove("is-empty");
const mode = getMode(state.node);
const size = state.refs?.a?.width
? `${state.refs.a.width}×${state.refs.a.height} · `
: "";
state.status.textContent = `${size}${MODE_HINTS[mode]}`;
// Keep loading/error status separate from the image identity label.
state.status.textContent = "";
state.caption.textContent = compareSizeLabel(state.refs);
}
// After a save the caption names the files as links: ComfyUI often runs on
// another computer, and this is how the mix gets from there to an editor.
// The next pair of pictures puts the size back.
function showSaved(state, files) {
const links = savedLinks(files);
if (!links.length) return;
state.caption.textContent = "Saved ";
links.forEach(({ label, file }, index) => {
const link = document.createElement("a");
link.textContent = label;
link.href = api.apiURL(`/view?${mediaViewQuery(file, "output")}`);
link.download = file.filename;
link.title = `Download ${file.filename}`;
link.addEventListener("pointerdown", (event) => event.stopPropagation());
state.caption.append(index ? " · " : "", link);
});
}
function updateModeButtons(state) {
const mode = getMode(state.node);
state.slideButton.classList.toggle("active", mode === "slide");
state.holdButton.classList.toggle("active", mode === "hold");
if (!state.stage.classList.contains("is-empty")) setReady(state);
state.modeSwitch.set(compareSwitchValue(getMode(state.node), state.showingB));
}
// Hand the stage to the brush, or take it back, when the mode says so. The
// node keeps its size: Erase's rows wrap and take their room from the
// picture. Pack 2.12.1 grew a small node for Erase and kept the size from
// before with the workflow; a node saved like that still gets it back.
function syncErase(state) {
const node = state.node;
const on = getMode(node) === "erase";
if (state.eraser.active === on) return;
state.eraser.setActive(on);
if (on || !node.size) return;
const back = sizeAfterErase([...node.size], node.properties[SIZE_BEFORE_ERASE]);
delete node.properties[SIZE_BEFORE_ERASE];
if (back) node.setSize?.(back);
}
// In toggle mode the reveal is all or nothing.
function applyToggle(state) {
state.fraction = state.showingB ? 0 : 1;
applyClip(state);
updateModeButtons(state);
state.node.setDirtyCanvas?.(true, true);
}
// One graph's links, in the shape compareResultSide reads them.
function linkView(graph) {
const origin = (linkId) => {
const link = lookupLink(graph, linkId);
return link ? { id: String(link.origin_id), slot: link.origin_slot } : null;
};
return {
origin,
feeds: (id) => (graph?.getNodeById?.(id)?.inputs ?? []).map((input) => origin(input?.link)?.id),
inside(id) {
const subgraph = graph?.getNodeById?.(id)?.subgraph;
if (!subgraph) return null;
const view = linkView(subgraph);
return { ...view, output: (slot) => view.origin(subgraph.outputs?.[slot]?.linkIds?.[0]) };
},
};
}
// Which input holds the result, read from the graph the panel sits in.
function resultSide(node) {
const view = linkView(node.graph);
const source = (name) => view.origin(node.inputs?.find((input) => input?.name === name)?.link);
return compareResultSide(source("image_a"), source("image_b"), view);
}
// Slide mode rests on the result while the pointer is away.
function showResult(state) {
state.fraction = resultSide(state.node) === "B" ? 0 : 1;
applyClip(state);
}
// A click on the mode switch. The mode saves with the workflow; a flip
// between A and B within the lock does too, through the side it stores.
function pickMode(state, value) {
const node = state.node;
const before = compareSwitchValue(getMode(node), state.showingB);
if (value === "slide") {
node.properties.ausboss_compare_mode = normalizeCompareMode("slide");
state.showingB = false;
state.fraction = 1;
syncErase(state);
if (restOnResult) showResult(state);
else applyClip(state);
} else if (value === "erase") {
node.properties.ausboss_compare_mode = "erase";
syncErase(state);
} else {
// Keep the legacy mode key; the optional side property restores new locks.
node.properties.ausboss_compare_mode = "toggle";
node.properties.ausboss_compare_side = value === "b" ? "B" : "A";
state.showingB = value === "b";
syncErase(state);
applyToggle(state);
}
updateModeButtons(state);
node.setDirtyCanvas?.(true, true);
if (value !== before) notifyAusbossChange();
}
function loadPreviews(state, refs) {
state.refs = refs;
state.loaded = 0;
state.eraser.setPictures(null);
setEmpty(state, "Loading previews…");
// A new pair opens on the result, unless a lock or the pointer holds the view.
if (restOnResult && getMode(state.node) === "slide" && !state.stage.matches(":hover")) showResult(state);
state.imageA.src = api.apiURL(`/view?${mediaViewQuery(refs.a, "temp")}`);
state.imageB.src = api.apiURL(`/view?${mediaViewQuery(refs.b, "temp")}`);
}
// The arrows need room beside the mode switch. While they show, the node's
// floor is what the footer's fixed parts take, measured; the caption gives
// way first.
function compareMinWidth(state) {
if (!state || state.stepper.root.hidden) return VIDEO_MIN_WIDTH;
const parts = state.stepper.root.offsetWidth + state.modeSwitch.root.offsetWidth;
if (!parts || !state.footer.clientWidth) return VIDEO_MIN_WIDTH;
const inset = state.node.size[0] - state.footer.clientWidth;
return Math.max(VIDEO_MIN_WIDTH, Math.ceil(inset + parts + 2 * FOOTER_GAP));
}
// A run's pairs. The panel shows one, and the arrows step through the rest:
// a list of pictures runs the node once per picture.
function showPairs(state, pairs) {
const hidden = state.stepper.root.hidden;
state.pairs = pairs;
state.index = keepIndex(state.index, pairs.length);
state.stepper.set(state.index, pairs.length);
if (state.stepper.root.hidden !== hidden) growToNodeFloor(state.widget);
if (pairs.length) loadPreviews(state, pairs[state.index]);
}
// Align swaps B for B lined up with A, or back: the pair on screen changes,
// and so does the one the arrows come back to.
function showAligned(state, refs) {
if (!refs) return;
if (state.pairs[state.index]) state.pairs[state.index] = refs;
loadPreviews(state, refs);
}
function stepPair(state, step) {
if (state.pairs.length < 2) return;
state.index = stepIndex(state.index, state.pairs.length, step);
state.stepper.set(state.index, state.pairs.length);
loadPreviews(state, state.pairs[state.index]);
}
function buildPanel(node) {
if (node.__ausbossCompare) return node.__ausbossCompare;
ensureCompareCss();
@@ -98,122 +298,194 @@ function buildPanel(node) {
const status = document.createElement("div");
status.className = "ausboss-compare-status";
status.textContent = "Run to load the A/B previews";
const tools = document.createElement("div");
tools.className = "ausboss-compare-tools";
const slideButton = makeToolButton("SLIDE", "Slide: the seam follows the pointer across the image");
const holdButton = makeToolButton("HOLD", "Hold: press and hold anywhere to see B, release for A");
tools.append(slideButton, holdButton);
stage.append(imageA, imageB, seam, status, tools);
root.append(stage);
const identity = document.createElement("div");
identity.className = "ausboss-compare-identity";
const identityLeft = document.createElement("span");
const identityRight = document.createElement("span");
identityLeft.textContent = "A";
identityRight.textContent = "B";
identity.append(identityLeft, identityRight);
const abort = new AbortController();
const signal = abort.signal;
// One switch for what the panel does: slide across the pair, hold one of
// the two, or mix them with the brush. Its marker slides to the choice.
const modeSwitch = makeSlideSwitch({
label: "What the panel does",
// Each choice as wide as its name, so the row still fits a narrow node.
fit: true,
options: [
{ value: "slide", label: "Slide", title: "Slide: the line follows the pointer across the picture, A on its left and B on its right" },
{ value: "a", label: "A", title: "Show picture A and hold it" },
{ value: "b", label: "B", title: "Show picture B and hold it" },
{ value: "erase", label: "Erase", title: "Erase: rub one picture away with a brush so the other shows through, then save the mix" },
],
value: "slide",
signal,
onChange: (value) => node.__ausbossCompare && pickMode(node.__ausbossCompare, value),
});
stage.append(imageB, imageA, seam, status, identity);
const caption = document.createElement("div");
caption.className = "ausboss-compare-caption";
const eraser = makeEraser({
node, root, stage, imageA, imageB, signal,
onSaved: (files) => showSaved(node.__ausbossCompare, files),
onAligned: (refs) => node.__ausbossCompare && showAligned(node.__ausbossCompare, refs),
});
const stepper = makePictureStepper({
noun: "pair",
signal,
onStep: (step) => node.__ausbossCompare && stepPair(node.__ausbossCompare, step),
});
const footer = document.createElement("div");
footer.className = "ausboss-compare-footer";
footer.append(stepper.root, caption, modeSwitch.root);
root.append(eraser.bar, stage, footer, eraser.saveRow);
const widget = node.addDOMWidget(PANEL_WIDGET, "ausboss_compare", root, {
serialize: false,
hideOnZoom: false,
getMinHeight: () => 132,
getMinHeight: () => PANEL_FLOOR,
});
keepDomWidgetWidthAuto(widget);
widget.computeSize = (width) => {
const resolvedWidth = Math.max(VIDEO_MIN_WIDTH, Number(width || node.size?.[0] || 360));
return [resolvedWidth, responsivePreviewHeight(resolvedWidth, 132, 520) + PANEL_CHROME];
};
widget.computeLayoutSize = () => ({ minWidth: VIDEO_MIN_WIDTH, minHeight: 144 });
widget.options ??= {};
widget.options.minNodeSize = [VIDEO_MIN_WIDTH, 220];
// Not saved with the workflow either: options.serialize only keeps it out
// of the prompt, and saved values come back by position.
widget.serialize = false;
// + WIDGET_FRAME: the frontend insets the element, so a bare floor hands
// the panel fewer CSS pixels than the stage + caption minimums and shaves
// the caption's glyphs at the node's minimum height.
fillNodeHeight(widget, {
minWidth: () => compareMinWidth(node.__ausbossCompare),
minHeight: PANEL_FLOOR,
minNodeSize: [VIDEO_MIN_WIDTH, 220],
exactMinWidth: true,
});
const abort = new AbortController();
const state = node.__ausbossCompare = {
node, root, stage, imageA, imageB, seam, status, slideButton, holdButton,
widget, abort, refs: null, fraction: 0, loaded: 0, holding: false,
node, root, stage, imageA, imageB, seam, status, caption, identity, identityLeft, identityRight,
footer, modeSwitch, eraser, stepper,
widget, abort, refs: null, pairs: [], index: 0, fraction: 1, loaded: 0, showingB: false,
};
applyClip(state);
updateModeButtons(state);
const signal = abort.signal;
const setMode = (mode) => (event) => {
event.preventDefault();
event.stopPropagation();
node.properties.ausboss_compare_mode = normalizeCompareMode(mode);
if (mode !== "hold") state.holding = false;
state.fraction = 0;
// The labels are placed in pixels, so they follow the stage's width.
const resized = new ResizeObserver(() => {
applyClip(state);
updateModeButtons(state);
node.setDirtyCanvas?.(true, true);
};
slideButton.addEventListener("click", setMode("slide"), { signal });
holdButton.addEventListener("click", setMode("hold"), { signal });
// The panel owns pointer movement only inside itself; pointerdown is never
// prevented, so dragging the node from its title keeps working. Events
// born in the tools bar never reach the stage behaviors — in hold mode a
// stage pointerdown captures the pointer, which would retarget the
// release and eat the button's click (the "stuck on HOLD" bug).
placeBackdrops(state);
});
resized.observe(stage);
signal.addEventListener("abort", () => resized.disconnect());
// Movement belongs to the preview only; graph dragging and panning remain
// untouched. A/B locks ignore pointer movement and exits.
stage.addEventListener("pointermove", (event) => {
if (tools.contains(event.target)) return;
if (!state.refs || getMode(node) !== "slide" || state.holding) return;
if (!state.refs || getMode(node) !== "slide") return;
const rect = stage.getBoundingClientRect();
state.fraction = clipFraction(event.clientX, rect.left, rect.width);
state.fraction = slideFraction(event.clientX, rect.left, rect.width);
applyClip(state);
}, { signal });
stage.addEventListener("pointerdown", (event) => {
if (tools.contains(event.target)) return;
if (!state.refs || getMode(node) !== "hold") return;
state.holding = true;
state.fraction = 1;
// Back to the result on every exit, including top/bottom. With the
// setting off, settle to the nearest full image instead.
stage.addEventListener("pointerleave", (event) => {
if (!state.refs || getMode(node) !== "slide") return;
if (restOnResult) return showResult(state);
const rect = stage.getBoundingClientRect();
state.fraction = slideFraction(event.clientX, rect.left, rect.width, true);
applyClip(state);
try {
stage.setPointerCapture(event.pointerId);
} catch {
// Pointer capture is best-effort; release still arrives via the
// window when the browser refuses it.
}
}, { signal });
const endHold = () => {
if (!state.holding) return;
state.holding = false;
state.fraction = 0;
applyClip(state);
};
stage.addEventListener("pointerup", endHold, { signal });
stage.addEventListener("pointercancel", endHold, { signal });
const onImageLoad = () => {
placeBackdrops(state);
state.loaded += 1;
if (state.loaded >= 2) {
setReady(state);
eraser.setPictures(state.refs);
node.setDirtyCanvas?.(true, true);
}
};
imageA.addEventListener("load", onImageLoad, { signal });
imageB.addEventListener("load", onImageLoad, { signal });
const onImageError = () => setEmpty(state, "A compare preview could not load");
const onImageError = () => {
eraser.setPictures(null);
setEmpty(state, "A compare preview could not load");
};
imageA.addEventListener("error", onImageError, { signal });
imageB.addEventListener("error", onImageError, { signal });
return state;
}
// Going back to a workflow tab puts its results back in ComfyUI's store,
// but the panel only ever filled from onExecuted. Show the stored pair.
function showStoredResult(node) {
const state = node.__ausbossCompare;
if (!state || state.refs) return;
showPairs(state, findComparePairs(app.nodeOutputs?.[outputKey(node, app.rootGraph ?? app.graph)]));
}
app.registerExtension({
name: "ausboss.compare.panel",
settings: [
{
id: REST_SETTING,
name: "Rest on the result",
type: "boolean",
defaultValue: true,
category: ["🆎 AusBoss", "Image Compare", "Rest on the result"],
tooltip:
"While the pointer is away, Image Compare A/B shows the result: "
+ "after every run, and again each time the pointer leaves the "
+ "picture. Off: it stays on the picture nearest to where the "
+ "pointer left, and a new run does not change it.",
onChange(value) {
restOnResult = value !== false;
},
},
],
setup() {
// onChange only fires on later edits, so seed from the stored value here.
restOnResult = app.ui?.settings?.getSettingValue?.(REST_SETTING) !== false;
},
onNodeOutputsUpdated() {
setTimeout(() => {
for (const node of app.graph?._nodes ?? []) if (node?.comfyClass === NODE_NAME) showStoredResult(node);
}, 0);
},
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_NAME) return;
chainCallback(nodeType.prototype, "onNodeCreated", function () {
buildPanel(this);
// Only for a genuinely new node: onConfigure restores a saved size after
// this runs, so a workflow's own dimensions still win.
this.setSize?.([
Math.max(DEFAULT_NODE_SIZE[0], this.size?.[0] ?? 0),
Math.max(DEFAULT_NODE_SIZE[1], this.size?.[1] ?? 0),
]);
});
chainCallback(nodeType.prototype, "onConfigure", function () {
const state = this.__ausbossCompare;
queueMicrotask(() => {
const state = buildPanel(this);
updateModeButtons(state);
if (!state || this.__ausbossCompare !== state || state.abort.signal.aborted) return;
state.showingB = this.properties?.ausboss_compare_side === "B";
syncErase(state);
// The saved paint, if the pictures are already up.
if (state.refs && state.loaded >= 2) state.eraser.setPictures(state.refs);
if (getMode(this) === "toggle") applyToggle(state);
else updateModeButtons(state);
});
});
chainCallback(nodeType.prototype, "onExecuted", function (message) {
if (isForeignRun()) return;
const state = buildPanel(this);
const refs = findCompareImages(message);
if (refs) loadPreviews(state, refs);
else setEmpty(state, "Execution finished without compare previews");
const pairs = findComparePairs(message);
showPairs(state, pairs);
if (!pairs.length) setEmpty(state, "Execution finished without compare previews");
app.canvas?.setDirty?.(true, true);
});
// A link on the filename input decides whether the name is typed here.
chainCallback(nodeType.prototype, "onConnectionsChange", function () {
this.__ausbossCompare?.eraser?.refresh();
});
chainCallback(nodeType.prototype, "onRemoved", function () {
this.__ausbossCompare?.abort?.abort();
this.__ausbossCompare = null;
+27 -10
View File
@@ -8,20 +8,36 @@ cleanly; this node makes any input legal without hand-typing resolutions.
## Controls
- **image**: BHWC batch to align; every frame is treated the same.
- **multiple**: Both output sides come out divisible by this. `16` or `32`
suits most diffusion models; latent nodes generally want at least `8`.
- **mode**: How the multiple is reached. Each mode implies its own rounding
direction, so there is no separate rounding widget:
- **Multiple** (`multiple`, starts at `16`): Both output sides come out
divisible by this. `16` or `32` suits most diffusion models; latent nodes
generally want at least `8`.
- **Mode** (`mode`): How the multiple is reached. Each mode implies its own
rounding direction, so there is no separate rounding widget:
- **resize** — rescale to the *nearest* multiple. A slight stretch, but
every pixel's content survives. The default.
- **crop** — center-crop *down* to the next multiple. No distortion; a few
edge pixels are trimmed.
- **pad** — replicate-pad the edges *up* to the next multiple, split
evenly. No distortion; a few edge pixels are invented by repetition.
- **crop** — crop *down* to the next multiple. No distortion; a few edge
pixels are trimmed where **Anchor** says.
- **pad** — pad the edges *up* to the next multiple. No distortion; the
new pixels land where **Anchor** says and are filled per **Pad fill**.
- **Anchor** (`crop_position`): Crop mode only — which part of the frame
survives. `center` trims both edges evenly; `top`, `bottom`, `left`, or
`right` pin that edge and trim the opposite one. Ignored by resize and pad.
- **Anchor** (`pad_position`): Pad mode only — where the image sits on the
grown canvas; the new pixels land on the opposite side. `center` splits
them evenly. Same vocabulary as `crop_position`.
- **Pad fill** (`pad_fill`): Pad mode only — what fills the new area.
`replicate` stretches the edge pixels out; `color` uses **Pad color** as a
solid.
- **Pad color** (`pad_color`, starts black): The solid fill, shown only
while **Pad fill** is `color`. Hex, `R,G,B` numbers, or a CSS color name;
a color the node cannot read becomes mid-gray.
The two **Anchor** rows, **Pad fill** and **Pad color** only show in the
mode that uses them.
A side already smaller than one multiple can never be cropped legal, so it
snaps up to exactly one multiple in every mode (replicate-padded when the
mode would otherwise shrink it).
snaps up to exactly one multiple in every mode (in crop mode the deficit is
replicate-padded evenly, whatever the **Anchor**).
## Outputs
@@ -30,6 +46,7 @@ mode would otherwise shrink it).
Latent, a resize, or conditioning nodes so the numbers can never drift
from the actual image.
## Notes
At most `multiple - 1` pixels are added or removed per side, so the change
+94
View File
@@ -0,0 +1,94 @@
# Callout
A note for the canvas. Type a line or two and the text grows to fill the
note, so people can read it without zooming in. Type an arrow emoji such as ⬆️
to point at the part you are talking about.
Use it to explain a step in a workflow you share. It never runs and has no
wires.
## Tile presets
Right-click the Callout and choose **Title tile** to give your existing text a black heading with a teal accent. **Set YouTube link tile** and **Set Discord link tile** fill in the AusBoss link and label. Click the label to open the link, or drag the tile to move it. Choose **Plain callout** to return to the standard style with the current text.
- Press **Edit** on a tile to open a larger editor. A link tile has separate **Button text** and **Link address** fields. Existing mixed text stays in a text box.
- Double-click a title to edit it. **Save** or `Ctrl+Enter` keeps the change; **Cancel** or `Esc` leaves the original text.
- Drag the visible bottom-right grip to resize. You can also focus the grip and use arrow keys; hold **Shift** for smaller changes. Resizing keeps the tile in place and supports Undo.
## Writing
- On a plain callout, double-click the note, or press **Edit**, to type. Click outside the box to
keep the text. **Esc** drops what you typed.
- A blank line starts a new point. Each point gets a thin teal line beside it.
- Put two stars around a word to stress it: `**Resize is on.**`
- For the details, make a line **small print**: put the caret on it and press
**Small** under the text box. It is drawn small under the line above, so
the first line stays big enough to read from far out. Press **Small** again
to make it normal.
- A line that starts with `##` is read the same as one that doesn't, so notes
written for the core Markdown Note paste in as they are.
- Drag the corner to change the size. The text grows or shrinks to fit.
## Arrows
Type an arrow and it turns into a teal arrow, in the middle of your line.
- Use an arrow emoji: ⬆️ ⬇️ ⬅️ ➡️ ↗️ ↘️ ↙️ ↖️. The pointing hands 👆 👇 👈 👉
and the plain arrows ← ↑ → ↓ work too, and so do `->` and `<-`.
- Or press an arrow in the row under the text box while you edit.
- Put the note next to the part it explains and point the arrow at it.
### Link an arrow to a node
While you edit, click a node on the canvas, then press an arrow. The arrow is
linked to that node. Click a linked arrow later and the canvas brings the node
into view, the same way the frame button in Workflow Switches does. Hover the
arrow to see which node it goes to.
In the text a linked arrow is written like `➡️#12`, where 12 is the node's
number. You can leave the `#12` out to get a plain arrow. If the node is
deleted the arrow stays, faded, and does nothing.
## Links
A note can link to a page on the web, or to a part of the workflow.
- **To a page:** select the words, press **Link** under the text box, and
paste the address. Or select the words and paste the address straight over
them. Click the words later and the page opens in a new tab. A small ↗
after the words shows that they leave ComfyUI, and hovering shows where
they go.
- **To a node or a group:** while you edit, click the node or the group on
the canvas, select the words and press **Link**. Click the words later and
the canvas brings it into view. An arrow can point at a group too: click
the group, then press an arrow.
- A note that is nothing but one link is a big button for it: its words fill
the note.
In the text a link is written `[the words](https://example.com)`, or
`[the words](#12)` for node 12 and `[the words](#g3)` for group 3.
## Technical details
- The text is stored in the node's one hidden `text` input, so saving,
undo and copy work like any other node. Nothing else is saved with it.
- A note copied together with the nodes and groups its links name links to
the copies. A link to anything left behind keeps its number.
- In the text, small print is a line that starts with six `#` and a space:
`###### Off, your words go in as typed.` Small print is 62% of the note's
text size and never under 11 px.
- Notes written before small print look the same. An older version of the
pack drops those six `#` like any other heading mark, so a note with small
print still reads right there, with every line at one size.
- The text size is the largest whole pixel size, from 12 to 30, that fits the
note. A very long text at the smallest size is cut off at the bottom: make
the note taller.
- Only `http` and `https` addresses become links. Anything else in the round
brackets leaves the text as typed. A link opens in a new tab and is never
opened by the note itself: only your click does.
- A link from someone else's workflow goes where its author pointed it. Hover
it to read the address before you click.
- An older version of the pack shows a link as typed, brackets and address
included, so the address can still be read and copied there.
- The font is IBM Plex Sans when the computer has it, otherwise the system
font. The node loads no fonts and makes no network requests.
+7
View File
@@ -0,0 +1,7 @@
# Callout Tile
A compact title or colored link button for a workflow. Choose **Title tile**, **Set YouTube link tile**, or **Set Discord link tile** from its menu. Press **Edit** to change its text or link; you can also double-click a title. Drag its visible corner to resize.
It uses the same text, arrows and links as [Callout](AUSBOSS_NODES_Callout.md). **Plain callout** returns to the ordinary note face. Nothing runs and it has no outputs.
Tiles save with their own node type. If ComfyUI reports this node missing, install or update **ComfyUI-AusBoss**, restart ComfyUI, and reload the workflow. An older pack cannot draw the tile correctly. Keep the missing node so its text and settings are preserved.
+40 -15
View File
@@ -1,30 +1,55 @@
# Color Match
Copies the color statistics of a reference image onto the input. Both images
are converted to LAB, the per-channel mean and spread of the input are mapped
onto the reference's, and the result is converted back — the classic fix for
an inpainted or stitched region that came back with a slight color cast.
Makes one picture's colors match another's. Use it when an inpainted or
stitched area comes back a little too warm, cool, dark or bright next to the
original. Four methods go from a gentle fix to an exact match, and a
first-frame mode matches every frame of a video to its first, which can
reduce color drift between frames.
## Controls
- **image**: The image to correct.
- **reference**: The image whose look to copy. For a stitched inpaint, feed
the original image here. A single reference broadcasts across a batch;
a matched batch pairs frame to frame.
- **strength**: `0` returns the input untouched, `1` applies the full match;
values between blend linearly.
- **Strength** (`strength`, starts at `1`): `0` returns the input untouched,
`1` applies the full match; values between blend linearly.
- **reference** (optional): The image whose look to copy. For a stitched
inpaint, feed the original image here. A single reference broadcasts
across a batch; a matched batch pairs frame to frame. Required unless
**Reference** (`reference_mode`) is **first frame**.
- **Method** (`method`): How the colors move.
- **lab** — per-channel mean/std shift in LAB. Perceptual and the safe
default: corrects overall casts and contrast without shifting
individual hues around.
- **rgb** — the same mean/std shift in raw channels.
- **mkl** — maps the full color covariance; best when hues are rotated,
not just shifted.
- **hist** (`histogram`) — matches each channel's distribution exactly.
The strongest and least subtle.
- **mask** (optional): Restricts the match to the white area — both the
statistics measured on the image and where the correction lands. Black
pixels pass through **bit-identical**. Reference statistics always come
from the whole reference frame.
from the whole reference frame. Without a mask the whole image is
matched. A mask of another size than the image stops the run.
- **Invert mask** (`invert_mask`): Treats the mask's black area as the region
to correct instead of the white area.
- **Reference** (`reference_mode`): Where the target statistics come from.
**reference** uses the connected reference image. **first frame**
(`first_frame`) uses the batch's own first frame as the target for every frame, no reference
needed; the `reference` input is ignored. That can reduce color drift
between frames; it does not stabilize motion.
## Outputs
- **image**: The color-matched image.
- **image**: The color-matched image; pixels outside the mask are untouched.
## Notes
- The transfer is per-channel mean/std in LAB, so it corrects overall casts
and contrast without shifting individual hues around.
- Pair it with **Stitch Inpaint 🆎**: stitch first, then match the
blended region back to the original using the inpaint mask.
- The node takes a mask but outputs none on purpose: the mask only scopes
the fix and passes through your graph unchanged — wire your original
mask onward.
- Pair it with **Stitch Inpaint 🆎**: stitch first, then wire Stitch
Inpaint's `blend_mask` into **mask** and the original picture into
**reference**. That matches exactly the pasted area back to the original.
- For video whose color drifts between frames, **Reference** set to
**first frame** matches every frame to the clip's opening color. It cannot
stabilize motion, and frames whose content changes, such as a pan onto a
darker scene, are pulled toward the first frame's colors as well.
+178 -16
View File
@@ -1,31 +1,193 @@
# A/B Compare
# Image Compare A/B
Shows two images on one panel so differences pop out instead of hiding in a
side-by-side. The first frame of each batch is previewed; `image_a` passes
through the output unchanged, so the node can sit in the middle of a graph
without breaking anything downstream.
side-by-side. The first frame of each batch is previewed; `image_a` and
`image_b` pass through their outputs unchanged, so the node can sit in the
middle of a graph without breaking anything downstream.
## Inputs
- **image_a**: The baseline batch. Its first frame is preview A, and the
whole batch is what the output carries.
- **image_b**: The comparison batch. Its first frame is preview B.
whole batch is what the `image_a` output carries.
- **image_b**: The comparison batch. Its first frame is preview B, and the
whole batch is what the `image_b` output carries.
- **filename** (optional): a name for the files that **Erase** saves, such
as the source picture's file name. Leave it unwired to type the name on
the node.
## Panel
Run the workflow once to load the previews, then compare:
Run the workflow once to load the previews. The switch under the picture
picks what the panel does, and its marker slides to your choice:
- **Slide** (default): move the pointer across the panel. B is revealed up
to the pointer with a thin seam marking the split.
- **Hold**: press and hold anywhere on the panel to see B in full; release
to snap back to A.
- **Slide** (default): the panel shows the result. Move the pointer across
it to compare: A appears on the left of the line and B on the right. Move
the pointer away and the result is back. Every new run shows its result
too.
- **A** / **B**: show that picture and hold it, whatever the pointer does.
- **Erase**: mix the two pictures by hand. One picture is on top; paint on
it and it rubs away, so the other one shows through. Then save the mix.
See **Erase** below.
The button in the panel's corner switches between the two modes, and the
choice is stored with the node.
A picture with see-through parts shows them over a checkerboard, as an
editor does.
## Output
The labels ride with the split line: **A** just left of it and **B** just right of
it, on the picture each one names. With one picture showing, its label sits alone
at the top centre. The chosen mode and locked side are
stored with the node, including support for modes from older workflows. The resolution
sits under the picture, beside the switch; differing sizes are
labeled individually.
## Several pictures
Give the node a list of pictures, such as a folder of images, and it shows
arrows under the picture (‹ 2 / 5 ›). Click them to step through the pairs:
A and B change together. **Erase** saves the pair you are looking at.
A batch still shows its first frame only.
## Erase
Use it when you want part of one picture and part of the other: the edit
is good but it changed the hands, so you paint the old hands back. Or you
only want the new sky.
It also brings back a clear background. When the picture underneath has
see-through parts, such as a cut-out the model filled in behind, rub the
top one away there and they come back clear. The saved PNG and PSD keep
them.
Pick **Erase** on the switch. A row of tools opens above the picture:
- **Erase | Restore**: what the brush does. Erase rubs the top picture
away. Restore paints it back. Holding **Ctrl**, or using the right
mouse button, does the other one for as long as you hold it.
- **Size** is the brush's width, in pixels of the picture.
- The dot beside **Size** previews one brush dab. It grows with Size and its edge fades as you increase Soft.
- **Soft** is the brush's edge: 0 is a hard edge, 100 fades all the way
from the middle.
- Drag left and right on **Size** or **Soft**, on the word, the number or
the unit, to change the value. Click the number to type one.
- **On top: A | B** is the picture you start from. Pick the other and it
goes on top instead. Your paint stays where it is.
- **ALIGN** lines B up with A. Edit models often draw the picture a few
pixels to the side or a little zoomed in, so the edges of your paint
show a step. Click it and B is moved back over A; Save and Copy use the
lined-up B. Click it again to go back to B as it came. It fixes a shift
and a zoom, not a picture that bent or changed shape. Where B was moved
off its edge, A fills the gap.
- **UNDO** takes back the last stroke, or a Clear. **CLEAR** takes all the
paint off.
The brush shows around the pointer in teal: a ring at its full size, and
its shape inside, solid in the middle and fading where the edge is soft.
It turns coral while it paints back.
Set the brush without leaving the picture, with the same keys as in
Photoshop:
- **E** erases and **B** paints back, as with Photoshop's eraser and
brush.
- **[** and **]** make it smaller and larger.
- **Shift + [** and **Shift + ]** make the edge softer and harder.
- Hold **Alt** and the **right mouse button**, then drag: left and right
for the size, up and down for the edge (up is softer). The brush stays
where you pressed and changes as you drag, with its size and softness
written under it; the dashed ring is where its solid middle ends. One
drag can do both: go across for the size, then turn up or down for the
edge. On a Mac, hold Control and Option and drag.
These keys only work while the pointer is on the picture. ComfyUI's own
shortcuts are left alone.
A second row opens under the switch, for saving:
- **Name**: what the files are called. `compare` is used when it is empty.
Letters, digits, spaces, dots, dashes and underscores are kept; a folder
cannot be part of the name. With the **filename** input wired, the name
comes from there and the field only shows it.
- **PSD off | on**: on makes Save write two more files (see below).
- **Copy** puts the mix on the clipboard at full size, to paste into an
editor or a chat. Nothing is saved. Browsers only allow this when
ComfyUI is opened as `localhost` or over `https`; from another computer
on a plain address, use Save.
- **Save**, at the right end, writes the mix to ComfyUI's output folder as
a PNG, such as `compare_00001.png`. Nothing is run again, and every save
takes the next number, so nothing is written over.
With **PSD** on, Save also writes:
- `compare_00001.psd`: both pictures as layers, with your paint as a
layer mask on the top one. Open it in Photoshop, GIMP or another
editor that reads PSD files, and keep working on the mask there.
- `compare_00001_mask.png`: the mask alone. White is where the picture
underneath shows.
After a save, the line under the picture says **Saved** with a link for
each file. Click one to download it, which helps when ComfyUI runs on
another computer.
Your paint is kept with the workflow. A new run keeps it while the picture
keeps its size, so you can change a seed and look again; press **CLEAR**
to start over. Pictures of another size start clean.
## Which picture is the result
B, the "after" picture. If your graph makes A out of B's picture (B is the
picture you loaded and A is the edited one), the result is A. To keep one
picture up whatever happens, use the **A** or **B** button.
## Outputs
- **image_a**: The `image_a` batch, untouched.
- **image_b**: The `image_b` batch, untouched: the picture you compared
against, often the edited one. Wire it on when the picture you want to
keep is B.
Previews are written to ComfyUI's temp folder and vanish with it; nothing is
saved to the output folder and no network requests are made.
Previews are written to ComfyUI's temp folder and vanish with it. Nothing
is saved to the output folder until you press **SAVE** in Erase, and no
network requests are made.
## Technical details
- While sliding, the outer 6% on either side snaps to a full image.
- The result is found from the links: when the node that feeds `image_b` is
somewhere upstream of the node that feeds `image_a`, A was made from B and
A is the result. When both pictures come out of one subgraph, the same
check is made inside it. In every other case it is B: two unrelated
pictures, both pictures out of one plain node, or a link the node cannot
follow (a Set and Get pair, for example).
- Settings → AusBoss → Image Compare → **Rest on the result** switches this
off. The panel then stays on the picture nearest to where the pointer
left, and a new run does not change it.
- **Erase works on the first picture of each batch**, at its full size.
The mix is always A's size: a B of another size is resized to fit A.
- **See-through parts** are kept in a preview only when a picture has some,
so solid pictures save exactly as before. The mix lays the top picture
over the other as an editor does. In the PSD each layer keeps its own
see-through parts, and the flat picture carries the mix's, its colours
stored over white the way Photoshop stores them. **Align** measures the
two pictures laid over gray and moves B's see-through parts with it.
- **What is kept is the strokes**, not a painted picture. The panel draws
them for you, and Save draws them again on the server at full size, so
the saved edge is as sharp as the picture however small the node is.
Copy draws them at full size in the browser.
- **Erase never changes the node's size.** On a narrow node its rows wrap
onto a second line and the picture gets shorter instead.
- **The brush** is a solid middle with a blurred edge. At Soft 100 the
middle is half the brush wide and the blur is an eighth of the size.
- **Save mixes the two pictures of the node's last run**, which ComfyUI
keeps in its temp folder. After a restart of ComfyUI they are gone: run
the workflow once more, then save. The browser sends the strokes, a key
for that run and the name: it never names a file to read, and the name
is checked again on the server.
- **A wired name is tidied**: its folder and extension are dropped, and
anything a file name cannot hold becomes an underscore
(`sets/a/photo 12.jpg` saves `photo 12_00001.png`).
- **The saved files carry no workflow.** A mix is made by hand, so there
is nothing a workflow could make again.
- **The PSD** is 8-bit RGB and not compressed: about 12 bytes for each
pixel, so 50 MB for a 2048 x 2048 picture. It carries no colour profile,
like the PNG.
- **Limits**: 400 strokes in one mix. The node says so when it is full.
+91 -18
View File
@@ -1,34 +1,93 @@
# Crop For Inpaint
Cuts the masked region — plus enough surrounding context — out of an image
so an inpainting model works at the region's native resolution instead of
shrinking the whole frame. Pairs with **Stitch Inpaint 🆎**, which
pastes the result back exactly where it came from.
Cuts out the part of a picture you masked, with some of what is around it, so
the model repaints just that part at full detail instead of the whole picture
shrunk down. Use it with **Stitch Inpaint 🆎**, which pastes the result back
exactly where it came from. The **Extend** rows (`extend_*`) add new space on a
side of the picture, which is how the pair outpaints.
## Controls
- **image**: BHWC image or video frames to crop around the mask.
- **mask**: White marks the area to inpaint. A multi-frame mask is unioned
into one crop window so a whole video shares one stitcher; an empty mask
selects the full image (and stitches back unchanged).
- **context_factor**: Grows the mask bounding box symmetrically by this
factor. The grown window is shifted back inside the frame first; only
when it truly cannot fit is the image extended with edge-replicated
padding.
- **blend_pixels**: Feather width used *only when pasting back*: the paste
mask is widened by this many pixels and blurred. The sampling mask sent
to the inpainter stays hard-edged, so feathering never weakens what the
model sees. `0` pastes with the raw mask.
- **output_multiple**: Crop and target dimensions are rounded up to a
multiple of this so samplers accept them.
- **target_width / target_height**: Rescale the crop to a fixed size for
selects the full image (and stitches back unchanged). A picture with no
mask painted on it stops the run with "No mask painted" and how to paint
one (core Load Image sends a small blank stand-in in that case). A mask
of another size than the picture also stops the run, and the message
gives both sizes.
- **Context** (`context_factor`): Grows the mask bounding box
symmetrically by this factor. The grown window is shifted back inside the
frame first. When it's bigger than the picture, **Stay in picture**
decides what happens.
- **Stay in picture** (`keep_inside`, on by default): Keeps the crop inside
the picture. When the context would reach past an edge (a big mask, or a
small picture), the crop stops at that edge, so the model sees only real
picture and, after a **Target** rescale, paints the new part larger and
sharper. Off is the old behavior: the crop runs past the edge and the
extra space is filled with stretched copies of the edge pixels. One catch
with it on: the model sees more of what's around your mask and carries it
on, so paint over leftovers at the mask's edge that you don't want kept.
- **Blend** (`blend_pixels`): Feather width used *only when pasting back*:
the paste mask is widened by this many pixels and blurred. The sampling
mask sent to the inpainter stays hard-edged, so feathering never weakens
what the model sees. `0` pastes with the raw mask.
- **Multiple** (`output_multiple`): The crop's size is rounded up to a
multiple of this. Keep it at `32` (the default) for Qwen Image 2.1, Flux
and most newer models. Their VAE trims a picture that is not a multiple
of 16 or 32, and the trimmed result, stretched back over the crop, looks
slightly bigger than the original. Stitch Inpaint says so in the console
when that happens.
- **Target width / Target height** (`target_width` / `target_height`, in
the **Target size, extend** fold): Rescale the crop to a fixed size for
the sampler. `0` keeps the native crop size; setting only one dimension
derives the other from the crop's aspect ratio.
derives the other from the crop's aspect ratio. Explicit values here
override `target_megapixels`. When the crop is rescaled, the crop area
takes in a few more pixels of context on one side so it has the exact
shape of the target size, and the picture is scaled the same amount
both ways, never stretched.
### Shaping the mask
- **Grow** (`mask_grow`): Dilate the sampling mask by this many pixels
(negative shrinks) so the inpainter repaints past the drawn edge. This
reshapes what gets painted, unlike `blend_pixels`, which only feathers the
paste-back.
- **Blur** (`mask_blur`): Gaussian sigma softening the sampling mask's
edge, for models that honor soft masks. `0` keeps the hard edge.
- **Invert mask** (`invert_mask`): Inpaint the black area instead of the
white area — everything outside the drawn region. Applied before growing
or blurring.
### Sizing the crop
On the card, the **Target size, extend** fold holds these rows,
**Target width / Target height** and the **Extend** rows.
- **Extra context** (`context_pixels`): Flat extra context in pixels added
around the mask box after `context_factor`'s growth.
- **Target** (`target_megapixels`): Rescale the crop for the sampler so its
area is about this many megapixels — `1.0` suits SDXL-class models, `0`
keeps the native crop size. Explicit `target_width`/`target_height` wins.
- **Rescale** (`rescale_algorithm`): Resize filter for both directions of
the sampler round trip. `bilinear` is the safe default, `bicubic` keeps
upscales a touch crisper, `area` suits heavy downscales, `nearest` never
invents pixels.
### Outpainting
- **Extend left / Extend right / Extend up / Extend down** (`extend_left` /
`extend_right` / `extend_up` / `extend_down`): Grow the picture itself by
this many pixels on that side. The new bands are filled with stretched
copies of the edge pixels, are always painted (even with **Invert mask**
on), and become part of the stitched output — draw nothing and the
extended bands alone are inpainted.
## Outputs
- **image**: The cropped region, sized for the sampler.
- **mask**: The matching hard-edged sampling mask.
- **mask**: The matching hard-edged sampling mask (soft only where
`mask_blur` says so).
- **stitcher**: Everything Stitch Inpaint needs to put the result back.
## Wiring
@@ -42,3 +101,17 @@ Load Image ── Crop For Inpaint ── image ──> LaMa Inpaint ── imag
Any inpainting sampler fits between the two nodes the same way — connect
the crop's `image` and `mask` to it, then its output to Stitch Inpaint's
`inpainted` input.
A picture with see-through parts keeps them through the pair. ComfyUI's
Load Image splits them off into its `mask`; wire its `IMAGE` and `MASK`
into **Join Image with Alpha** to put them back on before the crop. The
sampler gets the colours, and Stitch Inpaint gives the result its clear
parts back.
## Technical details
- Starting values: Context `1.2`, Blend `16` px, Multiple `32`; Grow, Blur,
Extra context, the Target rows and the Extend rows start at `0`.
- With no Target size set, the crop is still rounded up to **Multiple**, so
with **Stay in picture** on it can still reach a few pixels (at most
Multiple - 1) past an edge.
-21
View File
@@ -1,21 +0,0 @@
# Drop Shadow
Casts a colored shadow from a subject mask and composites it beneath the
original image. The subject itself is protected from the shadow, while pixels
outside the shadow remain unchanged.
## Controls
- **image**: The image containing the subject.
- **mask**: White identifies the subject that casts the shadow. One mask can
broadcast across an image batch.
- **offset_x / offset_y**: Signed pixel offset for the cast direction.
- **grow**: Expands the shadow before softening it.
- **blur**: Gaussian edge softness in pixels; `0` keeps a hard edge.
- **shadow_color**: Hex, RGB, grayscale, or CSS color name.
- **opacity**: Shadow strength from `0` to `1`; `0` is an exact pass-through.
## Output
- **image**: The input image with the colored shadow composited beneath the
masked subject.
+55
View File
@@ -0,0 +1,55 @@
# Float
A number with decimals. Set one strength, scale, or CFG here, wire it to
every node that needs it, and change it in one place.
## Controls
- **Value** (`value`): The number to send out. It starts at `0`. The box
shows the decimals its steps use (`0.00`) and holds up to three, so a
typed `0.125` stays `0.125`.
- The gear, right: **Look**. **normal** is the usual box with its label.
**big** shows the number large in the middle of the node, with an arrow
on each side, for a setting people change on every run, such as the
length of a clip. A drag and typing work the same. Each node keeps its own look, and it is
saved with the workflow.
## Limits
Wire the output to a setting that has limits and the box takes them over.
Wired to a sampler's **denoise**, it goes from 0 to 1 and no further: type
1.5 and you get 1. The card shows **0–1** beside the value.
- It works through a Reroute and into a box (subgraph) too.
- Wired to two settings, it keeps to what both allow.
- Unwire it and it takes any number again.
- A new box takes the value of the first setting you wire it to, so wiring
it up changes nothing until you move it. A box you have already set, or
one that came with a workflow, keeps its own value.
## Output
- **float**: The value, unchanged.
For arithmetic on the way — scaling, clamping with `min`/`max` — feed this
into **Math Expression 🆎** instead of retyping derived numbers.
## Technical details
- The limits are those of the input the wire lands on, the way ComfyUI's own
Primitive node takes them. When the inputs agree on a step, the fine step
(Shift) is that step.
- A value that is outside the limits when the wire is made is brought to the
nearest one.
- The box holds three decimals, or as many as the step it took over needs.
It shows the decimals its arrow step reaches (at least one) and goes past
them only for a value that uses more: `1.00` wired to denoise, `8.0` wired
to CFG, `0.855` for a typed 0.855.
- The limits are kept by the node's face. A value sent straight through the
API is passed on as it is.
- Wired only to inputs that share no value (one stops at 1, the other starts
at 2), the box keeps no limits.
- The look is stored in the node's properties as `ausboss_text_look: "big"`, the
same as Text 🆎. It is not an input, so the saved value and what the node
sends are the same in both looks, and an older version of the pack shows
the usual box.
-93
View File
@@ -1,93 +0,0 @@
# Frame Chooser
Pauses the running graph and turns the node into a clickable filmstrip of every
incoming frame. Pick the frames to keep, press **Keep selected**, and the graph
resumes with just those frames — always in their original order.
## Controls
- **frames**: A BHWC image batch, commonly the frame output of a video loader.
- **behavior**: `always pause` stops every run until you answer. `keep last
selection` re-applies this node's previous answer immediately and only pauses
when no previous answer fits the current batch.
- **preview_max_size**: Longest edge of the filmstrip thumbnails. Smaller
values encode and load faster; the full-resolution frames are untouched.
- **timeout_seconds**: How long a pause waits before **on_timeout** answers
for you. `0` waits forever. While armed, the panel header counts down the
seconds left.
- **on_timeout**: What an expired countdown does — `keep all`, `keep first`,
`keep last`, or `cancel` (interrupts the run like the stop button). Ignored
while **timeout_seconds** is `0`.
- **pick_list**: One-based frame numbers, e.g. `1,4,9` (commas or spaces).
When filled, the node applies them immediately — no pause, no filmstrip —
and the run can cache like any ordinary node. **Clear it to make the node
pause again.** A number outside the batch fails the run instead of
guessing.
Under **keep last selection**, answering a pause fills this widget in for
you with the kept indices, so the next queue reproduces the same choice
headlessly. Under **always pause** nothing is written here: a filled
`pick_list` pre-answers the node, so writing one back would answer every
later queue on its own and quietly retire the setting you chose. Keeping
all frames writes an empty list, not an enumeration of the batch — the
answer stays "keep everything" however long the next clip is.
## While paused
- Click thumbnails to toggle them; the header counts the selection, and
**ALL** / **NONE** flip the whole filmstrip at once.
- **Keep selected** resumes with the chosen frames. **Keep all** resumes with
every frame — an empty selection deliberately means keep-all, so a quick
"just continue" can never produce an empty batch.
- **Cancel** interrupts the run exactly like ComfyUI's stop button.
Interrupting the queue also releases the pause.
- The controls grey out while an answer is on its way, so a doubled Enter or
click cannot send it twice; a reply that lands after the pause is already
over is ignored rather than reported as a failure.
- Reloading the page does not lose the pause: when the graph finishes
loading, the panel re-fetches every still-waiting chooser from the server
and re-renders its filmstrip (countdown included).
- A pause that starts out of sight — another browser tab, another workflow,
or the node scrolled off the canvas — raises a toast naming the node and
briefly highlights the waiting workflow tab. One notice per pause; a
filmstrip you can already see stays quiet.
- The first moments after the filmstrip appears ignore clicks, so a click
meant for the canvas cannot answer a pause that popped up under the cursor.
## Keyboard
Active while the panel has focus — it takes focus when the pause opens, and a
click anywhere on it returns focus. Keys typed into a text field are left
alone.
| Key | Action |
| --- | --- |
| `1`-`9` | Toggle that frame |
| `A` | Select all |
| `N` | Select none |
| `Enter` | Keep selected |
| `Escape` | Cancel the run |
## Outputs
- **frames**: The kept frames as a BHWC batch, in source order.
- **count**: How many frames were kept.
- **indices**: The kept one-based indices, comma-joined (`1,4,9`).
Thumbnails are written to ComfyUI's temp folder and disappear with its normal
cleanup; answers travel over a local `/ausboss/frame_chooser` route. The last
selection is remembered per node id for the current server session only. Each
pause has a one-use token, so an older panel cannot answer a later pause that
reuses the same node id.
## One answer per pause
Several things can end the same pause: a second browser tab, Escape chasing
Enter, the countdown expiring in the instant an answer arrives, or the queue
being stopped. The server settles that on the way in — the first one to reach
the pause decides it, and every later attempt is refused rather than allowed
to overwrite the decision. So a cancel that lands behind a keep cannot stop a
run that is already continuing, an expired timer cannot discard the answer you
just gave, and the panel that lost never claims it was the one that counted.
Whichever way the pause went is broadcast to every open tab, which is what
releases the other panels and fills in `pick_list`.
+28 -21
View File
@@ -1,43 +1,50 @@
# Frame Interpolate
Resamples a frame batch from one frame rate to another, so `24 -> 30` works
as naturally as doubling. Every output frame maps to a position on the source
timeline: positions that land exactly on a source frame are copied untouched,
and only the true in-betweens are synthesized.
Changes a clip's frame rate, for example 24 to 30, by making the in-between
pictures. Use it to smooth a generated clip or to match another clip's rate;
`24 -> 30` works as naturally as doubling. Every output frame maps to a
position on the source timeline: positions that land exactly on a source
frame are copied untouched, and only the true in-betweens are synthesized.
## Methods
- **blend**: A crossfade between the two neighboring frames. Instant and
dependency-free, but fast motion looks soft because it averages instead of
moving pixels.
- **optical flow**: Estimates RAFT motion vectors in both directions, warps
both neighbors toward the in-between time, and blends the warps. Much
sharper on motion. It only reads the official RAFT-small checkpoint from
the torch hub cache and never downloads it during execution. If the file is
absent, the node reports its exact expected path and source URL; approve and
place the model there separately, or use **blend**.
- **optical flow (RAFT)** (`optical flow (requires cached RAFT weights)`):
Estimates RAFT motion vectors in both directions, warps both neighbors
toward the in-between time, and blends the warps. Much sharper on motion.
It reads torchvision's official RAFT-small checkpoint,
`raft_small_C_T_V2-01064c6d.pth`, from the torch hub cache
(`~/.cache/torch/hub/checkpoints/` unless `TORCH_HOME` moves it) and never
downloads it during a run. Download it from
`https://download.pytorch.org/models/raft_small_C_T_V2-01064c6d.pth` into
that folder; without it the node names the exact path it looked in. Or use
**blend**.
## Scene cuts
Interpolating across a hard cut produces morph artifacts: two unrelated
shots smeared into each other for a frame or two. When the mean absolute
difference between adjacent source frames rises above
**scene_cut_threshold**, the node treats that pair as a cut and holds the
difference between adjacent source frames rises above **Scene cut**
(`scene_cut_threshold`), the node treats that pair as a cut and holds the
last frame before it instead of morphing. Set the threshold to `0` to
disable detection and interpolate everything.
## Controls
- **frames**: Video frames as a BHWC `IMAGE` batch.
- **source_fps**: Frame rate of the incoming batch; wire Load Video's fps
output to match the source.
- **target_fps**: Frame rate to resample to. Matching the source rate passes
the batch through unchanged.
- **method**: `blend` or `optical flow` as described above.
- **scene_cut_threshold**: Cut sensitivity from 0 to 1; `0.35` suits most
footage, `0` disables.
- **batch_size**: In-between frames computed per batch. Higher is faster but
uses more VRAM; results land in system memory as each batch finishes.
- **FPS**, left box (`source_fps`): Frame rate of the incoming batch; wire
Load Video's fps output to match the source.
- **FPS**, right box (`target_fps`): Frame rate to change to. Matching the
source rate passes the batch through unchanged. A new node starts at
24 → 30.
- **Method** (`method`): `blend` or **optical flow (RAFT)**, as described above.
- **Scene cut** (`scene_cut_threshold`): Cut sensitivity from 0 to 1; `0.35`
suits most footage, `0` disables.
- **Batch** (`batch_size`, starts at `8`): In-between frames computed per batch. Higher is
faster but uses more VRAM; results land in system memory as each batch
finishes.
## Outputs
+33
View File
@@ -0,0 +1,33 @@
# Free Memory
Passes anything through unchanged and frees graphics memory on the way. Put
it between two heavy steps — after a video decode, before an upscale — so
the second one starts with free memory instead of an out-of-memory error.
## Controls
- **value**: Any wire — the node's input and output are wildcard-typed, so
it splices into an `IMAGE`, `LATENT`, `MODEL`, or any other connection
without adapters.
## Output
- **value**: The input, passed through unchanged.
## Behavior
- What it frees, in order: ComfyUI's loaded models are unloaded, ComfyUI's
own cache is emptied, Python garbage is collected, and the CUDA allocator
cache is emptied.
- Every release step is best-effort. If ComfyUI moves or renames one of the
APIs involved, that step is skipped and the wire still flows; the node
never becomes the reason a workflow fails. What was actually freed is
printed to the console each run.
- The comfy imports happen when the node runs, not when the pack loads, so a
core change surfaces as a skipped step rather than deleting the node from
the menu.
- Like any node, it only runs when its input changes — it does not force
re-execution, so inserting it never breaks ComfyUI's caching downstream.
Freeing happens on the runs where work actually flows through.
- Models unloaded here are reloaded by the next node that needs them; that
reload costs time, which is the trade for the freed VRAM.
+268
View File
@@ -0,0 +1,268 @@
# H3 Prompt Writer
Type what happens, one line for each moment. Leave a blank line to cut to a new
place. The node writes H3's full prompt for you, and you never see its format.
Use it with MiniMax H3 or Pika H3 Flex. Connect up to four pictures and give each
one a job. Copy the moves from a video, keep a clip going, or add a voice or a
song.
## How to write it
Every line is a moment, and the lines play in order in one shot. The bar under
the box shows the second each one starts, before you spend a run.
```
@1 walks through a crowded night market toward us, looking over her shoulder.
@1 stops and looks up at the lanterns.
@1 stands at the edge of a rooftop in the rain, her back to us.
@1 slowly turns her head toward us.
```
That is two shots in two places. `@1` is Picture 1. With one person you can
write `She` or `He` instead.
The node also fills in two things H3 misses by itself. A moment gets its own
sound when it names something that makes one, like rain, footsteps or thunder.
And what changes stays changed: when the lights go out, the next moments in that
place say they are still out.
## The words box and the bar
The words the node acts on light up as you type: `@1` in teal (amber when
nothing is plugged in there), a time like `3s:` in amber, words in quotes in
yellow, and a blank line shows as a line marked CUT. Hover the `?` beside
"What happens" for a short list of everything you can type.
Typing `@` lists the pictures you plugged in, with their job. The first letters
of a line offer `sound:`, `music:`, `camera:` or `look:`, and after `camera:` or
`look:` you get the node's own choices. Tab picks one, Esc closes the list.
The bar under the box is the clip from start to end, one block for each moment
with the pictures it names:
- **Drag a block** along the bar to move its line. The times and cuts stay
where they are.
- **Drag the left edge** of a block to set when that moment starts. The node
types the time (`3s:`) into its line for you.
- **Click the left edge** to add or take out a cut there.
- **Click a block** to pick out its line in the words, ready to change.
Everything on the bar changes the words, so Ctrl+Z undoes it.
## Give each picture a job
Pick a job for each picture under the words, then name it in your lines with
`@1`, `@2`, `@3` or `@4`.
- **person**: this person goes wherever your lines say. Name the place in the
line, or name none and they stay where the picture shows. Use a clear photo.
- **character sheet**: one character drawn from several sides. Only the
character is used, not the panels.
- **place**: where it happens. Use a picture with nobody in it.
- **outfit**: clothes for your person. Works best with no head in the picture.
- **thing**: an object or a product. Works best with nobody in the picture.
- **style**: only the look is used, for the whole clip.
- **start on it**: the clip starts from this picture and stays in its place.
- **end on it**: the clip lands on this picture. It can't be named in a line.
Some scripts, with the jobs they need:
```
@1 sits at the counter in @2. Picture 2 is a place
@1 and @2 sit side by side on a night bus. Picture 2 is a person
@1 walks down a runway wearing @2. Picture 2 is an outfit
@1 spins slowly on a black glass table. Picture 1 is a thing
```
A picture that is off simply isn't counted. If a line still names it, the node
stops and tells you which one. It needs at least one picture or a video to start
from.
## Use a video
Connect a video, then pick what it is for:
- **copy its moves**: your person does the moves in the video. It needs a
picture of your person. Say where in your lines:
`@1 dances on a rooftop at sunset.` The clothes can come from words or from an
outfit picture.
- **swap the person**: your person takes over from the one in the video. The
place, the moves and the camera stay. With two person pictures, the first
replaces the person on the left and the second replaces the person on the
right. To choose who goes, type one line per picture, for example
`@1 replaces the man in the hat`. Put spoken words in quotes and name
the speaker. If a voice comes from off screen, say so in the line.
- **change something**: keep the video and describe the change, for example
`She wears a red raincoat`. Be specific about what should change.
- **keep going**: more of the same clip, from where it ends. Say what happens
next, or leave the box empty and it just carries on. Connect the whole clip:
the node sends H3 only the last 2 seconds.
- **new voice**: the same clip with a new voice. Connect the voice, and type
what they say in quotes.
If your swap keeps the video's audio, connect it to **video_sound** and turn on
**More → Clip's own sound**. The writer uses the clip to estimate when each
line is spoken and keeps only the incoming people's words in the swap prompt.
Type a time such as `3.2s:` to set it yourself. This switch starts off.
With an active video, **swap the person**, **change something** and **new voice**
use the incoming clip to set the frame count. **Length** says **From the clip**;
set the clip's length upstream. The bar does not show a guessed duration or offer
retiming controls for these jobs. H3 rounds the frame count to a supported length.
Other video jobs, or turning the video off, use the editable **Length** again.
For a swap, you can optionally describe the replacement on its own line:
```
the woman with black hair walking toward the camera
@1 looks like a woman with blonde hair and a black hoodie.
```
The first line chooses who goes; the `@1 looks like` line describes the connected
replacement person in Picture 1. Use the number of that picture, with its job set
to **person** or **character sheet**. This helps describe the intended appearance;
it does not guarantee an exact identity match. The line is optional: existing
workflows and scripts keep their previous behavior when it is absent.
## What you can type
- **A time**: start a line with `3s:` and it happens at 3 seconds.
- **A cut in the same place**: start the line with a kind of shot, like
`a close-up of her hand`. Any other line after a blank line goes to a new
place.
- **One long take**: type `no cuts` and blank lines stop being cuts.
- **An ending**: start a line with `ends on`, wherever it sits:
`ends on her sitting by the window`.
- **Speech**: put the words in quotes: `@1 says "we are late."` Each person
keeps their own voice.
- **Words on a sign**: name the sign: `a neon sign reads "OPEN"`.
- **Sound and music**: use the **Sound** and **Music** boxes, or type
`sound: rain on a tin roof` and `music: slow sad piano`. With no music you get
none, and the clip comes out quieter.
- **Camera and look**: pick them on the node, or type your own:
`camera: slow push in`, `look: a 1970s film`.
A `camera:` or `sound:` line inside a group of lines belongs to that shot. On its
own, with blank lines around it, it is for the whole clip.
Already wrote H3's six parts yourself? Paste them in and they go through as
they are.
## What you can plug in
- **picture_1** to **picture_4**: your pictures.
- **video** and **video_sound**: the frames and the sound of a video. **keep
going** sends the last part of the sound to H3. A swap uses the sound to find
speech timing when **Clip's own sound** is on. To keep source audio in a swap
or **change something**, route the original loader's audio to the save node.
- **voice**: a clip of someone talking, about 10 seconds, plus words in quotes
in your script. It goes to whoever speaks first. It works with your own voice,
or one you have permission to use. Put the spoken line last: with time left
after it, H3 tends to keep talking.
- **song**: your music. It is cut to the length of the clip and plays as it is,
and each moment moves onto its beat.
- **clip**: a small text model such as Qwen3-VL 8B. Type one line and it adds
what happens next, and it names the sound of the place. Your line always stays
first.
A voice and a song work together. H3 can't keep a song while someone speaks, so
the node keeps the song out of H3's way and hands it on as **song_under**, turned
down, to mix under the finished clip.
Then wire the node's outputs into H3: **prompt** into its prompt, **frames** into
its length, each **picture** into its next `ref_image`, **video** into `ref_video`,
**video_sound** into `ref_video_audio`, **voice** into its first `ref_audio`,
**song** into the next one. The pictures come out in the order the prompt counts
them, so a picture that is off leaves no gap. **width** and **height** go into
H3's width and height: the clip takes the shape of your video, or else of your
first picture. **song_under** goes into `audio2` of Audio Merge, with the clip's
sound in `audio1`.
**keeps_clip_sound** is on when a video is connected and **Clip's own sound** is
on, or the video job is **change something**. Use it to select the source audio
for the finished clip. It is the last output; the existing outputs keep their
positions. Keeping source audio still needs the workflow to pass that audio to
its save node.
Nothing plugged in means nothing is added. So a page can turn each one on and off
with [Workflow Switches](#AUSBOSS_NODES_WorkflowSwitches): a bypassed loader reads
as not plugged in.
## The controls
- **Sound** (`sound`): what the clip sounds like, for the whole clip. Same as a
`sound:` line.
- **Music** (`music`): the music for the whole clip. Same as a `music:` line.
- **Length** (`seconds`): how long the clip is, 5 to 15.
- **Camera** (`camera`): how the camera moves the whole way through.
- **Look** (`look`): from the picture keeps your photo's look. The others redraw
the clip. Clay and 3D don't work from words, so change your picture first with
an edit model.
- **Picture 1** to **Picture 4** (`job_1` to `job_4`): what each picture is
for, on the picture chips under the bar. An empty slot shows as a faint `+ @3`.
- **Video** (`video_job`): what the video is for, on the video chip.
- **Song starts at** (`song_start`, under **More**): the second of the song to
start from, so the clip gets the good part.
- **Moments on the beat** (`cut_on_beat`, under **More**): moves each moment onto
the nearest beat of the song. Off keeps your own seconds.
- **Language** (`language`, under **More**): the language of the words in quotes.
## Tips
- About one moment a second is the most H3 can play. The line under the rows
warns you, and it warns you when a spoken line won't fit its moment.
- Say which way they face, and think about where they end up. If someone walks
away from us, we see their back until a line turns them.
- H3 draws each shot on its own. A cut to a new place hides that. A cut inside one
place shows every gap, so keep a place in one shot and move the camera instead.
- Nothing moves or exists unless a line says so. If the birds should fly, say so
in that shot.
- Say what you would see, not what someone feels. `She smiles` works.
`She feels happy` doesn't.
- A clip came out wrong? Run again with a new seed.
## Slots
| Slot | Direction | Type | Purpose |
| --- | --- | --- | --- |
| `script` | input | STRING | What happens, one line for each moment. |
| `seconds` | input | INT | How long the clip is. |
| `clip` | input, optional | CLIP | A small text model that fills in a one-line idea. |
| `picture_1` to `picture_4` | input, optional | IMAGE | Your pictures. |
| `video` | input, optional | IMAGE | The frames of a video. |
| `video_sound` | input, optional | AUDIO | The video's own sound. |
| `voice` | input, optional | AUDIO | A clip of the voice to use. |
| `song` | input, optional | AUDIO | The music to use. |
| `prompt` | output | STRING | The finished prompt. |
| `picture_1` to `picture_4` | output | IMAGE | Your pictures, in the order the prompt counts them. |
| `video` | output | IMAGE | The video, or only its end to keep going from. |
| `video_sound` | output | AUDIO | The video's sound, when it should reach H3. |
| `voice` | output | AUDIO | The voice, 15 seconds at most. |
| `song` | output | AUDIO | The song, cut to the clip. Empty when a voice is connected too. |
| `song_under` | output | AUDIO | With a voice and a song, the song to mix under the finished clip. |
| `frames` | output | INT | The clip's length in frames. |
| `width`, `height` | output | INT | The shape of your video, or else of your first picture. |
| `keeps_clip_sound` | output | BOOLEAN | Whether the finished clip should use the video's own sound. |
## Technical details
Code does the reading, so nothing typed can be lost or moved. It follows Pika's
and MiniMax's published prompt guides: every picture is defined once, each one
states what is kept and how, and a person who changes clothes or moves to a new
place is marked that way.
A timed shot mark makes H3 jump, whatever the words around it say: in tests 31 of
36 one-take clips jumped within half a second of a mark. So a mark is written only
for a real cut, and the moments of one take are sentences inside one shot. Clips
written that way had no jumps in 4 of 4 tries.
The prompt always tells H3 not to keep the background of a person's picture. With
a place in your lines the clip stayed there to the end on 2 of 2 tries; without
that sentence the picture's street came back halfway through on 2 of 2. With no
place in your lines H3 has nothing else to use, so it stayed in the picture's
place on 2 of 2.
H3's own rewriter runs only on paid services, and the open model reads a prompt as
it is. This node does that job on your machine, with no model needed.
+288 -15
View File
@@ -1,26 +1,299 @@
# Image Crop + Rotate + Pad
Loads an image and applies one reusable **rotate → crop → pad** transform. Click **Open editor** for the full-screen canvas; normal queued and API execution use the saved widget values without needing the editor.
Loads a picture and lets you turn it, crop it and add empty space around it, by
dragging handles on the node. Use it to frame a picture for a model, or to add
space around a picture that the model then fills in (outpainting).
## Controls
The picture is turned first, then cropped, then padded. The node also makes the
**mask** that tells the model where to paint, and a **stitcher** that puts your
original back over the result.
- **image**: Image picker/upload from ComfyUI's input folder.
- **rotation_degrees**: Clockwise rotation before crop and padding.
- **crop_aspect_ratio**: Free crop, source ratio, or a fixed ratio.
- **crop_x / crop_y / crop_width / crop_height**: Crop in rotated-image pixels. Width and height `0` mean the full available dimension.
- **pad_left / pad_top / pad_right / pad_bottom**: New pixels around the crop.
- **feather**: Feathers the mask into kept pixels and fades the image edge into the fill color, so outpaints blend instead of ending at a hard seam.
- **canvas_multiple**: Rounds the final canvas up by adding the minimum extra pixels to the right and bottom.
- **fill_color**: `#RRGGBB` or three RGB values used for generated pixels.
## Quick start
1. Choose a file above the preview, or click **Load**. Dropping a
picture onto the node works too.
2. Drag the handles on the picture. Cyan squares crop. Orange diamonds add space
on one side. Orange corners make the whole canvas bigger or smaller. The
green knob turns the picture.
3. Or tap a ratio such as **16:9** to pad the picture to that shape.
4. Wire **image** and **mask** to your model, and **stitcher** to Stitch Inpaint
after the sampler.
**Open editor** shows the same handles on a full-screen canvas, with zoom and
pan. **Save & close** keeps your edits. **Cancel** or Escape puts everything
back, and asks first if anything changed. You do not need the editor: a run
uses what the node shows.
## On the node
From top to bottom:
- **File picker**: choose **Inputs** for uploaded pictures or **Outputs** for renders, newest first. The list shows the
picture under the pointer and filters as you type. **Load** adds one. A
picture ComfyUI made, named like `photo.png [output]`, loads too, as it does
in core Load Image. The gear holds the node's display options.
- **The picture**, with its handles.
- **The size line**: each step that sets the output size, ending with the size
the run gives you.
- **Crop, pad & resize**: a collapsible group (closed to start, with the fill
swatch and a short summary on its bar) holding:
- **Aspect ratio** and **Fit to ratio** (Crop or Pad), above the orientation button, the ratios (1:1, 4:3, 3:2, 16:9,
21:9) and the padlock.
- **Align** and the two **Center** buttons.
- **Fill** and **Feather**.
- **Resize**, with **Megapixels** and **Step** alongside it when Resize is on.
- **Open editor**, **Reset crop** and **Reset**.
Crop and padding have no number boxes. You set them by dragging, and the size
line shows the result. Picking another picture starts the framing over
(rotation, crop and padding); Fill, Feather, Align, Resize and a lit
ratio stay.
### Ratios, Fit and the padlock
Tap a ratio under the picture to pad the picture to it: every pixel stays and
fill bands are added around it, centred. Set **Fit** to crop and the ratio trims
the picture instead. Tap the lit ratio again to go back to the whole picture.
You can set **Fit to ratio** before picking a ratio. If a ratio is already lit, switching between Crop and Pad applies it immediately.
- **A lit ratio is the shape the canvas has now.** Drag a handle to another
shape and the ratio goes dark; the row says **Custom** and the size line under
the picture gives the real ratio (`1.49:1`).
- **A picture that already has the shape gets nothing added.** Within about 1%
counts, so a nearly 9:16 photo gets no 1 px band. The size line under the
picture says so ("already 9:16: pick another ratio or turn it"), and while a
ratio is lit it names it where it acted (`pad to 16:9 2532×1424`). A lit ratio
saved in a workflow is used again on every new picture you load.
- **The padlock** at the end of the row keeps the shape while you drag: pull one
side out and the other side's padding follows, split evenly. The row says
**Held** while it is on. On the untouched picture (the row says **Source**)
there is no shape to hold, so it stays off. **Reset** and tapping the lit ratio
turn it off, and **Reset crop** also takes away the bands it added.
- **The orientation button** at the start of the row turns the shape on its side:
16:9 becomes 9:16, padded around the picture (crop mode turns the crop box
about its centre). The picture itself never rotates. With nothing picked it
only turns the ratios, so the next tap goes that way.
- **Drag the picture itself** to move it inside its padding; the canvas keeps its
size. Over the picture, the cursor and small arrows show which ways it can go.
- Rotating keeps a lit ratio: the padding follows the turned picture.
### Corner handles and Center
The four orange corners outside the picture make the whole canvas bigger or
smaller and keep its shape: drag one out to add room on two sides at once, for
example to zoom out before an outpaint. The corner across from the one you drag
stays where it is. Hold **Alt** (Option on a Mac) while you drag to change all four
sides at once, so the picture keeps its place in the middle. You can press or let
go of Alt during the drag. A corner stops when one of its two sides has no padding
left to take away.
**Center** moves the crop to the middle of the rotated source, side to side or
top to bottom. If there is padding on that axis, the same click also balances
the bands around the crop. The crop and output keep their sizes, and the
generated mask follows the new placement. Each button dims when its axis is
as centered as the pixels allow.
The orange diamonds move one side at a time, and the padlock works on them as
described above. The corners keep the shape with or without the padlock. The
green rotate knob moves aside when a corner handle sits where it usually goes.
### Fill, Feather and Resize
- **Fill** (`fill_color`): the colour of the added space and of the empty
corners a turn leaves. It starts gray. Added space on the picture is drawn in
the real fill colour with a faint hatch, and only the part the crop cuts away
is darkened.
- **Feather** (`feather`, 24 px to start): softens the edge of the **mask** into
the kept picture, so the model and the stitch blend the seam. The image itself
keeps a hard edge against the fill. That solid band is what outpaint models
and LoRAs recognise as the area to paint.
- **Resize** (`resize_to_megapixels`, off to start): resizes the finished output
to a size budget and keeps its shape. Turning it on opens a row with
**Megapixels** (`megapixels`, the budget) and **Step** (`resolution_steps`,
the number each resized side rounds to; 8 or 64 suits most image models).
**Method** (`resize_method`) is in the editor, under **Resize output**.
The edge between your picture and the fill stays one clean line at any size.
### Align
**Align** (`canvas_multiple`) makes the width and height divide evenly by
the number you pick. Some models need sizes divisible by 8, 16 or 32. At 1 it is
off. Its arrows go 1, 8, 16, 24 and so on; hold Shift to step by 1.
Where you padded, the padding grows by the few pixels needed: on the right or
bottom band if there is one, otherwise on the left or top. Where you did not
pad, no fill is added. The picture is trimmed by a few pixels instead, evenly
from both edges, so an edge you did not pad never gets a thin strip for the
model to paint. A picture smaller than the number itself is padded on the right
or bottom.
### The size line
The line under the picture names every step that sets the output size, in the
order the run applies them, with the size the run emits last and brightest:
`crop 2080×1170 → pad 2208×1298 → round to 64 2240×1344 → resize 1344×768`. A step
that changes nothing is left out, and "round to" is **Align** at work. An
amber line warns when rounding each side to the Step stretches the picture by
more than 1% (`Resize makes the picture 1.5% taller`); its tooltip names a
Step that avoids it, and with Fit on pad, **Pad to keep shape** adds a few pixels of
padding so nothing stretches. It also warns when the resize undoes **Align**. Hover the line for the same breakdown line by line; the editor's right
panel shows it too.
The amber line keeps still while you drag a handle or a number. If the drag
makes it untrue it goes away, and the line for the new size comes back a
moment after you let go. The space for the warning stays under the dimensions. On a narrow node, hover
over shortened text to read it in full.
### Reset crop and Reset
**Reset crop** shows the whole picture again without changing rotation or
padding. **Reset** clears rotation, crop and padding and turns the padlock off;
Fill, Feather and **Align** stay.
## Drawing a mask
Right-click the node and choose **Open in MaskEditor** to paint over the parts
you want the model to repaint. After **Save**, the picture on the node shows
them in teal, and the teal moves with the picture when you crop, turn or pad
it. The full editor shows it too. The run treats painted parts like the
padding: it fills them with the fill colour and marks them in the **mask**,
so under the teal you see the fill colour, not your picture.
**Keeping your picture under the paint.** Some workflows need what was under
the paint: an inpaint with denoise under 1, or an edit model that has to see
what it changes. Open the editor and set **Painted area** to **keep**, in the
**Inpaint & Stitch** section. The **mask** still marks the painted parts, and
your picture stays under them in **image**, **original** and
**prompt_image**. The picture on the node still shows the fill colour under
the teal.
See-through parts of a PNG show in teal the same way, because the node paints
them too. To hide the teal, click the gear next to **Load** and turn off **Show the mask on the picture**. The choice applies
to every Image Crop + Rotate + Pad in this browser.
## Outputs
- **image**: BHWC float image batch. Animated image frames receive the identical transform.
- **mask**: BHW generated-area mask combining source transparency, empty rotation corners, and padding.
- **image**: the turned, cropped and padded picture. Every frame of an animated
image gets the same framing.
- **mask**: White where the model paints: the padding, the empty corners a turn leaves, and the see-through parts of your picture.
- **stitcher**: Wire to Stitch Inpaint to restore the kept canvas around an outpaint result, blended into the source by **Blend** (32 px unless changed). It follows the final resized canvas. After a hand crop, set **Paste onto** to original in Stitch Inpaint's gear menu to get your whole picture back with the edit in place.
- **original**: your picture before rotation, crop, padding or resize, with
see-through parts shown as white (your picture there when **Painted area**
is on keep).
- **width** / **height**: The output size after the transform and any resize.
- **prompt_image**: The image again, with see-through parts shown as white instead of the fill colour. Wire it to whatever writes your prompt. It is the same as **image** for a picture with no see-through parts, and when **Painted area** is on keep.
## Editor gestures
## See-through pictures
Drag cyan squares to resize the crop, drag inside to move it, orange diamonds to add padding, and the green handle to rotate. Hold `Shift` while rotating to snap to 15 degrees. Use the wheel to zoom and middle mouse or `Alt`-drag to pan. The same handles work directly on the node's compact preview (fit-only there — the wheel keeps zooming the graph); zoom and pan are editor-only.
A PNG can have see-through parts: a product cutout with no background, a
photo with round corners, a picture with a hole in it. The node treats those
parts like the padding. They are filled with the fill colour, marked in the
mask, and the model paints them. Stitch Inpaint never puts them back over
the painted result.
The node performs no network requests and writes no files beyond a normal user-initiated ComfyUI upload.
Use **prompt_image** for the node that writes your prompt. There the
see-through parts show as white, the way a picture viewer shows them, so the
prompt describes a real backdrop. Shown the gray fill instead, a prompt
writer calls it a "gray backdrop" and the model keeps the flat gray. The
model itself still gets **image**, with the fill colour it was trained on.
A picture with no see-through parts comes out exactly as before.
### Technical details
- A pixel counts as see-through when it is less than 90% as solid (alpha)
as the most solid pixel in the picture. Measuring against the most solid
pixel keeps a picture saved at, say, 50% opacity throughout whole instead
of painting it over.
- A picture that is at least 90% solid everywhere is left exactly as it was
loaded. Generated pictures often carry alpha values of 249-254 in places,
and those pictures do not change.
- Kept pixels are used fully solid, in their own stored colour. See-through
pixels never reach the canvas: their stored colour (often black, or the
old background) is what used to leave a dark or light ring round a cutout.
- Why 90%: on an oval photo whose edge stores colours darkened by their
alpha, a 50% cut kept a rim up to half dark, the ring the model painted
back. At 90% the kept rim is at most 10% darker, and matting noise inside
solid subjects stays above it, so no holes open there.
- A turned picture's see-through parts turn with it, like its corners. In
**prompt_image** they show white while the padding and the corners keep
the fill colour.
- The MaskEditor saves its mask as see-through parts of the picture, so the
same rule applies: a stroke more than 10% strong is painted in full,
including one at the MaskEditor's default 70% opacity.
## The editor
**Open editor** opens the full-screen canvas. Its left side holds the node's own
controls: the same ratio row with its orientation button and padlock, the same
**Fit** switch, and the same number boxes (drag to scrub, click to type, Shift
for fine steps). The editor adds a few things the node does not show:
- **Rotate → Degrees** (`rotation_degrees`): type an exact turn, clockwise.
Each step is 1°, or 0.1° with Shift. **Reset rotation** sets it back to 0.
- **Padding & mask → Reset padding** removes all padding. The same section has
Fill, Feather, Align and a **Center** row.
- **Resize output → Method** (`resize_method`): how the resize samples the
picture. `lanczos`, the default, is the sharp one.
- **More**: ratios you added in `ausboss_presets.json` that no button shows.
The list only appears when you have some.
- **Reset view** undoes zoom and pan. **Reset transform**
clears rotation, crop, padding, fill, feather and Align; the picture,
resize and stitch settings stay.
- **Preview**: a small picture of the finished output while you work.
A ratio replaces the existing crop and padding and keeps rotation, fill and
resize settings. **Align** and the resize **Step** can slightly change
the fitted shape. The size box on the stage sits clear of the handles.
### Handles and gestures
Drag cyan squares to resize the crop, drag inside to move it, orange diamonds to add padding on one side, orange corners to make the canvas bigger or smaller in its own shape (hold `Alt`, Option on a Mac, for all four sides), and the green handle to rotate. Hold `Shift` while rotating to snap to 15 degrees. The knob turns all the way round either way: past 180° it carries on from -180°, so an upside-down picture turns back up whichever way you drag. Rotating keeps the crop's size and keeps it over the same part of the picture, whichever control turns it (knob, number box, Reset rotation); with no crop the canvas grows to hold the tilted picture. The knob keeps clear of the padding handles and crop squares. Use the wheel to zoom and middle mouse or `Alt`-drag on an empty spot to pan. The same handles work directly on the node's compact preview (fit-only there — the wheel keeps zooming the graph); zoom and pan are editor-only.
### Inpaint & Stitch
The editor's right side holds the stitcher's settings, the same as on the clip node:
- **Blend** (`stitch_blend`, default 32): the ramp, in output pixels, where generated
pixels fade over the source. It is separate from **Feather**, which shapes the mask.
- **Show blend** tints the stage with the paste mask: the generated area (painted and
see-through parts included), the feather, then grow and blend applied through any resize.
- **Advanced → Grow paste** (`stitch_grow`, default 0) moves the paste boundary first:
a few positive pixels let the generation repaint the source edge when a seam still shows.
- **Painted area** (`painted_area`, default `fill`): what a part you painted in the
MaskEditor is handed on as. **fill**: the fill colour, so the model never sees what was
there. **keep** (`keep picture`): your picture stays under the paint and only the mask
marks it. See-through parts of a PNG follow the same switch; a cutout usually stores
black or its old background there, so leave a cutout on fill.
## Technical details
- **Saved values.** The handles write plain number values that save with the
workflow and that an API workflow can set directly. None of them shows as a
row on the node:
- `crop_x` / `crop_y` / `crop_width` / `crop_height`: the crop, in pixels of
the turned picture. Width and height `0` mean the full size.
- `pad_left` / `pad_top` / `pad_right` / `pad_bottom`: the added space on
each side, in pixels.
- `crop_aspect_ratio`: `free`, `source` or a fixed ratio that the crop box
keeps. It stays `free` unless Fit is on crop and the padlock is on.
- `fill_color`: `#RGB` or `#RRGGBB`, three numbers (`R, G, B`), one gray
number, or a colour name. Anything it cannot read becomes mid-gray.
- **Corners.** A corner follows the pointer along the canvas diagonal, and the
shape it keeps is the canvas before **Align** rounds it. With
Align on, **Center** counts the fill it adds, so the bands you see
come out even.
- **Resize.** It works like core's *Scale Image to Total Pixels*: the budget is
`megapixels × 1024 × 1024`, the shape is kept, and each side rounds to a
multiple of **Step**. Your picture is resized by itself, with the chosen
**Method**, and the fill is put around it afterwards, so no fill is mixed
into the picture's edge and each band is a whole number of pixels wide. A
band that would come to under half a pixel is left out. The mask inside the
picture (the feather, the corners of a turn, see-through parts) is resized
with it, bilinear so its edges cannot ring.
- **Your own ratios.** Copy `ausboss_presets_example.json` in the pack's folder
to `ausboss_presets.json` and edit the list, then reload the browser tab.
- Restoring a workflow or undoing a change refreshes the picture without
resetting the saved framing.
- The node performs no network requests and writes no files beyond a normal
user-initiated ComfyUI upload.
+132
View File
@@ -0,0 +1,132 @@
# Image Folder
Pick a folder, tick the pictures you want, and run the workflow on each one.
Use it when the same job has to be done on many pictures: change one thing in
all of them, caption them, resize them, make pairs for training.
## How to use it
1. Bring your folder in. Drop it on the node from your file explorer. Or
press **Browse**, then **Copy a folder from your computer**, and choose
it. Its pictures are copied into ComfyUI's `input` folder and show as
tiles.
2. A folder that is already inside ComfyUI's `input` or `output` folder is
chosen with **Browse**.
3. Click a tile to take a picture out or put it back. **Shift + click** does
every tile from the last one you clicked to this one. **All** and **None**
do the whole folder.
4. Wire **image** into your workflow, and **filename** into Save Image's
`filename` so each result keeps its picture's name.
5. Press **Run**.
## A folder from anywhere on your computer
The node only reads folders inside ComfyUI's own `input` and `output`
folders. A folder from anywhere else is copied in:
- **Drop it on the node**, or choose it with **Browse → Copy a folder from
your computer** or **Add → A whole folder**. Your file explorer opens for
the last two.
- **Your browser asks first** when you choose a folder in the file explorer.
It shows its own question, such as "Upload 74 files to this site?". The
site is your ComfyUI, and the upload is the copy into its input folder.
The node cannot change those words. A folder dropped on the node is
copied without that question.
- Its pictures go into `input/<the folder's name>`, folders inside it too,
and the node moves to that copy.
- **A really big folder asks first.** With more than 300 pictures or more
than 1 GB, the node says how many and how much, and waits for your yes.
Copying takes that much disk space again.
- Bringing the same folder in again writes over its copies. It does not
pile up doubles.
- A path typed or pasted into **Folder** works when it lies inside ComfyUI's
input or output folder. For any other path the node says so, with a
button that opens the file explorer.
## One run, or one picture per run
**Run** decides how the pictures go through.
- **all in one run**: one press of Run does every picked picture, one after
the other. If one picture stops the workflow, the ones after it are not
done.
- **one per run**: each run loads one picture, and **Picture** moves on by
one when the run is queued. Set ComfyUI's run count to the number of
pictures and they are all queued at once. A picture that fails only skips
itself. The tile with the white ring is the next one, and a double click on
a tile makes it the next.
After the last picture, **At the end** either stops the run with a message
or starts over at the first. With **start over**, Picture goes back to 1 and
the pictures repeat in order for as many runs as you queue.
## The card
- **From** (`source`): ComfyUI's **input** folder, or its **output** folder
for pictures you made.
- **Folder** (`folder`): the folder inside it. Empty is the input or output
folder itself. **Browse** lists the folders there.
- **Subfolders** (`subfolders`): also take the pictures in the folders inside.
- **Order** (`sort`): by name, or newest or oldest first.
- **Run** (`run`): see above.
- **Picture** (`position`): with one per run, the picked picture the next
run loads, counted from 1.
- **After a run** (`after_run`): **next** moves Picture on, **stay** keeps
loading the same one, **random** jumps to any of the picked ones.
- **At the end** (`at_the_end`): after the last picture, **stop the run**
stops with a message, and **start over** goes back to the first and keeps
going.
## The tiles
- The line above them says how many are picked, and with one per run which
one is next.
- A picked tile is bright with a tick and its number. The others are dim.
- A number on teal means that picture went through since you opened the
workflow.
- **Add** copies pictures, or a whole folder, from your computer into
ComfyUI's input folder.
- **↻** looks in the folder again.
## Outputs
- **image**: each picked picture, one at a time. What comes after runs once
for each.
- **mask**: white where the picture is see-through. A picture with no
see-through part gives the same empty mask Load Image does.
- **filename**: the file name without its ending.
- **index**: the picture's number among the picked ones, from 1.
- **count**: how many pictures are picked.
## Technical details
- **Only ComfyUI's own folders.** The node reads folders inside ComfyUI's
input or output folder and nothing else. A path that leads anywhere else is
refused, and so is a link inside those folders that points out of them.
There is no setting that changes this.
- The picked pictures are saved in the workflow as a list of names
(`pictures`). Empty means every picture, so a picture added to the folder
later is in too. A name that is no longer in the folder is skipped. It
takes no wire: the ticks on the node write it.
- Pictures are PNG, JPG, WebP, BMP, GIF (first frame) and TIFF. A photo that
was taken turned comes out upright. Files whose names start with a dot are
skipped.
- By name means the way a person sorts: `photo_2` before `photo_10`.
- With **Subfolders** on, a picture's `filename` carries its folder in
front (`trips/beach.png` gives `trips_beach`), so two files with one name
never save over each other.
- **image**, **mask**, **filename** and **index** are lists. Pictures keep
their own sizes. They are not resized or stacked into one batch.
- Moving Picture on is done by the editor when a run is queued. A workflow
sent through the API loads exactly the picture `position` names.
- With **start over**, a Picture number past the last picture counts round
from the first. A workflow saved with such a number loads that picture and
moves on from there.
- The node runs again when a picked file is added, removed or rewritten.
- A folder lists up to 5,000 pictures, and a folder that is brought in is
copied up to 3,000.
- Bringing a folder in is done by your browser through ComfyUI's own upload.
The server never opens a path outside ComfyUI's input and output folders.
- A mask painted in the MaskEditor is not part of a folder picture. Use Mask
by Name, or load that one picture with a single-picture loader.
+76
View File
@@ -0,0 +1,76 @@
# Image Resize
Resizes a picture. Choose how to set the new size: an exact width and
height, the longest or shortest edge, a megapixel budget, or a multiplier.
A mask can ride along and gets the same resize, and the new width and
height come out as numbers you can wire onward.
A new node starts on **Megapixels** at 1 MP, with **Fit** on fit,
**Multiple** 1 and the lanczos filter.
## Target modes
**Target** (`target_mode`) picks the mode. The card shows only that mode's
size row, and the other size values are ignored.
- **Width × height** (`width+height`): The two **Size** fields, W and H
(`width`, `height`). Their input sockets sit with the node's inputs,
under `image` and `mask`; a linked one greys its field out. `0` keeps that
source dimension, and with only one set the other follows the source
aspect.
- **Longest edge** / **Shortest edge** (`longest_edge`, `shortest_edge`):
Scales until that edge equals **Edge** (`edge_length`) exactly; the other
edge keeps the aspect. `0` keeps the source size.
- **Megapixels** (`megapixels`): Scales (aspect preserved) until width ×
height is about **Budget** (`megapixels`) million pixels. `0` keeps the
source size.
- **Scale factor** (`scale_factor`): Multiplies both dimensions by **Scale**
— `0.5` halves, `2.0` doubles. `0` keeps the source size.
## Fit
**Fit** (`keep_proportion`) decides what happens when the target aspect
differs from the source. It only matters in **Width × height** mode: in the
other modes the picture always keeps its shape, whatever Fit says.
- **stretch**: Distorts straight to the target.
- **fit**: Shrinks the target box to the source aspect — the output can be
smaller than requested, but there are never bars and nothing is cropped.
- **cover** (`cover_crop`): Fills the target completely and center-crops
the overflow.
- **pad**: Fits inside the target and fills the rest with **Fill**
(`fill_color`: hex, `R, G, B`, one grayscale number, or a CSS name; a
color the node cannot read becomes mid-gray). The
new bars are `1.0` in the mask output — the same generated-area contract
as the pack's other pad nodes, so it feeds an inpainter directly.
## Notes
- **Multiple** (`divisible_by`) snaps each output dimension to the nearest
multiple (never below one step) — `16` for WAN, `8` for most latent
spaces. It wins over exact proportion, so a dimension can shift by up to
half a step. It applies even when everything else says "keep the source".
- **Bars only appear in Width × height mode with Fit on pad and both W and
H set.** In every other target mode the box is derived from the source's
own aspect, so there is nothing to letterbox against: the proportion modes
all resolve a `divisible_by` snap with an invisible sub-half-step resize,
and the mask output stays black. A Width × height box with a `0` in it
follows the source the same way, so pad adds no bar to it. With both set,
bars (white in the mask) fill whatever the picture does not cover. That
includes a bar a pixel or two wide when the size is rounded, for example
a 1000×700 picture with W 1000, H 700 and **Multiple** 16 becomes
1008×704 with a 2 px bar.
- **Filter** (`interpolation`): `lanczos` (PIL, in float — the sharpest
all-rounder), `bicubic`, `bilinear`, `nearest` (pixel art, hard masks),
`area` (best for strong downscales). A resize that changes nothing passes
the tensor through bit-identical.
- The mask output is the input mask through the same transform, plus the
pad bars. With no mask wired it is black, except for pad bars, which are
white. IMAGE is BHWC, MASK is BHW, and batches flow through frame by
frame.
## Outputs
- **image**: the resized picture.
- **mask**: the mask, resized the same way.
- **width** / **height**: the new size in pixels.
+12 -2
View File
@@ -1,13 +1,16 @@
# Image Size
Reads an image's dimensions and hands them out as INTs: **width**,
**height**, **longest_edge**, and **shortest_edge**.
**height**, **longest_edge**, **shortest_edge**, and **count**.
Wire them wherever a workflow wants numbers that must match the actual
image — an Empty Latent sized to the source, a resize keyed off the longest
edge, aspect-aware math with the shortest. Because the values come off the
wire, they can never drift out of date the way retyped numbers do.
After a run the node shows each number beside its output, such as
`width: 1024`, so you can read the size without wiring anything to it.
## Outputs
- **width** / **height**: The frame size in pixels.
@@ -15,9 +18,16 @@ wire, they can never drift out of date the way retyped numbers do.
keys on.
- **shortest_edge**: `min(width, height)` — the number crop-to-fill and
many preprocessing pipelines key on.
- **count**: The number of images in the batch — `1` for a single image,
the frame count for video frames.
## Notes
A batch reports the size its frames share (IMAGE batches are uniform by
construction). The node does no work worth caching or canceling — it reads
two numbers off the tensor's shape.
its numbers off the tensor's shape.
The numbers beside the outputs are from the last run. They are saved with
the workflow, so a workflow you open again shows them, and they are cleared
when you wire in another picture. If the node measured a list of batches in
one run, it shows no numbers, since no one number is true for all of them.
+51
View File
@@ -0,0 +1,51 @@
# Integer
A whole number. Set a step count, a size, or any other whole number once and
wire it to every node that needs it.
## Controls
- **Value** (`value`): The whole number to send out. It starts at `0`. The
arrows and a drag move it by 1, or click and type. It holds numbers as
big as a seed.
- The gear, right: **Look**. **normal** is the usual box with its label.
**big** shows the number large in the middle of the node, with an arrow
on each side, for a setting people change on every run, such as the
length of a clip. A drag and typing work the same. Each node keeps its own look, and it is
saved with the workflow.
## Limits
Wire the output to a setting that has limits and the box takes them over.
Wired to a sampler's **steps**, it goes from 1 to 10000: type 0 and you get
1. The card shows **1–10000** beside the value.
- It works through a Reroute and into a box (subgraph) too.
- Wired to two settings, it keeps to what both allow.
- Unwire it and it takes any whole number again.
- A new box takes the value of the first setting you wire it to, so wiring
it up changes nothing until you move it. A box you have already set, or
one that came with a workflow, keeps its own value.
## Output
- **int**: The value, unchanged.
For arithmetic on the way — halving a size, adding a margin — feed this into
**Math Expression 🆎** instead of retyping derived numbers.
## Technical details
- The limits are those of the input the wire lands on, the way ComfyUI's own
Primitive node takes them.
- A value that is outside the limits when the wire is made is brought to the
nearest one.
- The limits are kept by the node's face. A value sent straight through the
API is passed on as it is.
- On its own the box goes up to 9,007,199,254,740,991, the largest whole
number a browser holds exactly. It stopped at 2,147,483,647 before, which
most seeds are above.
- The look is stored in the node's properties as `ausboss_text_look: "big"`, the
same as Text 🆎. It is not an input, so the saved value and what the node
sends are the same in both looks, and an older version of the pack shows
the usual box.
+71
View File
@@ -0,0 +1,71 @@
# Krea 2 Encode
The prompt node for Krea 2. Type your prompt and negative prompt here, and
optionally give it a picture for the result to follow, all in one node.
Wire a VAE and a reference image and the reference is attached to the
positive output. Leave them unwired and this is a plain two-prompt encoder.
The negative comes out of the same node, so a turbo graph running at CFG 1.0
still has something to plug in without a second text encode sitting there
doing nothing.
For an outpaint with the AnyPaint LoRA, also wire the **mask** from the node
that padded your picture. Without it, a dark or very colourful picture can
come back with the new area still gray, like a gray frame or a gray corner.
## Controls
- **clip**: The Krea 2 text encoder.
- The big text box (`prompt`): Describe the whole finished canvas, not just
the new area — the model generates all of it and matches the reference
where it must. The two boxes have no labels, only gray hint text.
- The small text box (`negative_prompt`): Ignored at CFG 1.0, which is where
turbo runs. Empty is the normal case there.
- **vae**: Needed to turn the reference images into latents. Without it no
reference is attached to the result. If **VLM reference** is on, the text
encoder still looks at the pictures.
- **reference**: The image the result follows. For AnyPaint that is the
padded `image` from Image Crop + Rotate + Pad 🆎 or Load Image + Pad 🆎;
for Registered Outpaint, Load Image + Pad 🆎's unpadded `reference`
output. Any image works; it is fitted to a multiple of 16 first.
- **extra_image**: A second reference, e.g. a style or character plate.
- **VLM reference** (`vlm_reference`, off by default): Also show the
references to the vision tower, so the text encoder describes them. On for
AnyPaint with the padded
canvas as the reference; off for Registered Outpaint with the unpadded
source, where the description tends to pull the result toward a paraphrase
of the source. The Krea 2 Outpaint Model Patch page covers both setups.
- **mask**: The area to paint: the `mask` output of Image Crop + Rotate +
Pad 🆎 or Load Image + Pad 🆎, or the painted mask of an inpaint. Wire it
for AnyPaint. The reference then shows that area in your picture's own
colour instead of the gray fill, and the model paints it.
## Outputs
- **positive**: Prompt conditioning with the reference latents attached.
- **negative**: Negative prompt conditioning.
## Notes and limitations
- **References are downscaled to a 384px long edge and snapped to a multiple
of 16.** The VAE downsamples by 8 and the DiT patchifies by 2, so an edge off
a multiple of 16 lands on a partial patch and the token grid stops lining up
with the canvas grid. The cap is deliberate — a reference is there to say
"this is the picture", and the canvas latent carries the detail.
- Attaching reference latents alone does not tell the model *where* they go.
Pair this with Krea 2 Outpaint Model Patch 🆎 for outpainting.
- On a core without the Krea 2 prompt template, `vlm_reference` still works —
it falls back to tokenizing without the template.
## Technical details: why the mask matters
AnyPaint was trained with the new area of its reference filled with the
median colour of the rest of the picture, never with a fixed gray. The
padded canvas carries a flat `#808080` gray instead. Next to a picture whose
median is close to mid-gray that makes no difference, but next to a dark or
saturated picture the model reads the gray as part of the scene and paints
it back, as a flat gray frame or corner. The mask's new area (where it is
above 0.5) is refilled with the median colour of everything else before the
reference goes to the VAE and to the vision tower. The canvas the rest of
the graph uses keeps its gray, so the prompt writer and the stitch are
unchanged. Only `reference` is refilled; `extra_image` is left as it is.
@@ -0,0 +1,103 @@
# Krea 2 Outpaint Model Patch
Tells Krea 2 **where** a reference image sits on the canvas. Use it when you
extend a picture with Krea 2 (outpainting), so the model carries your picture
on instead of painting something that only looks similar.
Place it **after** any LoRA loader and **before** the sampler.
How it works: a reference normally reaches the model with no position, so
the model treats it as a loose style hint. It borrows the look and reinvents
the content. This patch pins the reference to the rectangle the stitcher
reports.
## Controls
The node's card shows **Reference** (`source rect` / `whole canvas`) and
**KV cache** (`every step` / `once per run`). Both accept links and retain
their saved values when the workflow is reloaded.
- **model**: A Krea 2 model. Patch last, so a LoRA loaded afterwards does not
replace the patched forward pass.
- **stitcher**: From Load Image + Pad 🆎 or a Crop + Rotate + Pad 🆎 node.
It tells the patch where your picture sits on the canvas. With
a stitcher from Crop For Inpaint 🆎, set **Reference** to `whole canvas`:
on `source rect` the reference is pinned to where the crop sat in the
full picture, not spread over the crop.
- **KV cache** (`kv_cache`, `once per run` by default): Compute the
reference's keys and values once per run instead of once per step. The
reference does not change while sampling, so this is free speed. Set it to
`every step` only to rule the cache out when debugging.
- **Reference** (`placement`): Where the reference tokens go. `source rect`
(`source rectangle`, the default) pins them to the rectangle the stitcher
reports. `whole canvas` spreads them over the full frame. Which one is
right depends on the LoRA — see below.
## Output
- **model**: The model with reference tokens registered into the canvas grid.
## Two LoRAs, two placements
The base Krea 2 weights were never trained on registered references; what
makes this patch work is a LoRA trained for it, and the two that exist want
different placements.
**Every side at once — `whole canvas` + AnyPaint.** yijunwang2's
[Krea 2 AnyPaint](https://huggingface.co/yijunwang2/krea2-anypaint)
(`krea2_anypaint_rank32.safetensors`) was trained with the padded canvas
itself as the reference, spread over the whole frame, and the known pixels
held in place by the sampler. In its training the new area of that reference
was always a flat patch of the picture's own median colour. Wire it like the
*Krea 2 Outpaint* example:
- A pad node with any padding you like: Image Crop + Rotate + Pad 🆎, as in
the example, or Load Image + Pad 🆎. Left, right, top and bottom together
are fine. Flat colour fill.
- Krea 2 Encode 🆎 with the pad node's **image** output (the padded canvas,
not Load Image + Pad's unpadded *reference* output) as the reference,
`vlm_reference` **on**, and the pad node's **mask** wired into its `mask`
input. The mask makes the encode refill the new area with the picture's
median colour, as in training. Without it a dark or very colourful picture
can come back with the gray padding painted in as a gray frame.
- This patch on `whole canvas`.
- VAE Encode the padded canvas, Set Latent Noise Mask with the pad mask, and
sample from that latent at full denoise. The mask is what keeps the source
pixels; the reference is what tells the model what they are.
To repaint part of a picture instead of extending it, use the *Krea 2
Inpaint Masked* example: Crop For Inpaint 🆎 cuts out the area you painted,
and this patch runs on `whole canvas` with the same LoRA. Load Image + Pad 🆎
itself does not take a painted mask.
**One axis per pass — `source rectangle` + Registered Outpaint.** The same
author's [Registered Outpaint](https://huggingface.co/yijunwang2/krea2-outpaint)
(`krea2_outpaint_rank32.safetensors`) is the LoRA this placement was built
for: the *unpadded* source goes in as the reference (Load Image + Pad 🆎's
`reference` output, **VLM reference** off) and is pinned to its rectangle. It
was trained on a source spanning one whole canvas axis — pad left/right and
the source must span the full height, pad top/bottom and it must span the
full width. Padding both axes in one pass is outside its training and the
new region can break up; the node says so in the console when it sees it,
and the cure is two passes: pad one axis, run, load the result, pad the
other. **A thin strip counts.** More than 8 px of padding on the other
axis, such as a few pixels left over from a drag, means the source no
longer spans it. **Align** on the Crop + Rotate + Pad nodes and
Load Image + Pad's **Multiple** never add a strip on a side you did not
pad. The warning reports the spare pixels on each axis.
Without either LoRA the patch still places the reference, but the bare
Turbo model treats it loosely and results are hit and miss.
## Notes and limitations
- **Nothing happens without reference latents.** If the conditioning carries
none, the patched forward pass calls straight through to the original. Wire
Krea 2 Encode 🆎 with a VAE and a reference image.
- **It patches comfy internals.** The placement reaches into the flux
attention layers, so a ComfyUI release that moves them can break this node
specifically. It imports those internals when you run it rather than at
startup, so a break surfaces as an error on this node instead of the node
disappearing from the menu.
- Reference tokens cost attention. The reference is shrunk so its longest
side is 384 px before encoding for that reason — see Krea 2 Encode 🆎.
+44 -7
View File
@@ -1,28 +1,65 @@
# LaMa Inpaint
Removes masked content with a TorchScript LaMa checkpoint. It processes video
frames one at a time to keep VRAM bounded and composites the result through the
input mask so pixels where the mask is zero remain unchanged.
Removes whatever you paint over, such as a watermark or an object, and fills
the gap to match what is around it. It works on pictures and on video, and
everything outside the mask stays exactly as it was.
It uses the LaMa model, which you download once (see below). Video frames are
done one at a time, so a long clip does not need more graphics memory.
## Model setup
Place `big-lama.pt` in:
Download [`big-lama.pt`](https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt)
and place it in:
```text
ComfyUI/models/lama/big-lama.pt
```
The node never downloads a model. After adding or replacing a checkpoint,
refresh the browser or restart ComfyUI so the model list is updated.
refresh the browser or restart ComfyUI so the model list is updated. Until
the file is there, the list still shows `big-lama.pt`, and a run stops with a
message that says where to put it.
The node only opens known copies of this file, whatever you name them: the
download above, or the one from
[fashn-ai/LaMa](https://huggingface.co/fashn-ai/LaMa). A LaMa model file can
run code when it opens, so any other file stops the run with a message and is
never loaded.
## Controls
- **image**: BHWC images or video frames.
- **mask**: BHW mask where white identifies content to replace. One mask is
broadcast across the batch; otherwise supply one mask per image.
- **model**: A `.pt` or `.pth` TorchScript checkpoint from `models/lama`.
broadcast across the batch; otherwise supply one mask per image. A mask of
another size is stretched to fit the picture. A picture with no mask
painted on it stops the run with "No mask painted" instead of coming back
unchanged.
- **Model** (`model`): A known copy of `big-lama.pt` from `models/lama`.
## Output
- **image**: A finite BHWC image batch with the original batch size, dimensions,
data type, extra channels, and unmasked pixels preserved.
## Preview
The panel shows the inpainted result, and before a run it falls back to a
thumbnail of whatever feeds the **image** input. A video inpaint streams each
finished frame to the panel as it goes, so a long run shows its progress
rather than sitting blank. A small badge above the node counts the frames
("frame 3/120"); **Settings → AusBoss → Chrome → Live status** turns the
badges off. When the run ends, the panel keeps the first frame.
There is one preview surface: ComfyUI's own result preview for this node is
stood down so the picture does not appear twice, once in the panel and again
underneath the node.
The node performs no network requests. It writes one preview PNG per run into
ComfyUI's temp folder, alongside what `PreviewImage` writes.
The small **preview** switch at the right of the bar above the picture turns
it off: the picture's box disappears, the node is that much shorter, and no
preview file is written. Turn it back on and the node returns to its height
with the last result in it; if the node ran while the switch was off, it asks
you to run the workflow again. The switch is the node's optional `preview` input.
+213
View File
@@ -0,0 +1,213 @@
# Layout Boxes
Draw boxes on a canvas and say what goes in each one: a thing, or words to
write in the picture. The picture you make then has each thing where you put
it. Use it for layout prompts, such as MOSS-Image's.
It does what ComfyUI's own Create Bounding Boxes does and gives the same
outputs. Its canvas also works in the classic node look, where Create Bounding
Boxes only shows "Node 2.0 only".
## How to use it
1. Set the canvas size in **Canvas** (W × H), or wire `width` and `height` from
Latent Size, so the boxes and your picture have the same size.
2. Drag on the canvas to draw a box. Its tag shows its number and its type,
such as **1 object**.
3. Under the canvas, the row **Box 1 is** sets what the chosen box holds,
**object** or **text**. Then type its words:
- **object**: the **Description** says what is in the box, for example "a
red bicycle leaning on a brick wall".
- **text**: the **Text** is the words to write in the picture, for example
"OPEN 24 HOURS". The **Description** says how they look, for example
"pink neon letters".
4. Wire `elements` to the `element` input of MOSS-Image Layout Prompt, and give
that node the same width and height.
Wire `preview` to a Preview Image to see the boxes with their numbers and words.
## Change the words in a picture
The `mask` output is white inside every box. Use it to change only one part
of a picture, such as the words on a sign or a shirt.
1. Wire a Load Image into `background` and pick your picture. It shows on the
canvas right away.
2. Draw a box over the old words, a little wider than the old words on every
side (about 12 pixels), or bits of the old letters can show at its edge.
Keep faces and hands out of it: whatever is in the box is drawn again.
The arrow keys move the chosen box one pixel at a time.
3. If the words run at a slant, turn the box to match them: drag the round
handle over the box (see **Turning a box** below).
4. Make it a **text** box. Type the new words in **Text**, and how they look in
**Description**, for example "white letters on a red sign".
5. Soften the edge of `mask`, for example with Mask Refine 🆎 at a blur of
about 6, or a line can show around the box. Then wire it into the inpaint
part of your workflow: Set Latent Noise Mask, so only the box is drawn
again, and Image Composite Masked, so everything else stays as it was.
6. With MOSS-Image, wire `elements` to MOSS-Image Layout Prompt. The model
gets the new words from the box, and your prompt only describes the picture.
## Turning a box
Words on a sign or a shirt often run at a slant. Turn the box to lie along
them, and only the slanted words are drawn again.
1. Click the box to choose it. A round handle shows over the middle of its
top edge.
2. Drag the handle around the box. Hold Shift to turn it in steps of 5
degrees.
3. Or type the angle in **Turn**, under the canvas: plus turns the box
clockwise. **0°**, or a double-click on the round handle, makes the box
level again.
The box's tag shows how far it is turned, for example **1 text 25°**. A turned
box still moves, sizes and takes the arrow keys. Its corners may reach past
the edge of the picture, so it can cover words that run to the edge.
A turned **text** box tells the model the slant for you: its description in
`elements` gets "tilted about 25 degrees, running downhill to the right" (or
"uphill") added, so you do not have to type it.
If the picture is resized on its way to the model, resize the mask with it:
Image Resize 🆎 takes a mask and gives it the same size and crop.
## Drawing and moving boxes
| Do this | To get this |
|---|---|
| Drag on an empty spot | Draw a new box |
| Shift + drag over a box | Draw a new box on top of it |
| Drag a box | Move it |
| Drag a corner or an edge of the chosen box | Make it bigger or smaller |
| Drag the round handle over the chosen box | Turn it (Shift: steps of 5 degrees) |
| Double-click the round handle | Make the box level again |
| Double-click a box | Type its words |
| Click a box's chip under the canvas (**1 text**, **2 object**) | Choose that box, also one hidden under another. When there are more boxes than chips, **‹ ›** step through them |
| Delete | Remove the chosen box |
| Arrow keys | Move the chosen box one pixel (Shift + arrow: ten pixels) |
| Ctrl + D | Copy the chosen box |
| Esc | Let go of the box, or put back a box you are dragging |
| Hold Ctrl (or Alt) while dragging | Place the box freely, off the grid |
The keys work while the canvas has the focus: click the canvas, or a box's
chip, first.
The handles and the tags keep their size on your screen when you zoom the
graph in or out, and a box too small for eight handles keeps its corners.
Over a picture the boxes are see-through and show no words, so you can read
what is under them; the chosen box's words are under the canvas.
An **object** box does not write words. If you typed words while it was a
**text** box, a line under the canvas says they are kept: click **text** and
they come back.
## The order of the boxes
Box 1 is at the back, and the last box is in front, so draw the things at the
back first. The two square buttons under the canvas move the chosen box back or
forward. MOSS-Image Layout Prompt's `character_boxes` takes these numbers.
## Colors
A box can have up to five colors for the model to use on that thing. **+** adds
a color, a click on a color changes it, and a right-click (or its cross)
removes it. The first color also colors the box on the canvas.
## The bar above the canvas
- **Grid**: boxes snap to a grid while you draw, move and size them with the
mouse. It is on for an empty canvas and off over a picture on `background`,
so a box can go anywhere on the picture. Click it to turn it on or off; the
node keeps your choice. Hold Ctrl (or Alt) while dragging to leave the grid
for one drag. The cells are 32 pixels, bigger on a very big canvas. A
turned box does not snap: it moves and sizes one pixel at a time.
- **Layouts**: start from a ready arrangement: one in the middle, two side by
side, three across, top and bottom, a title over a picture, four corners.
The boxes you already have move into the new places, level, and keep their
words.
- The canvas size, and where it comes from: **picture** when a picture is on
`background`, **wired** when width and height are wired.
- **Clear** removes every box.
- **⚙** (the gear): **Grid size**, the pixels from one grid line to the next:
Auto (32 pixels, more on a very big canvas), 8, 16, 32 or 64. The node keeps
it.
## Inputs
| Input | What it does |
|---|---|
| `background` | A picture to draw on. It shows under the boxes, and the canvas takes its size and shape, also when `width` and `height` are wired: the boxes are drawn on the picture, where the `mask` is made. The Canvas row dims while a picture is wired. |
| `bboxes` | Boxes to start from: bounding boxes, prompt elements (such as the `elements` of another Layout Boxes), or either as JSON text. New boxes on this wire fill the canvas when the workflow runs. Your changes on the canvas then stay until the boxes on the wire change. After **Clear**, the next run fills the canvas from the wire again. |
| `width`, `height` | The canvas size in pixels. Wired from Latent Size, the canvas takes that size and shape at once. |
## Outputs
| Output | Use |
|---|---|
| `preview` | A picture of the boxes with their numbers, words and colors, to check the layout. Turned boxes are drawn turned. |
| `bboxes` | The boxes in pixels of the canvas (x, y, width, height), for nodes that take bounding boxes. A turned box gives the straight box around it. |
| `elements` | The boxes as prompt elements. Wire it to MOSS-Image Layout Prompt. A turned box gives the straight box around it, and a turned text box says its slant in its description. |
| `mask` | White inside every box, black elsewhere, to change only those places of a picture. A turned box is white inside its turned shape only. |
## In place of Create Bounding Boxes
Layout Boxes has the same inputs and the same outputs in the same order (and
`mask` after them), so the wires move straight over. For the same boxes it
gives the same `bboxes` and `elements`, and MOSS-Image Layout Prompt makes the
same prompt from them.
The boxes drawn on a Create Bounding Boxes do not move over by themselves. To
take them along, wire that node's `elements` into `bboxes` of Layout Boxes and
run once: they fill the canvas. Then remove the old node.
## Technical details
- The boxes are kept on the node as text, in the hidden `layout` input. Each
box's place is kept as parts of the canvas from 0 to 1, so a new canvas size
keeps every box at the same place. An API prompt can set it:
`{"v": 1, "boxes": [{"x": 0.25, "y": 0.1, "w": 0.5, "h": 0.8, "type": "obj", "text": "", "desc": "a cat", "palette": ["#ff3399"]}]}`.
A list of pixel boxes in Create Bounding Boxes' own form is read as well.
- A turned box also has `"angle"`: degrees, clockwise on the screen, from -90
to 90, about the box's centre. `x`, `y`, `w` and `h` stay the box as it was
drawn, before the turn. Only turned boxes carry the key, and a box without
it is level, so layouts saved before boxes could turn give exactly what
they gave. The turn is made in pixels of the canvas the box was drawn on
(the picture on `background`, when there is one), so a turned box is a true
rectangle there whatever the canvas's shape.
- For a turned box, `mask` is white at every pixel whose centre is inside the
turned rectangle. `bboxes` and `elements` hold the straight box around it,
cut to the canvas: those types only know straight boxes. A text box turned
2 degrees or more gets "tilted about N degrees, running downhill to the
right" (clockwise) or "running uphill to the right" added to its
description in `elements`, after a comma, or as its own sentence when the
description ends with a full stop; not when the description already says
it. Object boxes get no such words.
- `elements` holds one element per box, in order:
`{"type": "obj" or "text", "bbox": [ymin, xmin, ymax, xmax], "text", "desc", "color_palette"}`.
The place is on a 0 to 1000 grid over the canvas, `text` is there for text
boxes only, and `color_palette` holds up to five colors when the box has any.
- `bboxes` is a list holding one list of boxes, the form ComfyUI's
bounding-box nodes pass on. With no boxes it is empty.
- `mask` is one mask (1 x height x width): 1 inside every box, 0 elsewhere,
with hard edges. With a picture on `background` it has the picture's own
size, and each box covers the same part of the picture that it covers on
the canvas. Without a picture it has the canvas size and covers the same
pixels as `bboxes`. With no boxes it is all 0.
- With a picture on `background`, the canvas on the node is the picture at
its own size, so the arrow keys move a box one pixel of the picture.
`width` and `height` still set the pixels `bboxes` counts in and the size of
`preview`.
- The picture shows before a run when `background` comes from a Load Image,
straight or through Reroutes. Through a node that changes the picture
first, such as a resize, it shows after a run.
- Before a run, the canvas can only know a wired size it can read from the
node it comes from: Latent Size, Integer or a Primitive. From any other
node, the canvas keeps its own size until a run, and then shows the size
the run used. Either way the boxes keep their place on the size that
arrives.
- Like Create Bounding Boxes, it is an output node: it runs with the workflow
even when nothing is wired to its outputs, so new boxes on the `bboxes` wire
reach the canvas.
- The background is saved, shrunk, in ComfyUI's temp folder for the node's
face. Nothing else is written.
-54
View File
@@ -1,54 +0,0 @@
# LM Studio Chat
Sends a prompt — and optionally an image — to a local LM Studio server and
returns the reply as text. Anything that speaks the OpenAI-compatible chat
API works too: Ollama's compatibility mode, llama.cpp's server, a remote
box. Typical jobs: expanding a rough idea into a full prompt, captioning an
image for img2img, or rewriting text mid-workflow.
## Controls
- **prompt**: What to ask. Required unless an image is connected — an image
with an empty prompt asks for a plain description.
- **system_prompt** (optional): Role and rules, e.g. *"You write
single-paragraph Stable Diffusion prompts. No preamble, no quotes."*
Empty sends no system message.
- **endpoint**: The server's base URL — LM Studio shows it on the Developer
tab, default `http://127.0.0.1:1234/v1`. A bare `host:port` or a full
`/v1` path both work.
- **model**: Model identifier as LM Studio lists it. **Empty uses whatever
the server has loaded**, which is the everyday case — no fetched dropdown
to go stale when LM Studio was not running at ComfyUI startup.
- **temperature**: 0 is near-deterministic, 0.7 balanced. Captioning likes
0.2–0.5.
- **max_tokens**: Longest allowed reply; `-1` leaves it to the server.
Reasoning models spend tokens thinking before answering — give them room.
- **seed**: Sent to the server, and either way a changed seed re-runs the
node instead of replaying the cached reply. This is the re-roll knob.
- **timeout_seconds**: How long to wait. Cold-loading a big model or a long
think can need minutes.
- **image_max_edge**: Downscale a connected image so its longest edge fits
this before sending (`0` = full size). Vision models resize internally
anyway, and a smaller upload prefills much faster.
- **image** (optional): For vision models. Only the **first frame** of a
batch is sent — pick one with Select Frame 🆎 for video. A
text-only model will refuse an image server-side.
## Outputs
- **text**: The reply with any `<think>` blocks removed — safe to wire
straight into a text encoder.
- **thinking**: What a reasoning model thought before answering; empty for
ordinary models. Handy in a preview node while tuning prompts.
## Behavior
- Runs off the executor thread, so the UI stays responsive during a long
generation; the timeout is the backstop.
- Caches like any node: identical inputs replay the saved reply instantly.
Change the seed to force a fresh generation.
- Errors are actionable: a refused connection says how to start the server,
an HTTP error carries the server's own message (e.g. *"Model not
found"*), and a non-JSON reply points at the endpoint setting.
Nothing is sent anywhere except the endpoint you configured.

Some files were not shown because too many files have changed in this diff Show More