Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d759ca1cea |
@@ -68,7 +68,7 @@ Rotate, crop and pad a picture by dragging handles on the node. Cyan squares cro
|
||||
|
||||
Tap a ratio under the preview 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 untransformed `original`, the output `width` and `height`, and a `prompt_image`. Wire the stitcher into **Stitch Inpaint** after the sampler to put your original back. By default, 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.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -78,9 +78,9 @@ Krea 2 Rotate + Outpaint turns a photo, pads it and paints the new area in one p
|
||||
|
||||
### 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 skips resizing, though the canvas multiple can still trim a few pixels; anything higher resizes the picture before padding. A picture wired into `source_image` is padded instead of the chosen file.
|
||||
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 an unpadded `reference` for the model, fitted to a multiple of 16 and shrunk if needed. Wire the stitcher into **Stitch Inpaint** after the sampler, and everything outside the seam stays your original picture.
|
||||
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)
|
||||
|
||||
@@ -100,7 +100,7 @@ An optional mask is resized the same way. It returns `image`, `mask`, `width` an
|
||||
|
||||
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 resized the plain way to the finished size), `width`, `height` and a `report` that says what was done.
|
||||
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
|
||||
|
||||
@@ -168,13 +168,13 @@ Picks one frame from a video, then gives you the same rotate, crop and pad contr
|
||||
|
||||
### 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 Length control, 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.
|
||||
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 sit on the node face, 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.
|
||||
|
||||
**Length** sets a number of output frames from IN. Drag IN or the small middle grip to move the window; drag OUT or type the count to change its length. The count stays when you choose a new clip, but the source can end before it is reached. For an exact count, link `fixed_frames`: 120 frames at 24 fps is five seconds. A positive value overrides OUT/`end_frame`, Length, `frame_load_cap` and Frames for; 0 turns it off. The source must be long enough, and IN can move earlier near the end so the window fits.
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
@@ -307,7 +307,7 @@ It returns `width`, `height` and an empty image latent. The gear picks the laten
|
||||
|
||||
### 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 shares the `background`, `bboxes`, `width` and `height` inputs with ComfyUI's own Create Bounding Boxes. For the same boxes, its first three outputs match, with `mask` added after them. It saves its boxes in its own `layout` text, and its canvas also works in the classic node look.
|
||||
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.
|
||||
|
||||
@@ -321,7 +321,7 @@ One seed for several samplers. **Random**, **Fixed** and **Step** set what happe
|
||||
|
||||
### 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 keeps its playback speed. Offset removes the start, and rounding to whole frames can change the duration.
|
||||
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
|
||||
|
||||
@@ -341,7 +341,7 @@ Type what happens, one line for each moment, and leave a blank line to cut to a
|
||||
|
||||
### Integer
|
||||
|
||||
One whole number (`INT`) on a scrub control, for sharing a size, count or other whole number across nodes. It takes a typed sum: `1024*1.5`.
|
||||
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
|
||||
|
||||
@@ -437,7 +437,7 @@ People who download your workflow get the nodes through ComfyUI-Manager's missin
|
||||
- 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 and show the first and last frames the run keeps.
|
||||
- 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.
|
||||
@@ -446,7 +446,7 @@ The video transform nodes read a video you pick from a list of ComfyUI's input f
|
||||
|
||||
## Optional extras
|
||||
|
||||
These optional 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.
|
||||
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
|
||||
|
||||
@@ -17,7 +17,8 @@ reduce color drift between frames.
|
||||
**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: adjusts overall casts and contrast. It can also change hues.
|
||||
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.
|
||||
|
||||
@@ -110,7 +110,7 @@ parts back.
|
||||
|
||||
## Technical details
|
||||
|
||||
- Starting values: Context `1.2`, Blend `16` px, Multiple `32`; Grow, Blur,
|
||||
- Starting values: Context `1.2`, Blend `16` px, Multiple `8`; 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
|
||||
|
||||
@@ -22,8 +22,7 @@ Wired to a sampler's **denoise**, it goes from 0 to 1 and no further: type
|
||||
|
||||
- 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 uses its own range again: -1,000,000,000,000,000 to
|
||||
1,000,000,000,000,000, with a stored step of `0.001`.
|
||||
- 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.
|
||||
|
||||
@@ -77,9 +77,8 @@ Some scripts, with the jobs they need:
|
||||
```
|
||||
|
||||
A picture that is off simply isn't counted. If a line still names it, the node
|
||||
stops and tells you which one. A plain script needs at least one picture or
|
||||
a video to start from. A prompt already written in H3's six parts can run
|
||||
without either.
|
||||
stops and tells you which one. It needs at least one picture or a video to start
|
||||
from.
|
||||
|
||||
## Use a video
|
||||
|
||||
@@ -97,9 +96,7 @@ Connect a video, then pick what it is for:
|
||||
- **new voice**: the same clip with a new voice. Connect the voice, and type
|
||||
what they say in quotes.
|
||||
|
||||
Swap and new voice ignore **Length**. They use the video's frame count,
|
||||
treated as 24 fps, capped at 15 seconds and rounded down to a length H3 takes.
|
||||
This can shorten the requested result; the video output itself stays whole.
|
||||
Swap and new voice ignore **Length** and keep the video's own length.
|
||||
|
||||
## What you can type
|
||||
|
||||
@@ -122,13 +119,12 @@ This can shorten the requested result; the video output itself stays whole.
|
||||
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 the story is kept.
|
||||
Spacing is cleaned up. A connected voice or song still updates the audio
|
||||
parts. The Sound and Music boxes do not add to this form of prompt.
|
||||
Already wrote H3's six parts yourself? Paste them in and they go through as
|
||||
they are.
|
||||
|
||||
## What you can plug in
|
||||
|
||||
- **picture_1**, **picture_2**, **picture_3**, **picture_4**: your pictures.
|
||||
- **picture_1** to **picture_4**: your pictures.
|
||||
- **video** and **video_sound**: the frames and the sound of a video. Only keep
|
||||
going uses the sound.
|
||||
- **voice**: a clip of someone talking, about 10 seconds, plus words in quotes
|
||||
@@ -163,16 +159,13 @@ as not plugged in.
|
||||
- **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`): requested seconds, `5` to `15`, starting at `8`.
|
||||
The `frames` output rounds this to a length H3 takes at 24 fps.
|
||||
- **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`, `job_2`, `job_3`, `job_4`): what
|
||||
each picture is for, on the picture chips under the bar. An empty slot
|
||||
shows as a faint `+ @3`.
|
||||
The first three start on `person`; the fourth starts on `thing`.
|
||||
- **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.
|
||||
@@ -218,16 +211,6 @@ as not plugged in.
|
||||
|
||||
## Technical details
|
||||
|
||||
H3 lengths use `17n + 5` frames, with `n` at least 1. A requested length
|
||||
rounds to the nearest such count at 24 fps: `5` seconds gives `124` frames,
|
||||
and `15` gives `362`. Swap and new voice round down instead, after capping
|
||||
the source count at `360`, so their largest output length is `345` frames.
|
||||
This node has no input for the source video's fps; change its rate to
|
||||
24 fps first when its timing must match.
|
||||
|
||||
With a six-part prompt and no pictures or video, `width` and `height` are
|
||||
`1344` and `756`. Unused picture, video and audio outputs are empty.
|
||||
|
||||
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
|
||||
|
||||
@@ -266,30 +266,6 @@ The editor's right side holds the stitcher's settings, the same as on the clip n
|
||||
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.
|
||||
|
||||
## Transform values
|
||||
|
||||
The handles and editor write these inputs. An API prompt can set them too.
|
||||
|
||||
| Input | Default | What it sets |
|
||||
| --- | --- | --- |
|
||||
| `rotation_degrees` | `0` | Clockwise turn, from `-180` to `180` degrees. |
|
||||
| `crop_aspect_ratio` | `free` | Crop shape: `free`, `source`, or a width:height ratio. |
|
||||
| `crop_x`, `crop_y` | `0` | Crop's left and top, in pixels of the turned picture; `0` to `65536`. |
|
||||
| `crop_width`, `crop_height` | `0` | Crop size, `0` to `65536`; `0` keeps the available size from that position. |
|
||||
| `pad_left`, `pad_top`, `pad_right`, `pad_bottom` | `0` | Added pixels on each side, `0` to `32768`. |
|
||||
| `feather` | `24` | Mask feather in pixels, `0` to `4096`. The picture meets the fill at a hard edge. |
|
||||
| `canvas_multiple` | `1` | Size multiple, `1` to `4096`; `1` turns rounding off. |
|
||||
| `fill_color` | `#808080` | Fill for padding and empty corners. |
|
||||
| `resize_to_megapixels` | `false` | Turn the output resize on. |
|
||||
| `megapixels` | `1` | Resize budget, `0.01` to `16`; one megapixel here is 1024 × 1024 pixels. |
|
||||
| `resize_method` | `lanczos` | Resize filter: `lanczos`, `area`, `bicubic`, `bilinear`, `nearest-exact`. |
|
||||
| `resolution_steps` | `1` | Round resized sides to this multiple, `1` to `256`. |
|
||||
|
||||
The built-in crop ratios are `1:1`, `9:16`, `16:9`, `2:3`, `3:2`,
|
||||
`3:4`, `4:3`, `9:21` and `21:9`. Other positive whole-number ratios
|
||||
are accepted too. The resize runs after the transform; its step can change
|
||||
a size that already met `canvas_multiple`.
|
||||
|
||||
## Technical details
|
||||
|
||||
- **Saved values.** The handles write plain number values that save with the
|
||||
|
||||
@@ -114,9 +114,8 @@ the pictures repeat in order for as many runs as you queue.
|
||||
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`). Folder separators become
|
||||
underscores, so names can still collide: `trips/beach.png` and
|
||||
`trips_beach.jpg` both give `trips_beach`.
|
||||
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
|
||||
|
||||
@@ -6,8 +6,8 @@ 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. Its range is listed
|
||||
below; use Seed for the full sampler seed range.
|
||||
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
|
||||
@@ -22,7 +22,7 @@ Wired to a sampler's **steps**, it goes from 1 to 10000: type 0 and you get
|
||||
|
||||
- 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 uses its own whole-number range again.
|
||||
- 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.
|
||||
@@ -42,9 +42,9 @@ For arithmetic on the way — halving a size, adding a margin — feed this into
|
||||
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 from -9,007,199,254,740,991 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.
|
||||
- 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
|
||||
|
||||
@@ -152,9 +152,8 @@ removes it. The first color also colors the box on the canvas.
|
||||
|
||||
## In place of Create Bounding Boxes
|
||||
|
||||
Layout Boxes shares the `background`, `bboxes`, `width` and `height` inputs.
|
||||
Its first three outputs match Create Bounding Boxes, with `mask` after them,
|
||||
so those wires move straight over. For the same boxes it
|
||||
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.
|
||||
|
||||
@@ -162,9 +161,6 @@ 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.
|
||||
|
||||
The saved editor values differ. Layout Boxes uses its own `layout` text;
|
||||
it does not take Create Bounding Boxes' `editor_state` or `last_incoming`.
|
||||
|
||||
## Technical details
|
||||
|
||||
- The boxes are kept on the node as text, in the hidden `layout` input. Each
|
||||
|
||||
@@ -26,9 +26,9 @@ padding value unchanged.
|
||||
- **Reset padding** appears in the top-right corner while there is padding
|
||||
to clear; it sets every side a link does not drive back to 0.
|
||||
- Clicks on empty canvas space fall through, so the node still drags.
|
||||
- The hidden `pad_left`, `pad_top`, `pad_right` and `pad_bottom` widgets
|
||||
hold the real values. The canvas sets them, so undo, save/load and the API
|
||||
format all see plain INT widgets.
|
||||
- The hidden `pad_left/top/right/bottom` widgets hold the real values — the
|
||||
canvas is their remote control, so undo, save/load, and the API format all
|
||||
see plain INT widgets.
|
||||
|
||||
## Guarantees
|
||||
|
||||
@@ -41,8 +41,6 @@ padding value unchanged.
|
||||
|
||||
## Controls
|
||||
|
||||
- **Exact padding** (`pad_left`, `pad_top`, `pad_right`, `pad_bottom`):
|
||||
Pixels to add on each side. Each starts at `0` and accepts `0` to `16384`.
|
||||
- **Source** (`image`): Choose or upload from ComfyUI's input folder; the list previews the image under the pointer and filters as you type. A picture ComfyUI made, named like `photo.png [output]`, loads too, as it does in core Load Image.
|
||||
- **source_image** (optional socket): Wire an image from another node here
|
||||
and it is padded instead of the file; the Source row dims while the wire
|
||||
@@ -52,8 +50,7 @@ padding value unchanged.
|
||||
loader that feeds one image per run pads a whole folder the same way.
|
||||
- **Fill** / **Color** / **Backdrop** (`mode` / `fill_color` /
|
||||
`backdrop_blur`): Five fills — `color`, `edge`, `edge pixel`,
|
||||
`pillarbox blur`, `mirror`. The defaults are `color`, `#808080` and
|
||||
blur `0.5`. **Color** shows for `color`, **Backdrop** for
|
||||
`pillarbox blur`, `mirror`. **Color** shows for `color`, **Backdrop** for
|
||||
`pillarbox blur`. `mirror` reflects the picture outward at every edge (the
|
||||
edge pixel included, and on past the picture's own size), so the padding
|
||||
reads as more of the scene: the margin to give an edit model room to
|
||||
|
||||
@@ -272,8 +272,10 @@ data.
|
||||
The run side matches: a stale saved path resolves by name at load time
|
||||
(exact → unique case-insensitive path → unique basename, with a one-line
|
||||
console note when it remaps). A LoRA that is genuinely missing **stops the
|
||||
run before anything is sampled**, and the error names the file. The error
|
||||
also says where the switch is.
|
||||
run before anything is sampled**, and the error names the file — a picture
|
||||
that rendered without its LoRA is worse than no picture, above all for a
|
||||
script or an agent that would happily judge it. The error also says where
|
||||
the switch is.
|
||||
|
||||
Gear menu → **Stop on missing LoRA** (on by default) turns that off for this
|
||||
node, and for LoRA Loaders you add later in this browser; nodes already saved
|
||||
@@ -343,8 +345,8 @@ switched on for the old one.
|
||||
- **Default strength** (starts at 1): the strength a newly added LoRA gets.
|
||||
- **Strength step** (starts at 0.05): how far one drag step or arrow click
|
||||
moves a strength. Shift always steps by 0.01.
|
||||
- **Trigger word separator** (`trigger_separator`): what joins the
|
||||
`triggers` output. It starts as a comma and a space.
|
||||
- **Trigger word separator** (a comma and a space to start): what joins the
|
||||
`triggers` output.
|
||||
- **Hide file extension** and **Hide folder names** (both on): how names
|
||||
show on rows. The full path stays in the tooltip.
|
||||
|
||||
@@ -354,18 +356,9 @@ switched on for the old one.
|
||||
- **clip**: The CLIP with every enabled LoRA applied (empty when no CLIP
|
||||
is connected).
|
||||
- **triggers**: The selected trigger words from enabled rows, deduplicated,
|
||||
joined with the gear menu's separator (a comma and a space by default).
|
||||
joined with the gear menu's separator (a comma by default).
|
||||
|
||||
Rows with a strength of `0` (both model and CLIP) are skipped at load time
|
||||
but still contribute their trigger words, so you can park a LoRA at zero
|
||||
while comparing. A row skipped because its file is missing contributes none:
|
||||
that LoRA is not in the picture.
|
||||
|
||||
## API inputs
|
||||
|
||||
- **model**: the model to patch.
|
||||
- **loras**: the stack as JSON text, empty (`[]`) by default. Each row can
|
||||
hold `name`, `enabled`, `strength`, `strength_clip` and `triggers`.
|
||||
- **clip** (optional): the text encoder to patch. Without it, CLIP strengths
|
||||
are ignored and the `clip` output is empty.
|
||||
- **trigger_separator** and **on_missing**: the settings described above.
|
||||
|
||||
@@ -39,27 +39,23 @@ names from here on; how it measures may still improve.
|
||||
- **edited**: the edited picture.
|
||||
- **source**: the original picture you edited, at the size the edit model
|
||||
saw it (in the Qwen Image 2.1 Edit + Realign example, the Image Resize
|
||||
output). The result comes out at this size. If you wire the padded canvas
|
||||
itself here with its stitcher, the result is just the source rectangle,
|
||||
without the padding. For a batch or a video, give
|
||||
output). The result comes out at this size. For a batch or a video, give
|
||||
one source picture, or one per edited frame.
|
||||
- **stitcher** (optional): from Load Image + Pad 🆎 when you padded the
|
||||
picture before editing. Image Crop + Rotate + Pad 🆎 works too. A Crop
|
||||
For Inpaint 🆎 stitcher does not work here.
|
||||
- **Fit** (`fit`): `zoom + shift` (the default) fixes the usual Qwen drift.
|
||||
- **Fit**: `zoom + shift` (the default) fixes the usual Qwen drift.
|
||||
`affine` also fixes a slight tilt.
|
||||
- **Empty fill** (`empty_fill`): when the edit slid past the edge, a thin
|
||||
strip ends up with nothing in it. `edge` (the default) stretches the nearest pixels
|
||||
- **Empty fill**: when the edit slid past the edge, a thin strip ends up
|
||||
with nothing in it. `edge` (the default) stretches the nearest pixels
|
||||
into it, `source` fills it from the original, `gray` fills it flat gray
|
||||
for inpainting. With a big enough margin there is no strip.
|
||||
- **Max zoom** (`max_zoom`): the biggest zoom it will undo, in percent
|
||||
(`1` to `50`, `20` by default).
|
||||
- **Max zoom**: the biggest zoom it will undo, in percent (20 by default).
|
||||
Anything bigger is treated as a change you meant and left alone.
|
||||
|
||||
## Outputs
|
||||
|
||||
- **image**: the edit, lined up with the original and at its size, or just
|
||||
the source rectangle when a padded canvas and its stitcher are wired in.
|
||||
- **image**: the edit, lined up with the original and at its size.
|
||||
- **empty_mask**: white where the lined-up edit has nothing (a strip that
|
||||
slid off the edge), black everywhere else. Crop the strip off or inpaint
|
||||
it.
|
||||
|
||||
@@ -65,23 +65,21 @@ file's name. A caption on the `caption_text` input writes the paired `.txt`.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **images** (IMAGE): the batch to save. Each image becomes a separate file.
|
||||
- **filename** (STRING, link only): the exact name from upstream - a Load
|
||||
Image name, a caption tool's id. Its image extension is swapped for the
|
||||
chosen format's (`photo123.jpg` -> `photo123.png`); a batch appends
|
||||
`_001`, `_002`... A linked but empty value stops the run instead of saving
|
||||
under a made-up name.
|
||||
- **caption_text** (STRING, link only): saved as a UTF-8 `.txt` beside every
|
||||
image with the same name (`portrait.png` -> `portrait.txt`). A non-blank
|
||||
linked caption wins. If it is blank or unwired, the old `caption` box is
|
||||
used instead. No sidecar is written when both are blank.
|
||||
image with the same name (`portrait.png` -> `portrait.txt`). Empty writes
|
||||
no sidecar. The old `caption` box still works underneath; a linked caption
|
||||
wins.
|
||||
|
||||
Rooted paths and `..` in names typed on the node are rejected before the
|
||||
run. A bad name arriving through the `filename` link stops the run at this
|
||||
node, before any file is written. `on_existing`
|
||||
from older workflows is still honored wherever a save can land on an
|
||||
existing file (counter off, exact or linked names). Its choices are
|
||||
`overwrite` (the default), `skip` and `error`.
|
||||
existing file (counter off, exact or linked names).
|
||||
|
||||
## Name tokens
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@ before an expensive stage, or thin a long sweep down to samples.
|
||||
## Outputs
|
||||
|
||||
- **images**: The kept frames, in their original order.
|
||||
- **fps**: The wired rate divided by `nth`, to keep playback speed;
|
||||
`0` when no fps is wired. Offset removes the start, and rounding to whole
|
||||
frames can change the duration. Five frames thinned by `2` give three
|
||||
frames at half the rate, which lasts slightly longer.
|
||||
- **fps**: The wired rate divided by `nth`, so the kept frames still span
|
||||
the same time; `0` when no fps is wired.
|
||||
|
||||
An offset at or past the end of the batch stops with the batch's size
|
||||
instead of returning an empty batch, which no downstream node could use.
|
||||
|
||||
@@ -8,17 +8,11 @@ Wire it like this: **Tiled Upscale 🆎** → your encode, sampler and decode
|
||||
→ **Tiled Upscale Stitch 🆎**. Plug `stitcher` straight across. The nodes
|
||||
in between run once for each tile by themselves.
|
||||
|
||||
## Input
|
||||
|
||||
- **image**: The picture or batch to cut into tiles. Every picture in a batch
|
||||
uses the same tile positions.
|
||||
|
||||
## Megapixels
|
||||
|
||||
**Megapixels** (`megapixels`) is the size of the finished picture. It starts
|
||||
at `4` and accepts `0` to `64`. It moves in half steps, and you can type a
|
||||
value between those limits. `0` keeps the picture's own size, for a light
|
||||
pass over a picture that is already big.
|
||||
**Megapixels** (`megapixels`) is the size of the finished picture. It moves
|
||||
in half steps, and you can type any number. `0` keeps the picture's own
|
||||
size, for a light pass over a picture that is already big.
|
||||
|
||||
- A picture that fits in one tile stays whole: one pass, no joins.
|
||||
- A bigger one is cut into the fewest tiles that fit, as square as they
|
||||
|
||||
@@ -28,11 +28,6 @@ picture before the tiles are joined.
|
||||
Grain and color noise in an old photo do not throw it off, and a faded
|
||||
photo comes back faded.
|
||||
|
||||
## Output
|
||||
|
||||
- **image**: The finished RGB picture at the requested size. The output is
|
||||
a list, with one batch for each stitcher supplied.
|
||||
|
||||
## Technical details
|
||||
|
||||
- Neighbouring tiles are blended across the strip they share, fading from
|
||||
|
||||
@@ -31,30 +31,6 @@ generated-area `mask`, a `stitcher` for Stitch Inpaint, the selected
|
||||
|
||||
The editor has the same rail full width, plus first/last, ±1, ±25, ±50, ±100, play/pause, keyboard arrows (one frame, 10 with Shift), Home/End and Space to play or pause. **Save & close** keeps your edits; **Cancel** or Escape puts everything back, and asks first if anything changed. Only one frame preview request remains active; stale requests are cancelled, and frames already shown are served from a small cache.
|
||||
|
||||
## Transform values
|
||||
|
||||
The handles and editor write these inputs. An API prompt can set them too.
|
||||
|
||||
| Input | Default | What it sets |
|
||||
| --- | --- | --- |
|
||||
| `rotation_degrees` | `0` | Clockwise turn, from `-180` to `180` degrees. |
|
||||
| `crop_aspect_ratio` | `free` | Crop shape: `free`, `source`, or a width:height ratio. |
|
||||
| `crop_x`, `crop_y` | `0` | Crop's left and top, in pixels of the turned picture; `0` to `65536`. |
|
||||
| `crop_width`, `crop_height` | `0` | Crop size, `0` to `65536`; `0` keeps the available size from that position. |
|
||||
| `pad_left`, `pad_top`, `pad_right`, `pad_bottom` | `0` | Added pixels on each side, `0` to `32768`. |
|
||||
| `feather` | `24` | Mask feather in pixels, `0` to `4096`. The picture meets the fill at a hard edge. |
|
||||
| `canvas_multiple` | `1` | Size multiple, `1` to `4096`; `1` turns rounding off. |
|
||||
| `fill_color` | `#808080` | Fill for padding and empty corners. |
|
||||
| `resize_to_megapixels` | `false` | Turn the output resize on. |
|
||||
| `megapixels` | `1` | Resize budget, `0.01` to `16`; one megapixel here is 1024 × 1024 pixels. |
|
||||
| `resize_method` | `lanczos` | Resize filter: `lanczos`, `area`, `bicubic`, `bilinear`, `nearest-exact`. |
|
||||
| `resolution_steps` | `1` | Round resized sides to this multiple, `1` to `256`. |
|
||||
|
||||
The built-in crop ratios are `1:1`, `9:16`, `16:9`, `2:3`, `3:2`,
|
||||
`3:4`, `4:3`, `9:21` and `21:9`. Other positive whole-number ratios
|
||||
are accepted too. The resize runs after the transform; its step can change
|
||||
a size that already met `canvas_multiple`.
|
||||
|
||||
## Transform and outputs
|
||||
|
||||
Rotation, crop, padding, feathering, fill, **Divisible by**, the megapixel resize, gestures, `image`, and `mask` match the image node. The output batch contains one frame.
|
||||
|
||||
@@ -114,30 +114,6 @@ padding, keeping rotation and resize settings.
|
||||
Pixel rounding can differ by one pixel between opposite bands. **Divisible by** and
|
||||
the resize **Step** can slightly change the final aspect ratio.
|
||||
|
||||
## Transform values
|
||||
|
||||
The handles and editor write these inputs. An API prompt can set them too.
|
||||
|
||||
| Input | Default | What it sets |
|
||||
| --- | --- | --- |
|
||||
| `rotation_degrees` | `0` | Clockwise turn, from `-180` to `180` degrees. |
|
||||
| `crop_aspect_ratio` | `free` | Crop shape: `free`, `source`, or a width:height ratio. |
|
||||
| `crop_x`, `crop_y` | `0` | Crop's left and top, in pixels of the turned picture; `0` to `65536`. |
|
||||
| `crop_width`, `crop_height` | `0` | Crop size, `0` to `65536`; `0` keeps the available size from that position. |
|
||||
| `pad_left`, `pad_top`, `pad_right`, `pad_bottom` | `0` | Added pixels on each side, `0` to `32768`. |
|
||||
| `feather` | `0` | Mask feather in pixels, `0` to `4096`. The picture meets the fill at a hard edge. |
|
||||
| `canvas_multiple` | `1` | Size multiple, `1` to `4096`; `1` turns rounding off. |
|
||||
| `fill_color` | `#000000` | Fill for padding and empty corners. |
|
||||
| `resize_to_megapixels` | `false` | Turn the output resize on. |
|
||||
| `megapixels` | `1` | Resize budget, `0.01` to `16`; one megapixel here is 1024 × 1024 pixels. |
|
||||
| `resize_method` | `lanczos` | Resize filter: `lanczos`, `area`, `bicubic`, `bilinear`, `nearest-exact`. |
|
||||
| `resolution_steps` | `1` | Round resized sides to this multiple, `1` to `256`. |
|
||||
|
||||
The built-in crop ratios are `1:1`, `9:16`, `16:9`, `2:3`, `3:2`,
|
||||
`3:4`, `4:3`, `9:21` and `21:9`. Other positive whole-number ratios
|
||||
are accepted too. The resize runs after the transform; its step can change
|
||||
a size that already met `canvas_multiple`.
|
||||
|
||||
## Transform and outputs
|
||||
|
||||
Rotation, crop, padding, feathering, fill, **Divisible by**, and the editor's handles
|
||||
@@ -177,25 +153,16 @@ The node performs no remote network requests and does not rewrite the source vid
|
||||
|
||||
## Connected timing inputs
|
||||
|
||||
Six optional timing inputs can take links above the editor:
|
||||
Five optional sockets sit on the left, above the editor:
|
||||
|
||||
| Input | Meaning |
|
||||
| --- | --- |
|
||||
| `force_rate` | Sample at this fps before Every nth. 0 keeps the source rate; the allowed range is 0 to 1000 fps. Drops or repeats frames to keep playback speed. Accepts FLOAT or INT. |
|
||||
| `force_rate` | Sample at this fps before Every nth. 0 keeps the source rate. Drops or repeats frames to keep playback speed. Accepts FLOAT or INT. |
|
||||
| `start_frame` | Zero-based source start frame. Overrides timeline IN. |
|
||||
| `end_frame` | Exclusive source end frame. 0 means the end of the source. Overrides timeline OUT. |
|
||||
| `fixed_frames` | Exact output frames, 0 to 100000; 0 turns it off. A positive value overrides OUT/end_frame, Length, frame_load_cap and Frames for. The source must be long enough. Near the end, IN can move earlier so the window fits. |
|
||||
| `fixed_frames` | Exact output frames from IN; the source must be long enough. Connected, it sets the length: OUT, Length and Frames for step aside. |
|
||||
| `frame_load_cap` | Maximum frames after rate conversion and Every nth, before Frames for. 0 means unlimited. Connected, it sets the length in place of Length. |
|
||||
| `last_seconds` | Load only this many seconds from the end of the clip, or of the part before OUT, without moving before IN. 0 keeps the current window. Use it to carry on from a clip: a long clip then costs no more memory than a short one. |
|
||||
|
||||
`seek_mode` (`frame index` by default, or `time seconds`), `frame_index`
|
||||
and `frame_time` set only the preview position. Both position values start
|
||||
at `0`; neither trims the output.
|
||||
|
||||
Fresh timing settings are IN/OUT `0`, Every nth `1`, Length (`max_frames`)
|
||||
`0` and Frames for (`frame_snap`) `free`. Every nth accepts `1` to `512`;
|
||||
Length accepts `0` to `100000`. The API choices for Frames for are `free`,
|
||||
`8n+1` and `4n+1`; they round the kept count down.
|
||||
| `last_seconds` | Load only this many seconds from the end of the clip, or of the part before OUT. 0 loads all of it. Use it to carry on from a clip: a long clip then costs no more memory than a short one. |
|
||||
|
||||
Frame bounds use the source's frame-rate grid, independent of `force_rate`.
|
||||
The footer labels **Source fps** separately from **Output fps**. A direct
|
||||
|
||||
@@ -11,8 +11,4 @@ be there. For anything new, use
|
||||
[LaMa Inpaint 🆎](AUSBOSS_NODES_LaMaInpaint.md) instead: the same engine
|
||||
with the full set of controls and live previews.
|
||||
|
||||
## Output
|
||||
|
||||
- **image**: The inpainted image batch. Its size and unmasked pixels are kept.
|
||||
|
||||
This id will keep working; it just will not grow new features.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# AusBoss nodes: a workflow guide
|
||||
# AusBoss nodes: a guide for AI agents
|
||||
|
||||
Find the job in the table below, then read that node's help page before
|
||||
wiring it. Each help page is `js/docs/<node id>.md` in this pack. ComfyUI
|
||||
shows the same page in its node help panel. This guide ships with the pack.
|
||||
Give this page to an AI agent and it can pick the right AusBoss node for a job,
|
||||
wire it into a workflow, and find the help for every input. It ships with the
|
||||
pack, so it is also in your ComfyUI install, under
|
||||
`custom_nodes/<this pack>/js/docs/agents/`.
|
||||
|
||||
**If you are the agent:** find the job in the table below, then read that
|
||||
node's help page before you wire it. Each help page is
|
||||
`js/docs/<node id>.md` in this pack. ComfyUI shows the same page in its node
|
||||
help panel.
|
||||
|
||||
## Which node for which job
|
||||
|
||||
@@ -29,14 +35,13 @@ shows the same page in its node help panel. This guide ships with the pack.
|
||||
| Load or save a video | Load Video (`AUSBOSS_NODES_LoadVideo`), Save Video (`AUSBOSS_NODES_SaveVideo`) |
|
||||
| Turn, crop and pad a video: one frame, or the whole clip | Video Crop + Rotate + Pad → Frame (`AUSBOSS_NODES_VideoCropRotatePad`), → Clip (`AUSBOSS_NODES_VideoCropRotatePadClip`) |
|
||||
| Change a clip's frame rate | Frame Interpolate (`AUSBOSS_NODES_FrameInterpolate`) |
|
||||
| Work with frame batches | Select Frame (`AUSBOSS_NODES_SelectFrame`), Select Every Nth (`AUSBOSS_NODES_SelectEveryNth`), Split Batch (`AUSBOSS_NODES_SplitBatch`), Merge Batches (`AUSBOSS_NODES_MergeBatches`) |
|
||||
| One shared seed, number or text | Seed (`AUSBOSS_NODES_Seed`), Integer (`AUSBOSS_NODES_Integer`), Float (`AUSBOSS_NODES_Float`), Text (`AUSBOSS_NODES_Text`) |
|
||||
| Write a MiniMax H3 or Pika H3 Flex prompt from a script and references | H3 Prompt Writer (`AUSBOSS_NODES_H3PromptWriter`) |
|
||||
| Work with frame batches | Select Frame, Select Every Nth, Split Batch, Merge Batches (`AUSBOSS_NODES_SelectFrame`, `_SelectEveryNth`, `_SplitBatch`, `_MergeBatches`) |
|
||||
| One shared seed, number or text | Seed, Integer, Float, Text (`AUSBOSS_NODES_Seed`, `_Integer`, `_Float`, `_Text`) |
|
||||
| Do maths on numbers | Math Expression (`AUSBOSS_NODES_MathExpression`) |
|
||||
| Show text on the node, or the picture while it is made | Show Text (`AUSBOSS_NODES_ShowText`), Step Preview (`AUSBOSS_NODES_StepPreview`) |
|
||||
| Free graphics memory between steps | Free Memory (`AUSBOSS_NODES_FreeMemory`) |
|
||||
| Time the whole run | Run Timer (`AUSBOSS_NODES_RunTimer`) |
|
||||
| Turn parts of a workflow off and on | Workflow Switches (`AUSBOSS_NODES_WorkflowSwitches`): [workflow file guide](workflow_switches.md) |
|
||||
| Turn parts of a workflow off and on | Workflow Switches (`AUSBOSS_NODES_WorkflowSwitches`): [its own agent guide](workflow_switches.md) |
|
||||
| Explain a shared workflow on the canvas | Workflow Note (`AUSBOSS_NODES_WorkflowNote`), Callout (`AUSBOSS_NODES_Callout`) |
|
||||
| Lay out boxes of things and words on a canvas | Layout Boxes (`AUSBOSS_NODES_LayoutBoxes`) |
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Workflow Switches: workflow file guide
|
||||
# Workflow Switches: a guide for AI agents
|
||||
|
||||
Add switches by writing the card into a saved workflow file. The card is
|
||||
one node with no wires. It lists which nodes each switch controls in one
|
||||
property. Pick the recipe below that fits, add the card, and set the nodes'
|
||||
starting state.
|
||||
Give this page to an AI agent and it can add switches to a ComfyUI workflow
|
||||
for you. Nobody has to click through menus: the agent writes the card straight
|
||||
into the workflow file.
|
||||
|
||||
**If you are the agent:** the card is one node in the workflow file. It has no
|
||||
wires. It lists which nodes each switch controls, in one property. Pick the
|
||||
recipe below that fits, add the card, and set the nodes' starting state.
|
||||
|
||||
## Before you start
|
||||
|
||||
@@ -91,9 +94,8 @@ To switch a whole group of nodes, put the group's `id` in `groups`.
|
||||
### Pick one part
|
||||
|
||||
One pill: picking a name turns its nodes on and the other rows' nodes off. Open
|
||||
it with one row's nodes at `0` and the rest at `4`. Two to four short names
|
||||
sit in a pill when they fit within 400 px. More rows or longer names become
|
||||
a list. A single row keeps the row view.
|
||||
it with one row's nodes at `0` and the rest at `4`. Up to 4 names sit in a
|
||||
pill, more become a list.
|
||||
|
||||
```json
|
||||
"exclusive": true,
|
||||
|
||||
@@ -64,7 +64,7 @@ export function restoreOutputLinks(node, outputs, graph) {
|
||||
const targetNode = graph.getNodeById(target.nodeId);
|
||||
if (!targetNode) continue;
|
||||
const current = lookupLink(graph, targetNode.inputs?.[target.slot]?.link);
|
||||
if (current?.origin_id === node.id) continue; // never stolen
|
||||
if (current?.origin_id === node.id && current.origin_slot === output.slot) continue; // never stolen
|
||||
linkSlots(node, output.slot, targetNode, target.slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ class AusBossSelectEveryNth:
|
||||
"offset skips that many frames before the first kept one, so "
|
||||
"nth 2 / offset 0 keeps frames 1, 3, 5 and offset 1 keeps 2, 4, 6. "
|
||||
"Wire the batch's fps in and the fps output divides it by nth, so "
|
||||
"the thinned clip keeps its playback speed. Offset and whole-frame "
|
||||
"rounding can change its duration."
|
||||
"the thinned clip keeps its duration."
|
||||
)
|
||||
SEARCH_ALIASES = ["every nth", "skip frames", "thin batch", "reduce frames", "ausboss"]
|
||||
|
||||
@@ -75,8 +74,8 @@ class AusBossSelectEveryNth:
|
||||
RETURN_NAMES = ("images", "fps")
|
||||
OUTPUT_TOOLTIPS = (
|
||||
"The kept frames, in their original order.",
|
||||
"The wired fps divided by nth, to keep playback speed; 0 when no fps "
|
||||
"is wired. Offset and whole-frame rounding can change the duration.",
|
||||
"The wired fps divided by nth, so the kept frames still span the same "
|
||||
"time; 0 when no fps is wired.",
|
||||
)
|
||||
FUNCTION = "select"
|
||||
|
||||
|
||||
@@ -130,8 +130,8 @@ class AusBossH3PromptWriter:
|
||||
"such as person, place or outfit, and name them @1 and @2 in your "
|
||||
"lines. Connect a video to copy its moves or carry it on. Add a voice "
|
||||
"or a song and it writes the lines H3 needs. H3's own six parts go "
|
||||
"through with spacing cleaned up and connected audio added. Connect "
|
||||
"a small text model and it fills in a one-line idea."
|
||||
"through as they are. Connect a small text model and it fills in a "
|
||||
"one-line idea."
|
||||
)
|
||||
SEARCH_ALIASES = [
|
||||
"h3 prompt",
|
||||
@@ -253,8 +253,7 @@ class AusBossH3PromptWriter:
|
||||
"default": VIDEO_JOBS[0],
|
||||
"tooltip": (
|
||||
"What the video is for: copy its moves, swap the person in it, keep it going, or "
|
||||
"give it a new voice. Swap and new voice ignore Seconds: the frame count is "
|
||||
"treated as 24 fps, capped at 15 seconds and rounded down to an H3 length."
|
||||
"give it a new voice. Swap and new voice ignore Seconds and keep the video's length."
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -210,8 +210,7 @@ class AusBossSaveImage:
|
||||
"tooltip": (
|
||||
"Caption from upstream: saved as a .txt in the same "
|
||||
"folder with the same name as the image (portrait.png "
|
||||
"-> portrait.txt). Blank falls back to caption; "
|
||||
"both blank means no sidecar."
|
||||
"-> portrait.txt). Empty writes no sidecar."
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
BRUSH_SIZE,
|
||||
ADJUST_DEAD_ZONE,
|
||||
adjustAxis,
|
||||
adjustedBrush,
|
||||
brushAdjusts,
|
||||
brushFlips,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
moveAdjust,
|
||||
startAdjust,
|
||||
steppedBrushSize,
|
||||
steppedSoft,
|
||||
toolKey,
|
||||
} from "../js/shared/brush.mjs";
|
||||
|
||||
@@ -101,3 +104,53 @@ test("the brush under the pointer fades as its marks do", () => {
|
||||
assert.ok(brushProfile(brushReach(80, 1), 80, 1) < 0.01);
|
||||
for (let d = 0; d < 50; d += 5) assert.ok(brushProfile(d, 80, 0.6) >= brushProfile(d + 5, 80, 0.6), "never brighter further out");
|
||||
});
|
||||
|
||||
test("brush values round, clamp and fall back without producing NaN", () => {
|
||||
assert.equal(clampBrushSize("25.6"), 26);
|
||||
assert.equal(clampBrushSize(1), BRUSH_SIZE[0]);
|
||||
for (const value of [undefined, "large", NaN, Infinity, -Infinity]) {
|
||||
assert.equal(clampBrushSize(value, [4, 1000]), 4);
|
||||
assert.equal(clampSoft(value, 20), 20);
|
||||
}
|
||||
assert.equal(clampSoft("24.6"), 25);
|
||||
assert.equal(clampSoft(-5), 0);
|
||||
assert.equal(clampSoft(101), 100);
|
||||
assert.equal(clampSoft(undefined, 120), 100);
|
||||
});
|
||||
|
||||
test("brush keys change small sizes by at least two pixels and stop at the bounds", () => {
|
||||
assert.equal(steppedBrushSize(2, true), 4);
|
||||
assert.equal(steppedBrushSize(4, false), 2);
|
||||
assert.equal(steppedBrushSize(2, false), 2);
|
||||
assert.equal(steppedBrushSize(100, false), 84);
|
||||
assert.equal(steppedBrushSize(2000, true), 2000);
|
||||
assert.equal(steppedSoft(50, true), 75);
|
||||
assert.equal(steppedSoft(50, false), 25);
|
||||
assert.equal(steppedSoft(90, true), 100);
|
||||
assert.equal(steppedSoft(10, false), 0);
|
||||
});
|
||||
|
||||
test("brush adjustment starts at the dead-zone boundary and breaks diagonal ties by size", () => {
|
||||
const edge = ADJUST_DEAD_ZONE;
|
||||
assert.equal(adjustAxis(edge - 1, -(edge - 1)), null);
|
||||
for (const direction of [-1, 1]) {
|
||||
assert.equal(adjustAxis(direction * edge, 0), "size");
|
||||
assert.equal(adjustAxis(0, direction * edge), "soft");
|
||||
assert.equal(adjustAxis(direction * edge, -edge), "size");
|
||||
}
|
||||
});
|
||||
|
||||
test("a tiny blur keeps the hard-brush edge, while a soft brush scales with its size", () => {
|
||||
// Below a quarter pixel of blur the preview uses a solid disc.
|
||||
assert.equal(brushProfile(1, 2, 0.5), 1);
|
||||
assert.equal(brushProfile(1.01, 2, 0.5), 0);
|
||||
assert.equal(brushReach(2, 0.5), 1);
|
||||
for (const soft of [0.25, 0.5, 1]) {
|
||||
assert.equal(brushReach(160, soft), 2 * brushReach(80, soft));
|
||||
for (const distance of [0, 10, 20, 40, 80]) {
|
||||
const value = brushProfile(distance, 80, soft);
|
||||
assert.ok(value >= 0 && value <= 1);
|
||||
assert.equal(brushProfile(distance * 2, 160, soft), value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,3 +89,60 @@ test("a failed lookup says why in plain words", () => {
|
||||
assert.equal(civitaiFailureText(new Error("Civitai answered 500.")), "Civitai answered 500.");
|
||||
assert.equal(civitaiFailureText(null), "The Civitai lookup failed.");
|
||||
});
|
||||
|
||||
test("parsing accepts an object at the size limit and rejects oversized or non-text bodies", () => {
|
||||
const body = '{"name":"' + "x".repeat(MAX_CIVITAI_BYTES - 11) + '"}';
|
||||
assert.equal(body.length, MAX_CIVITAI_BYTES);
|
||||
assert.equal(parseCivitaiBody(body).name.length, MAX_CIVITAI_BYTES - 11);
|
||||
assert.throws(() => parseCivitaiBody(body + " "), /too large/);
|
||||
for (const value of [null, undefined, {}, 1]) {
|
||||
assert.throws(() => parseCivitaiBody(value), /too large/);
|
||||
}
|
||||
for (const text of ["true", "false", "0", '"model"']) {
|
||||
assert.throws(() => parseCivitaiBody(text), /not what we expected/);
|
||||
}
|
||||
assert.throws(() => parseCivitaiBody('{"id":'), SyntaxError);
|
||||
});
|
||||
|
||||
test("numeric picture ratings take priority over legacy words and booleans", () => {
|
||||
for (const level of [1, 2, "2"]) {
|
||||
assert.equal(isMildPicture({ nsfwLevel: level, nsfw: "Mature" }), true);
|
||||
}
|
||||
for (const level of [4, 8, 16, 3]) {
|
||||
assert.equal(isMildPicture({ nsfwLevel: level, nsfw: false }), false);
|
||||
}
|
||||
assert.equal(isMildPicture({ nsfw: false }), true);
|
||||
assert.equal(isMildPicture({ nsfw: true }), false);
|
||||
assert.equal(isMildPicture({ nsfwLevel: 0, nsfw: "SOFT" }), true);
|
||||
assert.equal(isMildPicture(null), false);
|
||||
});
|
||||
|
||||
test("picture selection skips malformed entries even when any rating is allowed", () => {
|
||||
const still = { url: picture("still") };
|
||||
const info = { images: [
|
||||
null, {}, { url: 42 },
|
||||
{ url: picture("clip"), type: "video", nsfwLevel: 1 },
|
||||
{ url: "https://example.org/picture.jpeg", nsfwLevel: 1 },
|
||||
still,
|
||||
{ url: picture("later"), nsfwLevel: 1 },
|
||||
] };
|
||||
assert.equal(pickCivitaiPicture(info, true), still, "the first usable still wins without copying it");
|
||||
assert.equal(pickCivitaiPicture(info), info.images[6], "an unrated still is skipped in mild mode");
|
||||
for (const images of [null, {}, "pictures", []]) {
|
||||
assert.equal(pickCivitaiPicture({ images }, true), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("picture resizing drops query and fragment and refuses incomplete picture paths", () => {
|
||||
assert.equal(smallPictureUrl(picture("a") + "?width=999&token=example#preview"),
|
||||
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0b1c2d3e/width=450/a.jpeg");
|
||||
assert.equal(smallPictureUrl("https://image.civitai.com/a.jpeg"), null);
|
||||
assert.equal(smallPictureUrl("https://image.civitai.com/"), null);
|
||||
for (const url of [
|
||||
"https://civitai.com.example.org/file.jpeg",
|
||||
"https://civitai.com@other.example/file.jpeg",
|
||||
"file:///tmp/file.jpeg", "not a url",
|
||||
]) {
|
||||
assert.equal(isCivitaiAddress(url), false, url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,3 +145,82 @@ test("restoreOutputLinks hands back only the links a replacement took", () => {
|
||||
assert.equal(graph.links.get(saver.inputs[0].link).origin_id, 1);
|
||||
assert.equal(preview.inputs[0].link, untouched, "a link that was never taken stays as it is");
|
||||
});
|
||||
|
||||
test("lookupLink keeps link id zero and tolerates absent tables and stale ids", () => {
|
||||
const link = { id: 0 };
|
||||
for (const links of [new Map([[0, link]]), { 0: link }]) {
|
||||
assert.equal(lookupLink({ links }, 0), link);
|
||||
assert.equal(lookupLink({ links }, 99), null);
|
||||
assert.equal(lookupLink({ links }, undefined), null);
|
||||
}
|
||||
assert.equal(lookupLink({}, 0), null);
|
||||
});
|
||||
|
||||
test("snapshotLinks skips stale links in an older plain-object graph", () => {
|
||||
const graph = { links: {
|
||||
0: { origin_id: 1, origin_slot: 2, target_id: 5, target_slot: 0 },
|
||||
8: { origin_id: 5, origin_slot: 1, target_id: 9, target_slot: 3 },
|
||||
} };
|
||||
const node = {
|
||||
inputs: [{ name: "image", link: 0 }, { name: "empty", link: null }, { name: "stale", link: 99 }],
|
||||
outputs: [{ name: "unused" }, { name: "mask", links: [99, 8] }, { name: "stale", links: [77] }],
|
||||
};
|
||||
const snapshot = snapshotLinks(node, graph);
|
||||
assert.deepEqual(snapshot, {
|
||||
inputs: [{ name: "image", originId: 1, originSlot: 2 }],
|
||||
outputs: [{ name: "mask", slot: 1, targets: [{ nodeId: 9, slot: 3 }] }],
|
||||
});
|
||||
graph.links[0].origin_slot = 4;
|
||||
graph.links[8].target_slot = 0;
|
||||
node.inputs[0].name = "changed";
|
||||
assert.equal(snapshot.inputs[0].originSlot, 2, "the snapshot owns its slot values");
|
||||
assert.equal(snapshot.inputs[0].name, "image");
|
||||
assert.equal(snapshot.outputs[0].targets[0].slot, 3);
|
||||
assert.deepEqual(snapshotLinks({}, graph), { inputs: [], outputs: [] });
|
||||
});
|
||||
|
||||
test("linkSlots refuses redirects outside the target's inputs before calling connectSlots", () => {
|
||||
const { makeNode } = makeGraph();
|
||||
const source = makeNode(1, [], ["out"]);
|
||||
const sink = makeNode(2, ["in"], []);
|
||||
source.connectSlots = () => assert.fail("a refused redirect must not connect");
|
||||
for (const redirect of [null, false, "0", -1, 4, NaN]) {
|
||||
sink.onBeforeConnectInput = function (slot, requested) {
|
||||
assert.equal(this, sink);
|
||||
assert.equal(slot, 0);
|
||||
assert.equal(requested, 0);
|
||||
return redirect;
|
||||
};
|
||||
assert.equal(linkSlots(source, 0, sink, 0), null, String(redirect));
|
||||
}
|
||||
});
|
||||
|
||||
test("restoreOutputLinks skips missing targets and reconnects a disconnected input", () => {
|
||||
const { graph, makeNode } = makeGraph();
|
||||
const original = makeNode(1, [], ["image", "mask"]);
|
||||
const sink = makeNode(2, ["mask"], []);
|
||||
restoreOutputLinks(original, [{ slot: 1, targets: [
|
||||
{ nodeId: 99, slot: 0 }, { nodeId: 2, slot: 0 }, { nodeId: 2, slot: 99 },
|
||||
] }], graph);
|
||||
const link = lookupLink(graph, sink.inputs[0].link);
|
||||
assert.equal(link.origin_id, original.id);
|
||||
assert.equal(link.origin_slot, 1);
|
||||
assert.equal(graph.links.size, 1);
|
||||
});
|
||||
|
||||
test("restoreOutputLinks restores the output slot even when the source node is unchanged", () => {
|
||||
const { graph, makeNode } = makeGraph();
|
||||
const original = makeNode(1, [], ["first image", "second image"]);
|
||||
const sink = makeNode(2, ["image"], []);
|
||||
linkSlots(original, 1, sink, 0);
|
||||
const { outputs } = snapshotLinks(original, graph);
|
||||
linkSlots(original, 0, sink, 0);
|
||||
|
||||
restoreOutputLinks(original, outputs, graph);
|
||||
const restored = lookupLink(graph, sink.inputs[0].link);
|
||||
assert.equal(restored.origin_id, original.id);
|
||||
assert.equal(restored.origin_slot, 1);
|
||||
assert.deepEqual(original.outputs[0].links, []);
|
||||
assert.deepEqual(original.outputs[1].links, [restored.id]);
|
||||
assert.equal(graph.links.size, 1);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
ensureNodeMinHeight,
|
||||
fillNodeHeight,
|
||||
growToNodeFloor,
|
||||
holdNodeMinWidth,
|
||||
holdNodeMinHeight,
|
||||
holdVueNodeMinWidth,
|
||||
@@ -319,3 +320,90 @@ test("a new node hugs its floor and a loading workflow keeps its saved height",
|
||||
assert.equal(nodeHeightAfterCardChange({ floor: 469, current: 700, change: "x" }), 700);
|
||||
assert.equal(nodeHeightAfterCardChange(), 0);
|
||||
});
|
||||
|
||||
test("a failed width probe restores the widget and allows the next measurement", () => {
|
||||
const own = () => ({ minWidth: 320, minHeight: 80 });
|
||||
const widget = { computeLayoutSize: own };
|
||||
assert.equal(measureLayoutWidthPadding({ computeSize() { throw new Error("not laid out"); } }, widget), null);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
assert.equal(measureLayoutWidthPadding(frontendNode([widget]), widget), 104);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
});
|
||||
|
||||
test("a width probe cannot recursively start another probe", () => {
|
||||
const widget = { computeLayoutSize: () => ({ minWidth: 320, minHeight: 80 }) };
|
||||
const own = widget.computeLayoutSize;
|
||||
const node = {
|
||||
computeSize() {
|
||||
assert.equal(measureLayoutWidthPadding(this, widget), null);
|
||||
return [widget.computeLayoutSize().minWidth + 80, 200];
|
||||
},
|
||||
};
|
||||
assert.equal(measureLayoutWidthPadding(node, widget), 80);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
});
|
||||
|
||||
test("growing to a changing width floor preserves height and leaves wider nodes alone", () => {
|
||||
const sizes = [];
|
||||
const dirty = [];
|
||||
const host = { style: {} };
|
||||
const node = {
|
||||
size: [240, 480],
|
||||
setSize(size) { sizes.push(size); this.size = size; },
|
||||
setDirtyCanvas(...args) { dirty.push(args); },
|
||||
};
|
||||
const widget = { node, element: { closest: () => host } };
|
||||
let floor = 320;
|
||||
holdNodeMinWidth(widget, () => floor);
|
||||
assert.equal(growToNodeFloor(widget, 1), true);
|
||||
assert.deepEqual(node.size, [320, 480]);
|
||||
assert.equal(host.style.minWidth, "320px");
|
||||
assert.deepEqual(dirty, [[true, true]]);
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
floor = 280;
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
assert.equal(host.style.minWidth, "280px", "the CSS floor can shrink without shrinking the node");
|
||||
floor = 400;
|
||||
assert.equal(growToNodeFloor(widget, 1), true);
|
||||
assert.deepEqual(sizes, [[320, 480], [400, 480]]);
|
||||
node.size[0] = NaN;
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
assert.equal(growToNodeFloor({ node: { size: [100, 100] } }), false);
|
||||
assert.equal(growToNodeFloor(null), false);
|
||||
});
|
||||
|
||||
test("growing a node measures its floor again on the next frame with a bounded retry", () => {
|
||||
const saved = globalThis.requestAnimationFrame;
|
||||
const frames = [];
|
||||
globalThis.requestAnimationFrame = (callback) => { frames.push(callback); };
|
||||
try {
|
||||
let floor = 320;
|
||||
const node = { size: [240, 480], setSize(size) { this.size = size; } };
|
||||
const widget = { node };
|
||||
holdNodeMinWidth(widget, () => floor);
|
||||
assert.equal(growToNodeFloor(widget), true);
|
||||
assert.equal(frames.length, 1);
|
||||
floor = 360;
|
||||
frames.shift()();
|
||||
assert.deepEqual(node.size, [360, 480]);
|
||||
assert.equal(frames.length, 0, "the second pass must not schedule another frame");
|
||||
} finally {
|
||||
if (saved === undefined) delete globalThis.requestAnimationFrame;
|
||||
else globalThis.requestAnimationFrame = saved;
|
||||
}
|
||||
});
|
||||
|
||||
test("dynamic panel floors sanitize invalid sizes and retain unrelated options", () => {
|
||||
let width = "240";
|
||||
let height = "160";
|
||||
const widget = { options: { serialize: false } };
|
||||
fillNodeHeight(widget, { minWidth: () => width, minHeight: () => height });
|
||||
assert.deepEqual(widget.computeLayoutSize(), { minWidth: 240, minHeight: 160 });
|
||||
for (const invalid of [-4, NaN, Infinity, undefined, "wide"]) {
|
||||
width = invalid;
|
||||
height = invalid;
|
||||
assert.deepEqual(widget.computeLayoutSize(), { minWidth: 0, minHeight: 0 });
|
||||
}
|
||||
assert.equal(widget.options.serialize, false);
|
||||
assert.equal(fillNodeHeight(null), null);
|
||||
});
|
||||
|
||||
@@ -214,3 +214,71 @@ test("a number box reads a typed sum only where a plain number fails", () => {
|
||||
const source = readFileSync(join(ROOT, "js", "shared", "scrub_input.mjs"), "utf-8");
|
||||
assert.match(source, /Number\.isFinite\(number\) \? number : sumValue\(input\.value\)/);
|
||||
});
|
||||
|
||||
test("formatting keeps significant digits while trimming only fractional zeroes", () => {
|
||||
for (const [value, options, expected] of [
|
||||
[undefined, {}, ""],
|
||||
["12.340", { decimals: 3, minDecimals: 0 }, "12.34"],
|
||||
[-120, { decimals: 3, minDecimals: 0 }, "-120"],
|
||||
[-1.2346, { decimals: 3, minDecimals: 1 }, "-1.235"],
|
||||
[0.0001, { decimals: 4, minDecimals: 0 }, "0.0001"],
|
||||
[12, { decimals: -2 }, "12"],
|
||||
[1, { decimals: 2, minDecimals: -1 }, "1"],
|
||||
]) {
|
||||
assert.equal(formatScrubValue(value, options), expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid scrub values use a finite floor or zero before rounding", () => {
|
||||
for (const value of [undefined, NaN, Infinity, -Infinity, "not a number"]) {
|
||||
assert.equal(quantizeScrubValue(value), 0);
|
||||
assert.equal(quantizeScrubValue(value, { min: -2, max: 4 }), -2);
|
||||
}
|
||||
assert.equal(quantizeScrubValue("2.718", { decimals: 2 }), 2.72);
|
||||
assert.equal(quantizeScrubValue(-10, { min: -2, max: 4 }), -2);
|
||||
assert.equal(quantizeScrubValue(10, { min: -2, max: 4 }), 4);
|
||||
});
|
||||
|
||||
test("snapped steps cross zero and take several steps from an off-grid value", () => {
|
||||
for (const [start, count, expected] of [
|
||||
[-9, 1, -8], [-9, -1, -16], [-9, 3, 8],
|
||||
[9, -3, -8], [9, 3, 32], [0, -1, -8], [9, 0, 9],
|
||||
]) {
|
||||
assert.equal(steppedValue(start, count, 8, true), expected, `${start}, ${count}`);
|
||||
}
|
||||
assert.equal(steppedValue("9", -2, 8), -7);
|
||||
});
|
||||
|
||||
test("gesture boundaries accept equal diagonal travel only past the dead zone", () => {
|
||||
for (const direction of [-1, 1]) {
|
||||
assert.equal(isScrubGesture(direction * SCRUB_DEAD_ZONE, 0), false);
|
||||
assert.equal(isScrubGesture(direction * 4, -4), true);
|
||||
assert.equal(isScrubGesture(direction * 4, 5), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("Shift slows dragging when the supplied fine step is equal or larger", () => {
|
||||
const delta = SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP * SCRUB_SLOW_FACTOR * 2;
|
||||
for (const fineStep of [null, 1, 2]) {
|
||||
const options = { step: 1, fineStep, decimals: 0 };
|
||||
assert.equal(scrubbedValue(10, delta, true, options), 12);
|
||||
assert.equal(scrubbedValue(10, -delta, true, options), 8);
|
||||
assert.equal(scrubbedValue(10, delta, false, options), 18);
|
||||
}
|
||||
});
|
||||
|
||||
test("step precision accepts negative and string increments and rounds at six decimals", () => {
|
||||
for (const [step, expected] of [[-0.025, 3], ["0.000001", 6], [0.1234567, 6], [0, 0], [undefined, 0]]) {
|
||||
assert.equal(stepDecimals(step), expected, String(step));
|
||||
}
|
||||
});
|
||||
|
||||
test("typed sums associate division and subtraction left to right", () => {
|
||||
assert.equal(sumValue("24/4/2"), 3);
|
||||
assert.equal(sumValue("10-3-2"), 5);
|
||||
assert.equal(sumValue("20%6*2"), 4);
|
||||
assert.equal(sumValue("\t-(2 + +3) * -2\t"), 10);
|
||||
assert.equal(sumValue("1" + " ".repeat(199)), 1);
|
||||
assert.equal(sumValue("1" + " ".repeat(200)), null);
|
||||
assert.equal(sumValue("1/(2-2)"), null);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
CARD_PADDING, GROUP_HEIGHT, ROW_GAP, ROW_HEIGHT, SECTION_HEIGHT, SLOT_OFFSET, SOCKETLESS_CHOICE, SOCKETLESS_TEXT,
|
||||
cardHeight, choiceLabel, commitWidgetValue, holdsUnknownValue, markSocketless, resetUnknownValues, rowHeight, rowInLook, rowKind, rowMuted, rowTops,
|
||||
cardHeight, choiceLabel, comboValues, commitWidgetValue, holdsUnknownValue, markSocketless, resetUnknownValues, rowHeight, rowInLook, rowKind, rowMuted, rowTops,
|
||||
scrubSteps, segmentFits, socketWidgetY, visibleRows, widgetTooltip,
|
||||
} from "../js/shared/widget_card_math.mjs";
|
||||
|
||||
@@ -418,3 +418,49 @@ test("a choice's words come from the row: a map, or a function for lists only th
|
||||
// The pill or the list is chosen from the words a person reads, so two long file names still make a short pill.
|
||||
assert.equal(rowKind({ widget: "fine_edges", labels: row.labels }, { type: "combo", options: { values: ["off", "vitmatte-small-composition-1k/model.safetensors"] } }), "segment");
|
||||
});
|
||||
|
||||
test("dynamic combo values receive the widget and preserve numeric and boolean choices", () => {
|
||||
const values = [0, false, "0"];
|
||||
const widget = { type: "combo", value: 0, options: { values: (own) => {
|
||||
assert.equal(own, widget);
|
||||
return values;
|
||||
} } };
|
||||
assert.equal(comboValues(widget), values);
|
||||
assert.equal(holdsUnknownValue(widget), false);
|
||||
widget.value = false;
|
||||
assert.equal(holdsUnknownValue(widget), false);
|
||||
widget.value = true;
|
||||
assert.equal(holdsUnknownValue(widget), true);
|
||||
for (const value of [undefined, null, "choices", {}]) {
|
||||
assert.deepEqual(comboValues({ options: { values: () => value } }), []);
|
||||
}
|
||||
assert.deepEqual(comboValues(null), []);
|
||||
});
|
||||
|
||||
test("choice labels keep explicit empty and numeric labels and use values only as a fallback", () => {
|
||||
const row = { labels: { off: "", zero: 0, missing: null } };
|
||||
assert.equal(choiceLabel(row, "off"), "");
|
||||
assert.equal(choiceLabel(row, "zero"), 0);
|
||||
assert.equal(choiceLabel(row, "missing"), "missing");
|
||||
assert.equal(choiceLabel({ labels: () => undefined }, 0), "0");
|
||||
assert.equal(choiceLabel(null, false), "false");
|
||||
});
|
||||
|
||||
test("segmented choices include the exact label-length limit and reject the next character", () => {
|
||||
assert.equal(segmentFits(["a".repeat(13), "b".repeat(13)]), true);
|
||||
assert.equal(segmentFits(["a".repeat(13), "b".repeat(14)]), false);
|
||||
assert.equal(segmentFits([0, false]), true);
|
||||
assert.equal(segmentFits([]), false);
|
||||
assert.equal(segmentFits(null), false);
|
||||
});
|
||||
|
||||
test("scrub steps honor legacy increments, explicit precision and the pixel-range boundary", () => {
|
||||
assert.deepEqual(scrubSteps(), { step: 0.1, fineStep: 0.01, decimals: 2 });
|
||||
assert.deepEqual(scrubSteps({}, false), { step: 1, fineStep: 1, decimals: 0 });
|
||||
assert.deepEqual(scrubSteps({ step: 0.5 }), { step: 0.5, fineStep: 0.05, decimals: 2 });
|
||||
assert.deepEqual(scrubSteps({ step: 5, step2: 0.25, precision: 3, min: 0, max: 2 }),
|
||||
{ step: 0.25, fineStep: 0.25, decimals: 3 });
|
||||
assert.equal(scrubSteps({ min: 0, max: 2048 }, false, true).step, 1);
|
||||
assert.equal(scrubSteps({ min: 0, max: 2049 }, false, true).step, 8);
|
||||
assert.equal(scrubSteps({}, false, true).step, 1, "an unknown range is not assumed to be a wide pixel size");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user