Compare commits

...
6 Commits
Author SHA1 Message Date
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
26 changed files with 363 additions and 134 deletions
+30
View File
@@ -2,6 +2,36 @@
All notable changes to ComfyUI-AusBoss are documented here.
## 2.22.0 - 2026-11-07
- **Node cards have a calmer, easier-to-read look.** Number boxes, choices,
switches, fields and buttons are now one solid gray with no outlines, on a
dark card. Labels, units and section names are lighter and easier to read,
and every control in a card row is the same height. The bar above a result
picture (the picture arrows, the Preview steps and Preview switches, and
buttons such as AUTO) and the picture's frame match the cards. The colors
live in one shared file, so the rest of the pack can follow.
- **Mask by Touch 🆎 no longer fills holes unless you ask.** Fill small
holes, under More, now starts off, so a hole you pick stays a hole. Saved
workflows keep their setting.
- **H3 Prompt Writer: a swap no longer makes the new person talk to noise.** With
**Keep the clip's sound** on and no words typed, the writer told H3 the new
person speaks as in the video whenever the clip's sound was loud, so a train
or a beat made their mouth move. It now does that only when it hears a real
voice: half a second or more of clear speech.
- **Mask by Touch shows a changed picture without unplugging it.** When the
picture coming in changes and the run stops because the picks were made on
the old one, the new picture now shows on the node, ready to pick from. A
new reload button beside Undo shows it any time, without running the
workflow.
- **Save Image and Save Video: the Steps switch is now called Preview steps.** It
shows the picture or the clip's first frame while it is being made, and "Steps"
read like a sampler's step count. It works the same, and saved workflows keep
their setting.
## 2.21.2 - 2026-11-07
- **H3 Prompt Writer reads a plain swap sentence.** "Replace the man in the red
+1 -1
View File
@@ -199,7 +199,7 @@ It returns the new frames and their fps. It doesn't touch audio, so carry the so
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 **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.
**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
+3 -3
View File
@@ -1,9 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.21.2">
<title>release: v2.21.2</title>
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.22.0">
<title>release: v2.22.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.21.2</text>
<text x="76.5" y="14">v2.22.0</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 425 B

@@ -439,7 +439,7 @@
0,
0,
0,
true,
false,
"stop the run",
false,
false,
@@ -451,7 +451,7 @@
"grow": 0,
"soften": 0,
"smooth": 0,
"fill_holes": true,
"fill_holes": false,
"if_nothing": "stop the run",
"free_vram": false,
"invert": false,
+10 -2
View File
@@ -120,6 +120,8 @@ or an area you painted.
## The tools on the node
- **Reload** (the round arrow) shows the picture coming in now: see
**Which picture the picks belong to**, below.
- **Undo** takes back the last change, or a Clear. Ctrl+Z works as well: it
is ComfyUI's own undo, and every change counts.
- **Clear** picks nothing.
@@ -152,7 +154,8 @@ or an area you painted.
- **More → Smooth** (`smooth`): rounds off a ragged outline by this many
pixels.
- **More → Fill small holes** (`fill_holes`): closes pinholes inside the
picked area. Larger openings stay open, like a mug's handle.
picked area. Larger openings stay open, like a mug's handle. Off by
default, so a hole you pick stays a hole.
- **More → Invert** (`invert`): turns the mask round. On, the mask is
everything except what you picked.
- **More → If nothing** (`if_nothing`): **stop the run** or **empty mask**,
@@ -209,7 +212,12 @@ nothing instead.
Picks belong to the picture they were made on. Load another picture and the
node shows it with nothing picked, and says the old picks are not used.
If the workflow runs before the node has shown the new picture, the run
stops and says the picks were made on another picture.
stops and says the picks were made on another picture. The new picture
shows on the node then, ready to pick from.
- **The reload button** (the round arrow beside Undo) shows the picture
coming in now. Use it after you change the picture before this node, for
example with an edit. It runs only this node and what comes before it.
- With a Load Image wired straight in, or through Reroute nodes, the
picture shows as soon as you choose a file.
+1 -1
View File
@@ -52,7 +52,7 @@ file's name. A caption on the `caption_text` input writes the paired `.txt`.
still lists it. When a run saves more than one image (a batch, or a list
such as a folder of images), arrows beside the switch (‹ 2 / 5 ›) step
through them.
- **Steps** (`step_preview`, off by default): the switch left of
- **Preview steps** (`step_preview`, off by default): the switch left of
**preview** shows the picture while it is being made, one step at a time,
where the saved image goes, with a **STOP** button on the bar. Stop a run
that is going wrong after the first steps instead of waiting for the end;
+1 -1
View File
@@ -9,7 +9,7 @@ duration, shows a tall video whole, and has a **LOOP** control. `gif` and
`webp` are shown as pictures with just the file name, since a video player
cannot play either.
**Steps** above the player (`step_preview`, off by default) shows the clip's first frame in
**Preview steps** above the player (`step_preview`, off by default) shows the clip's first frame in
the player while it is being made, one step at a time, with a **STOP**
button on the player. Stop a clip that is going wrong after the first steps
instead of waiting for the end; a stopped run saves nothing. The saved
+13 -11
View File
@@ -19,7 +19,8 @@ import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { isForeignRun } from "../shared/prompt_scope.mjs";
import { executionIdOf, nodeByExecutionId } from "../shared/graph_ids.mjs";
import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "../shared/index.mjs";
import { FONT_UI, RADIUS_CONTROL, RADIUS_XS, THEME as T } from "../shared/theme.mjs";
import { chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "../shared/index.mjs";
import { ensureNodeMinHeight, fillNodeHeight, growToNodeFloor } from "../shared/panel_layout.mjs";
import { suppressCoreImagePreview } from "../shared/core_preview.mjs";
import { autoMaskValues } from "../shared/mask_auto.mjs";
@@ -130,28 +131,29 @@ function ensureCss() {
style.textContent = `
.ausboss-input-preview{box-sizing:border-box;display:flex;flex-direction:column;gap:4px;width:100%;height:100%;padding:0 6px 6px;pointer-events:none;overflow:hidden;}
.ausboss-input-preview-bar{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:${BAR_GAP}px;height:${BAR_HEIGHT}px;padding:0 ${BAR_PADDING}px;pointer-events:none;}
.ausboss-input-preview-stage{position:relative;flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;width:100%;overflow:hidden;border:1px solid rgba(0,180,170,.27);border-radius:6px;background:rgba(0,0,0,.28);}
.ausboss-input-preview-stage{position:relative;flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:center;width:100%;overflow:hidden;border:1px solid ${T.rule};border-radius:${RADIUS_CONTROL}px;background:${T.well};}
[data-node-id] .ausboss-input-preview-stage{box-sizing:border-box;min-height:${STAGE_HEIGHT}px;}
.ausboss-input-preview.preview-off .ausboss-input-preview-stage{display:none;}
.ausboss-input-preview.preview-off .ausboss-stepper{display:none;}
.ausboss-input-preview-stage img,.ausboss-input-preview-stage video{display:none;position:absolute;inset:0;margin:auto;max-width:100%;max-height:100%;object-fit:contain;}
.ausboss-input-preview-stage.show-image img{display:block;}
.ausboss-input-preview-stage.show-video video{display:block;}
.ausboss-input-preview-hint{display:none;max-width:86%;color:#78908e;font:11px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:center;}
.ausboss-input-preview-hint{display:none;max-width:86%;color:${T.faint};font:11px/1.35 ${FONT_UI};text-align:center;}
.ausboss-input-preview-stage:not(.show-image):not(.show-video) .ausboss-input-preview-hint{display:block;}
.ausboss-input-preview-tools{display:flex;gap:4px;pointer-events:auto;}
.ausboss-input-preview-tool{box-sizing:border-box;height:${BAR_HEIGHT}px;min-width:24px;padding:0 6px;border:1px solid rgba(0,180,170,.52);border-radius:4px;background:rgba(0,0,0,.7);color:#c8dddd;font:700 9px/18px "Segoe UI",sans-serif;cursor:pointer;}
.ausboss-input-preview-tool:hover{border-color:${BRAND};color:#fff;background:rgba(0,79,75,.78);}
.ausboss-input-preview-tool.active{border-color:${BRAND};color:${BRAND};}
.ausboss-input-preview-tool{box-sizing:border-box;height:${BAR_HEIGHT}px;min-width:24px;padding:0 6px;border:1px solid transparent;border-radius:${RADIUS_XS}px;background:${T.fill};color:${T.label};font:600 9.5px/18px ${FONT_UI};letter-spacing:.02em;cursor:pointer;}
.ausboss-input-preview-tool:hover{color:${T.ink};background:${T.fillHover};}
.ausboss-input-preview-tool.active{color:${T.brandInk};background:${T.brandSoft};}
.ausboss-input-preview-tool:focus-visible{outline:2px solid ${T.brand};outline-offset:1px;}
.ausboss-input-preview:not(.preview-off) .ausboss-panel-switch.steps + .ausboss-panel-switch{margin-left:0;}
.ausboss-input-preview.preview-off .ausboss-panel-switch.steps{display:none;}
.ausboss-input-preview-tool.ausboss-steps-stop{visibility:hidden;}
.ausboss-input-preview-tool.ausboss-steps-stop.live{visibility:visible;pointer-events:auto;}
.ausboss-input-preview-readout{box-sizing:border-box;height:${BAR_HEIGHT}px;padding:0 7px;border:1px solid rgba(0,180,170,.5);border-radius:10px;background:rgba(0,180,170,.14);color:${BRAND};font:700 9.5px/18px "Segoe UI",sans-serif;white-space:nowrap;pointer-events:none;}
.ausboss-input-preview-readout{box-sizing:border-box;height:${BAR_HEIGHT}px;padding:0 7px;border:1px solid transparent;border-radius:999px;background:${T.brandSoft};color:${T.brandInk};font:600 9.5px/18px ${FONT_UI};font-variant-numeric:tabular-nums;white-space:nowrap;pointer-events:none;}
.ausboss-input-preview-readout:empty{display:none;}
.ausboss-input-preview-readout.warn{border-color:#c98a2b;background:rgba(201,138,43,.16);color:#ffd9a0;}
.ausboss-input-preview-note{display:none;position:absolute;left:6px;right:6px;bottom:6px;z-index:6;padding:6px 8px;border:1px solid #c98a2b;border-radius:6px;background:rgba(20,10,0,.86);color:#ffd9a0;font:600 11px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:center;pointer-events:none;}
.ausboss-input-preview-toast{position:absolute;left:50%;bottom:6px;z-index:5;max-width:88%;padding:3px 7px;border-radius:4px;background:rgba(0,0,0,.78);color:#b8d3d1;font:10px/1.3 "Segoe UI",sans-serif;text-align:center;transform:translateX(-50%);pointer-events:none;}
.ausboss-input-preview-readout.warn{background:${T.warnSoft};color:${T.warnInk};}
.ausboss-input-preview-note{display:none;position:absolute;left:6px;right:6px;bottom:6px;z-index:6;padding:6px 8px;border:1px solid ${T.warn};border-radius:${RADIUS_CONTROL}px;background:${T.warnSoft};color:${T.warnInk};font:600 11px/1.35 ${FONT_UI};text-align:center;pointer-events:none;}
.ausboss-input-preview-toast{position:absolute;left:50%;bottom:6px;z-index:5;max-width:88%;padding:3px 7px;border-radius:${RADIUS_XS}px;background:${T.fill};color:${T.inkSoft};font:10px/1.3 ${FONT_UI};text-align:center;transform:translateX(-50%);pointer-events:none;}
`;
document.head.appendChild(style);
}
@@ -334,7 +336,7 @@ function installStepsSwitch(state, signal) {
if (!widget) return;
state.stepsWidget = widget;
hideWidget(widget);
const button = panelSwitch("Steps", "Show the picture here while it is being made, one step at a time, with a Stop button. Off by default.");
const button = panelSwitch("Preview steps", "Show the picture here while it is being made, one step at a time, with a Stop button. Off by default.");
button.classList.add("steps");
button.addEventListener("pointerdown", (event) => event.stopPropagation(), { signal });
button.addEventListener("click", (event) => {
+6 -1
View File
@@ -168,13 +168,18 @@ api.addEventListener("execution_error", ({ detail }) => {
api.addEventListener("execution_interrupted", () => settleAll());
// A look is one long, quiet run. The node sends what it has found every
// second or two, so things light up while it is still looking.
// A run that stops at the node (nothing picked on this picture) sends the
// picture that came in first, so a picture that changed upstream shows at
// once. It is shown like a kept note: it starts no run of its own.
api.addEventListener(LOOK_EVENT, ({ detail }) => {
try {
if (!faces.size || !detail?.note) return;
const id = String(detail.node ?? "");
const root = app.rootGraph ?? app.graph;
for (const face of faces) {
if (face.alive && face.inFlight && executionIdOf(root, face.node) === id) face.takeNote(detail.note, { interim: true });
if (!face.alive || executionIdOf(root, face.node) !== id) continue;
if (detail.stopped) face.takeNote(detail.note, { restored: true });
else if (face.inFlight) face.takeNote(detail.note, { interim: true });
}
} catch {
// Never in the way of a run.
+2 -2
View File
@@ -39,7 +39,7 @@ function getLoopEnabled(node) {
return !!node.properties.ausboss_save_video_loop;
}
// The STEPS button: the hidden `step_preview` widget (the card hides it).
// The Preview steps switch: the hidden `step_preview` widget (the card hides it).
// On, the player shows each step of the clip's first frame while it is
// being made, and a STOP button joins the tools for the run.
function stepsOn(state) {
@@ -131,7 +131,7 @@ function buildPreview(node) {
loopButton.classList.add("ausboss-steps-loop");
const stopButton = makeToolButton("STOP", "Stop this run now. The next run in the queue still starts.");
stopButton.classList.add("ausboss-steps-stop");
const stepsButton = panelSwitch("Steps", "Show the clip's first frame here while it is being made, one step at a time, with a Stop button. Off by default.");
const stepsButton = panelSwitch("Preview steps", "Show the clip's first frame here while it is being made, one step at a time, with a Stop button. Off by default.");
tools.classList.add("ausboss-steps-tools");
tools.append(stopButton, loopButton);
stage.append(video, image, status, tools);
+1 -1
View File
@@ -9,7 +9,7 @@ import { app } from "../../../scripts/app.js";
// Must match `version` in pyproject.toml — scripts/release_preflight.py
// enforces it. The backend serves its copy at /ausboss/pack_version; a
// mismatch means the browser cached JavaScript from an older install.
export const AUSBOSS_JS_VERSION = "2.21.2";
export const AUSBOSS_JS_VERSION = "2.22.0";
export const BRAND = "#00b4aa"; // AusBoss teal — keep node accents consistent.
export const BRAND_DARK = "#007f78";
+13 -12
View File
@@ -1,30 +1,31 @@
// Shared controls for compact node panels. Only elements explicitly given one
// of these classes are styled; the owning panel decides where they sit.
import { BRAND } from "./index.mjs";
import { FONT_UI, RADIUS_XS, THEME as T } from "./theme.mjs";
export const PANEL_LABELS = Object.freeze({
load: "Load", sources: { input: "Inputs", output: "Outputs" },
embed: "Embed workflow", off: "Off", on: "On",
});
const controlFont = 'system-ui, sans-serif';
export const CAPTION_TYPE = `font:600 11px/1.25 ${controlFont};color:#b2c6c3;`;
export const HINT_TYPE = `font:400 10px/1.25 ${controlFont};color:#95aba8;`;
const controlFont = FONT_UI;
export const CAPTION_TYPE = `font:600 11px/1.25 ${controlFont};color:${T.label};`;
export const HINT_TYPE = `font:400 10px/1.25 ${controlFont};color:${T.faint};`;
export function ensurePanelControls() {
if (document.getElementById("ausboss-panel-controls")) return;
const style = document.createElement("style");
style.id = "ausboss-panel-controls";
style.textContent = `
.ausboss-compact-action{box-sizing:border-box;flex:none;height:22px;min-width:24px;padding:0 7px;border:1px solid #365957;border-radius:4px;background:#111b1c;color:#bdd8d5;font:600 11px/1 ${controlFont};white-space:nowrap;cursor:pointer;pointer-events:auto}
.ausboss-compact-action:hover:enabled,.ausboss-compact-action.active{color:#dbfffb;border-color:${BRAND};background:#173332}
.ausboss-compact-action{box-sizing:border-box;flex:none;height:22px;min-width:24px;padding:0 7px;border:1px solid transparent;border-radius:${RADIUS_XS}px;background:${T.fill};color:${T.inkSoft};font:600 11px/1 ${controlFont};white-space:nowrap;cursor:pointer;pointer-events:auto}
.ausboss-compact-action:hover:enabled{color:${T.ink};background:${T.fillHover}}
.ausboss-compact-action.active{color:${T.brandInk};background:${T.brandSoft}}
.ausboss-compact-action:disabled{opacity:.5;cursor:default}
.ausboss-panel-switch{display:inline-flex;align-items:center;gap:6px;align-self:flex-end;flex:none;box-sizing:border-box;min-height:20px;margin-left:auto;padding:0 2px;background:transparent;border:0;color:#b2c6c3;font:600 11px/1 ${controlFont};cursor:pointer;user-select:none;pointer-events:auto}
.ausboss-panel-switch i{position:relative;width:24px;height:13px;border-radius:7px;background:#3a4445}
.ausboss-panel-switch i::after{content:"";position:absolute;top:2px;left:2px;width:9px;height:9px;border-radius:50%;background:#b6c4c3}
.ausboss-panel-switch[aria-checked=true] i{background:${BRAND}}
.ausboss-panel-switch[aria-checked=true] i::after{left:13px;background:#fff}
.ausboss-compact-action:focus-visible,.ausboss-panel-switch:focus-visible{outline:2px solid ${BRAND};outline-offset:1px}
.ausboss-panel-switch{display:inline-flex;align-items:center;gap:6px;align-self:flex-end;flex:none;box-sizing:border-box;min-height:20px;margin-left:auto;padding:0 2px;background:transparent;border:0;color:${T.label};font:600 11px/1 ${controlFont};cursor:pointer;user-select:none;pointer-events:auto}
.ausboss-panel-switch i{position:relative;width:24px;height:13px;border-radius:7px;background:${T.switchOff}}
.ausboss-panel-switch i::after{content:"";position:absolute;top:2px;left:2px;width:9px;height:9px;border-radius:50%;background:${T.inkSoft}}
.ausboss-panel-switch[aria-checked=true] i{background:${T.brand}}
.ausboss-panel-switch[aria-checked=true] i::after{left:13px;background:${T.inkHover}}
.ausboss-compact-action:focus-visible,.ausboss-panel-switch:focus-visible{outline:2px solid ${T.brand};outline-offset:1px}
`;
document.head.append(style);
}
+6 -6
View File
@@ -3,7 +3,7 @@
// steps through its pairs with it, and the preview panel of Save Image and
// the other preview nodes through their results. It hides itself while
// there is one picture or none, so a single picture looks as it always did.
import { BRAND } from "./index.mjs";
import { FONT_UI, RADIUS_CONTROL, RADIUS_XS, THEME as T } from "./theme.mjs";
import { stepperLabel } from "./picture_steps.mjs";
const CSS_ID = "ausboss-picture-stepper-v1";
@@ -13,12 +13,12 @@ function ensureCss() {
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
.ausboss-stepper{box-sizing:border-box;flex:none;display:flex;align-items:stretch;height:var(--ausboss-stepper-height,24px);padding:1px;border:1px solid #2a3437;border-radius:6px;background:#0f1516;pointer-events:auto;user-select:none;}
.ausboss-stepper{box-sizing:border-box;flex:none;display:flex;align-items:stretch;height:var(--ausboss-stepper-height,24px);padding:1px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};pointer-events:auto;user-select:none;}
.ausboss-stepper[hidden]{display:none;}
.ausboss-stepper button{box-sizing:border-box;width:20px;margin:0;padding:0;border:none;border-radius:4px;background:transparent;color:#8ba3a1;font:700 13px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;cursor:pointer;}
.ausboss-stepper button:hover{color:#fff;background:rgba(0,79,75,.78);}
.ausboss-stepper button:focus-visible{outline:1px solid ${BRAND};outline-offset:-1px;}
.ausboss-stepper span{min-width:34px;padding:0 3px;color:#d8eeee;font:700 10px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-variant-numeric:tabular-nums;text-align:center;white-space:nowrap;align-self:center;}
.ausboss-stepper button{box-sizing:border-box;width:20px;margin:0;padding:0;border:none;border-radius:${RADIUS_XS}px;background:transparent;color:${T.label};font:600 13px/1 ${FONT_UI};cursor:pointer;}
.ausboss-stepper button:hover{color:${T.ink};background:${T.fillHover};}
.ausboss-stepper button:focus-visible{outline:1px solid ${T.brand};outline-offset:-1px;}
.ausboss-stepper span{min-width:34px;padding:0 3px;color:${T.ink};font:600 10px/1 ${FONT_UI};font-variant-numeric:tabular-nums;text-align:center;white-space:nowrap;align-self:center;}
`;
document.head.appendChild(style);
}
+9 -11
View File
@@ -25,10 +25,8 @@
// Shift's fine step and typed values are never snapped.
import { keepKeyInField } from "./canvas_passthrough.mjs";
// Keep in sync with BRAND in shared/index.mjs - importing it would pull
// /scripts/app.js into node:test, and this module's math must stay testable.
const BRAND = "#00b4aa";
// theme.mjs has no imports, so this module's math stays testable in node:test.
import { FONT_UI, RADIUS_CONTROL, THEME as T } from "./theme.mjs";
// Horizontal pixels of drag per step; small enough to feel light, large
// enough that a shaky click cannot change the value. The dead zone keeps a
@@ -181,19 +179,19 @@ function ensureScrubCss() {
// for one) carry blanket `section input` rules, and these must outrank
// them wherever the control is mounted.
style.textContent = `
.ausboss-scrub{display:flex;width:72px;height:24px;border:1px solid #3a4047;border-radius:5px;
background:#23272c;overflow:hidden;flex:none;color:#d7dde2;font:12px system-ui}
.ausboss-scrub:focus-within{border-color:${BRAND}}
.ausboss-scrub{display:flex;width:72px;height:24px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;
background:${T.fill};overflow:hidden;flex:none;color:${T.ink};font:500 12px ${FONT_UI};font-variant-numeric:tabular-nums}
.ausboss-scrub:focus-within{border-color:${T.brand}}
.ausboss-scrub>.ausboss-scrub-input{flex:1 1 auto;min-width:0;width:100%;height:100%;border:none;
padding:0;margin:0;background:transparent;color:inherit;text-align:center;cursor:ew-resize;
user-select:none;font:inherit;outline:none;border-radius:0}
.ausboss-scrub>.ausboss-scrub-input:focus{cursor:text;user-select:text}
.ausboss-scrub>.ausboss-scrub-unit{flex:none;align-self:center;padding:0 3px 0 1px;color:#5f7674;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;text-align:center;user-select:none;pointer-events:none}
.ausboss-scrub>.ausboss-scrub-unit{flex:none;align-self:center;padding:0 3px 0 1px;color:${T.faint};font:600 10px ${FONT_UI};letter-spacing:.04em;text-transform:uppercase;text-align:center;user-select:none;pointer-events:none}
.ausboss-scrub>.ausboss-scrub-step{flex:none;width:14px;display:flex;flex-direction:column;
border-left:1px solid #3a4047}
border-left:1px solid transparent;background:${T.fillHover}}
.ausboss-scrub>.ausboss-scrub-step>button{flex:1 1 0;min-height:0;border:none;background:transparent;
color:#9ba2aa;cursor:pointer;padding:0;margin:0;display:grid;place-items:center;border-radius:0}
.ausboss-scrub>.ausboss-scrub-step>button:hover{color:${BRAND};background:rgba(255,255,255,.05)}
color:${T.label};cursor:pointer;padding:0;margin:0;display:grid;place-items:center;border-radius:0}
.ausboss-scrub>.ausboss-scrub-step>button:hover{color:${T.brand};background:rgba(255,255,255,.05)}
`;
document.head.append(style);
}
+9 -9
View File
@@ -2,7 +2,7 @@
// chosen one: the mode row of Image Compare A/B, and its off | on choices.
// The choices share one width, so the marker only has to move. With `fit`
// each choice is as wide as its own name, and the marker is measured.
import { BRAND } from "./index.mjs";
import { FONT_UI, RADIUS_CONTROL, RADIUS_XS, THEME as T } from "./theme.mjs";
const CSS_ID = "ausboss-slide-switch-v1";
@@ -11,17 +11,17 @@ function ensureCss() {
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
.ausboss-slide{position:relative;box-sizing:border-box;flex:none;display:grid;grid-auto-flow:column;grid-auto-columns:1fr;height:24px;padding:2px;border:1px solid #2a3437;border-radius:6px;background:#0f1516;}
.ausboss-slide{position:relative;box-sizing:border-box;flex:none;display:grid;grid-auto-flow:column;grid-auto-columns:1fr;height:24px;padding:2px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};}
.ausboss-slide.fit{grid-auto-columns:max-content;}
.ausboss-slide.fit button{padding:0 7px;}
.ausboss-slide-mark{position:absolute;top:2px;bottom:2px;left:2px;border-radius:4px;background:${BRAND};box-shadow:0 0 7px rgba(0,180,170,.3);transition:transform .16s ease,width .16s ease;pointer-events:none;}
.ausboss-slide-mark{position:absolute;top:2px;bottom:2px;left:2px;border-radius:${RADIUS_XS}px;background:${T.brand};transition:transform .16s ease,width .16s ease;pointer-events:none;}
.ausboss-slide.none .ausboss-slide-mark{display:none;}
.ausboss-slide button{position:relative;box-sizing:border-box;min-width:0;height:100%;margin:0;padding:0 9px;border:none;border-radius:4px;background:transparent;color:#8ba3a1;font:700 10px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.02em;white-space:nowrap;cursor:pointer;transition:color .16s ease;}
.ausboss-slide button:hover{color:#fff;}
.ausboss-slide button.on,.ausboss-slide button.on:hover{color:#04201d;}
.ausboss-slide.off-left button.on:first-of-type,.ausboss-slide.off-left button.on:first-of-type:hover{color:#d8eeee;}
.ausboss-slide.off-left.is-off .ausboss-slide-mark{background:#33423f;box-shadow:none;}
.ausboss-slide button:focus-visible{outline:1px solid ${BRAND};outline-offset:1px;}
.ausboss-slide button{position:relative;box-sizing:border-box;min-width:0;height:100%;margin:0;padding:0 9px;border:none;border-radius:${RADIUS_XS}px;background:transparent;color:${T.label};font:600 11px/1 ${FONT_UI};white-space:nowrap;cursor:pointer;transition:color .16s ease;}
.ausboss-slide button:hover{color:${T.inkHover};}
.ausboss-slide button.on,.ausboss-slide button.on:hover{color:${T.onBrand};}
.ausboss-slide.off-left button.on:first-of-type,.ausboss-slide.off-left button.on:first-of-type:hover{color:${T.ink};}
.ausboss-slide.off-left.is-off .ausboss-slide-mark{background:${T.switchOff};}
.ausboss-slide button:focus-visible{outline:1px solid ${T.brand};outline-offset:1px;}
@media (prefers-reduced-motion: reduce){.ausboss-slide-mark,.ausboss-slide button{transition:none;}}
`;
document.head.appendChild(style);
+8 -8
View File
@@ -12,7 +12,7 @@
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { nodeByExecutionId } from "./graph_ids.mjs";
import { BRAND } from "./index.mjs";
import { FONT_UI, RADIUS_CONTROL, THEME as T } from "./theme.mjs";
import { outputRecordQuery } from "./input_preview.mjs";
import { isForeignRun } from "./prompt_scope.mjs";
import {
@@ -213,15 +213,15 @@ export function ensureStepCss() {
.ausboss-steps.on{display:block;}
.ausboss-steps-showing>img,.ausboss-steps-showing>video,.ausboss-steps-showing>.ausboss-input-preview-hint,.ausboss-steps-showing>.ausboss-video-status{visibility:hidden;}
.ausboss-steps img,.ausboss-steps.on img{display:block;position:absolute;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;object-fit:contain;visibility:visible;}
.ausboss-steps-line{position:absolute;left:0;top:0;height:3px;width:0;background:${BRAND};box-shadow:0 0 6px rgba(0,180,170,.5);transition:width .15s linear;}
.ausboss-steps-chip{position:absolute;left:6px;right:6px;bottom:6px;box-sizing:border-box;padding:4px 7px;border:1px solid rgba(255,255,255,.08);border-radius:5px;background:rgba(8,12,13,.84);color:#d8ecea;font:600 10.5px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ausboss-steps-line{position:absolute;left:0;top:0;height:3px;width:0;background:${T.brand};transition:width .15s linear;}
.ausboss-steps-chip{position:absolute;left:6px;right:6px;bottom:6px;box-sizing:border-box;padding:4px 7px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:rgba(21,24,26,.88);color:${T.inkSoft};font:600 10.5px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ausboss-steps.stopped img{filter:brightness(.55) saturate(.7);}
.ausboss-steps.stopped .ausboss-steps-line{display:none;}
.ausboss-steps.stopped .ausboss-steps-chip{border-color:rgba(224,96,90,.55);color:#ffd0cc;}
.ausboss-steps-hint{position:absolute;left:50%;top:50%;max-width:86%;transform:translate(-50%,-50%);color:#78908e;font:11px/1.35 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:center;}
.ausboss-steps-stop{border-color:rgba(224,96,90,.8)!important;background:rgba(58,28,27,.9)!important;color:#ffd9d6!important;}
.ausboss-steps-stop:hover{background:rgba(120,38,33,.95)!important;color:#fff!important;}
.ausboss-steps-stop::before{content:"";display:inline-block;width:6px;height:6px;margin-right:5px;border-radius:1px;background:#e0605a;vertical-align:1px;}
.ausboss-steps.stopped .ausboss-steps-chip{border-color:${T.danger};color:${T.dangerInk};}
.ausboss-steps-hint{position:absolute;left:50%;top:50%;max-width:86%;transform:translate(-50%,-50%);color:${T.faint};font:11px/1.35 ${FONT_UI};text-align:center;}
.ausboss-steps-stop{border-color:transparent!important;background:${T.dangerSoft}!important;color:${T.dangerInk}!important;}
.ausboss-steps-stop:hover{background:rgba(120,38,33,.95)!important;color:${T.inkHover}!important;}
.ausboss-steps-stop::before{content:"";display:inline-block;width:6px;height:6px;margin-right:5px;border-radius:1px;background:${T.danger};vertical-align:1px;}
.ausboss-video-tool.ausboss-steps-stop{display:none;}
.ausboss-video-tool.ausboss-steps-stop.live{display:inline-block;}
.ausboss-video-stage.is-still .ausboss-video-tools.ausboss-steps-tools{display:flex;}
+50
View File
@@ -0,0 +1,50 @@
// The pack's node-face palette, in one place. Cards, number boxes and
// switches read their colours, font and sizes from here instead of
// carrying their own literals.
//
// The look: solid gray controls with no outlines on a near-black card,
// neutral grays, and one teal accent for whatever is on or picked.
//
// No imports on purpose: scrub_input.mjs and the node:test suites load
// this file without the ComfyUI app.
export const THEME = Object.freeze({
brand: "#00b4aa", // the one accent: picked segment, switch on, focus ring
onBrand: "#04201d", // text on a brand fill
brandSoft: "rgba(0,180,170,.16)", // chip fill
brandSoftHover: "rgba(0,180,170,.26)",
brandInk: "#45d6cb", // chip text on brandSoft
brandGlow: "#6fe0d6", // the big text look
card: "#101214", // widget card ground
fill: "#2d3134", // every control: solid, no border
fillHover: "#383c40", // control hover, the chevron column
well: "#0f1112", // picture stage ground
rule: "#2b2f32", // hairline after a section label
switchOff: "#62676d", // the mark of a switch sitting on off
ink: "#f6f7f8", // values: numbers, picked options, typed text
inkSoft: "#e0e3e5", // default card text
inkHover: "#ffffff",
label: "#b4b9bd", // row labels, unselected segments, chevrons
faint: "#979da2", // units, section labels, placeholders, muted rows
danger: "#e0605a", // stop: the dot on the Stop button
dangerInk: "#ffd9d6",
dangerSoft: "#3a1c1b",
warn: "#e0a040",
warnInk: "#ffdca8",
warnSoft: "#3a2a10",
});
export const FONT_UI = `system-ui,-apple-system,"Segoe UI",Roboto,sans-serif`;
// Every control in a card row is as tall as the row.
export const CONTROL_HEIGHT = 26;
export const RADIUS_XS = 4;
export const RADIUS_CONTROL = 6;
export const RADIUS_CARD = 10;
// The select's chevron, drawn in `label`.
export const SELECT_CHEVRON = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23b4b9bd' stroke-width='1.5'/%3E%3C/svg%3E")`;
+19 -9
View File
@@ -95,8 +95,9 @@ export const NOTE_KEY = "ausboss_mask_by_touch";
const TOOLS_HEIGHT = 24;
const BAR_HEIGHT = 20;
const STAGE_MIN = 150;
// Wide enough for the bar under the picture: the arrows, a pick's name, Subject and Background.
export const PANEL_MIN_WIDTH = 400;
// Wide enough for the tools over the picture (Reload, Undo, Clear and the eye
// included) and the bar under it: the arrows, a pick's name, Subject and Background.
export const PANEL_MIN_WIDTH = 428;
// What the panel's element needs at its smallest: tools, stage and bar, two
// gaps and the padding under them.
const CHIPS_HEIGHT = 22;
@@ -104,9 +105,9 @@ const CHIPS_HEIGHT = 22;
const CHIP_ROWS = 3;
// A node this wide has room for the names in front of the brush's two
// numbers. A narrower one shows the numbers alone.
const LABELS_FROM = 440;
const LABELS_FROM = 470;
// The width a new node opens with: the names fit.
export const OPENING_WIDTH = 460;
export const OPENING_WIDTH = 490;
const PANEL_NEEDS = TOOLS_HEIGHT + STAGE_MIN + BAR_HEIGHT + CHIPS_HEIGHT + 12 + 6;
// Saved with the workflow: the brush size once it was set by hand, the snap
// switch, and how the picked area is shown.
@@ -134,6 +135,8 @@ const EYE_TITLES = {
// The eye beside Undo and Clear, drawn here: an almond and its pupil.
const EYE_ICON = '<svg width="16" height="16" viewBox="-8 -8 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true">'
+ '<path d="M-6.6 0Q0 -6 6.6 0Q0 6 -6.6 0Z"/><circle r="2" fill="currentColor" stroke="none"/></svg>';
const RELOAD_ICON = '<svg width="14" height="14" viewBox="-8 -8 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
+ '<path d="M5.6 -2.4A6 6 0 1 0 5.2 3"/><path d="M6 -6.2V-2.2H2"/></svg>';
// The brush size, across the picture, in picture pixels.
const BRUSH_SIZE = [4, 1000];
// A brush mark counts as touching an area within this many pixels of the
@@ -241,7 +244,7 @@ function ensureCss() {
.ausboss-touch.narrow .ausboss-touch-tools{gap:3px;}
.ausboss-touch.narrow .ausboss-touch-field.size .ausboss-scrub{width:56px !important;}
.ausboss-touch.narrow .ausboss-touch-field.soft .ausboss-scrub{width:48px !important;}
.ausboss-touch-btn.eye{display:inline-flex;align-items:center;justify-content:center;width:26px;min-width:26px;padding:0;}
.ausboss-touch-btn.eye,.ausboss-touch-btn.reload{display:inline-flex;align-items:center;justify-content:center;width:26px;min-width:26px;padding:0;}
.ausboss-touch-btn.eye.on{border-color:${BRAND};background:${BRAND};color:#04201f;}
.ausboss-touch-btn.arrow{min-width:20px;width:20px;padding:0;font-size:14px;line-height:16px;}
.ausboss-touch-swatch{box-sizing:border-box;flex:none;width:${BAR_HEIGHT}px;height:${BAR_HEIGHT}px;border-radius:4px;background:#0b1113;border:1px solid rgba(0,180,170,.3);pointer-events:auto;}
@@ -742,7 +745,7 @@ function settings(face) {
grow: Math.round(number("grow")),
soften: Math.max(0, number("soften")),
smooth: Math.max(0, Math.round(number("smooth"))),
fill: widgetByName(face.node, "fill_holes")?.value !== false,
fill: widgetByName(face.node, "fill_holes")?.value === true,
invert: widgetByName(face.node, "invert")?.value === true,
fine: fineChoice(widgetByName(face.node, "fine_edges")?.value),
};
@@ -1403,6 +1406,7 @@ function syncTools(face) {
const waiting = live && face.pending.length > 0;
face.undo.disabled = !live || (!some && !face.past.length && !waiting);
face.clear.disabled = !some && !waiting;
face.reload.disabled = !live || face.inFlight;
if (face.lookButton) {
const offer = live && face.snap && !face.held && !face.inFlight && !face.pic.scoped && Boolean(face.host.lookOnButton?.());
face.lookButton.style.display = offer ? "" : "none";
@@ -2877,9 +2881,9 @@ export function mountTouchStage(node, host) {
};
readProperties(face);
const showPicture = () => { if (!face.inFlight) { face.retries = 0; if (face.pic) face.pic.paused = false; requestRun(face, "scope"); } };
face.start = button("ausboss-touch-start", "Show picture",
"Runs only this node, so the picture shows here and its pieces light up under the pointer.",
() => { if (!face.inFlight) { face.retries = 0; if (face.pic) face.pic.paused = false; requestRun(face, "scope"); } }, signal);
"Runs only this node, so the picture shows here and its pieces light up under the pointer.", showPicture, signal);
wake.append(face.start, hint);
// Tools: what a press does, the brush, undo and clear; on the right, how
@@ -2976,7 +2980,13 @@ export function mountTouchStage(node, host) {
if (event.shiftKey) face.setView(face.view === "cut" ? "mask" : face.view === "mask" ? "cut" : face.result === "cut" ? "mask" : "cut");
else face.setView(face.view === "tint" ? face.result : "tint");
}, { signal });
tools.append(face.toolSwitch.root, face.sizeField, face.softField, el("div", "ausboss-touch-gap"), face.undo, face.clear, face.eye);
// The picture coming in changed (an edit made upstream): this shows the
// new one, the same way Show picture does.
face.reload = button("ausboss-touch-btn reload", "",
"Reload the picture: runs only this node, so a picture that changed upstream shows here.", showPicture, signal);
face.reload.innerHTML = RELOAD_ICON;
face.reload.setAttribute("aria-label", "Reload the picture");
tools.append(face.toolSwitch.root, face.sizeField, face.softField, el("div", "ausboss-touch-gap"), face.reload, face.undo, face.clear, face.eye);
newButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
+45 -40
View File
@@ -30,11 +30,12 @@
// Pure decisions (which rows show, how tall the card is, how a number
// scrubs, where a socket sits) live in widget_card_math.mjs for node:test;
// this file is the DOM.
import { BRAND, chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "./index.mjs";
import { chainCallback, keepDomWidgetWidthAuto, notifyAusbossChange } from "./index.mjs";
import { keepKeyInField } from "./canvas_passthrough.mjs";
import { createMediaPicker } from "./media_picker.mjs";
import { ensureNodeMinHeight, fillNodeHeight, holdNodeMinWidth, nodeHeightAfterCardChange, pinVuePanelHeight } from "./panel_layout.mjs";
import { makeScrubInput, quantizeScrubValue, stepDecimals } from "./scrub_input.mjs";
import { CONTROL_HEIGHT, FONT_UI, RADIUS_CARD, RADIUS_CONTROL, RADIUS_XS, SELECT_CHEVRON, THEME as T } from "./theme.mjs";
import { limitsLabel, narrowedRange } from "./wired_limits.mjs";
import { hideWidget } from "./widget_visibility.mjs";
import {
@@ -55,7 +56,7 @@ const WRAPPER_INSET = 16;
// widget's layout y (DomWidgets: margin 10 per side).
const WRAPPER_TOP = 10;
const FONT = `-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif`;
const FONT = FONT_UI;
// A boolean pill's state labels must fit half a control; longer text is a
// tooltip, and the pill says on / off.
const SWITCH_LABEL_MAX = 16;
@@ -78,76 +79,80 @@ export function ensureCardCss() {
const style = document.createElement("style");
style.id = CSS_ID;
style.textContent = `
.ausboss-card{position:relative;box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:${ROW_GAP}px;padding:${CARD_PADDING}px 8px;overflow:hidden;border:1px solid rgba(0,180,170,.22);border-radius:8px;background:rgba(0,0,0,.28);font:12px/1.3 ${FONT};color:#c8dddd;pointer-events:none}
.ausboss-card{position:relative;box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;gap:${ROW_GAP}px;padding:${CARD_PADDING}px 8px;overflow:hidden;border:1px solid transparent;border-radius:${RADIUS_CARD}px;background:${T.card};font:12px/1.3 ${FONT};color:${T.inkSoft};pointer-events:none}
.ausboss-card-row{display:grid;grid-template-columns:minmax(56px,30%) 1fr;align-items:center;gap:8px;height:${ROW_HEIGHT}px;flex:none;pointer-events:auto}
.ausboss-card-row.hidden{display:none}
.ausboss-card-row.linked{opacity:.5}
.ausboss-card-row.linked .ausboss-card-control{pointer-events:none}
.ausboss-card-row.muted .ausboss-card-control{opacity:.35;pointer-events:none}
.ausboss-card-row.muted .ausboss-card-label{color:#566b69}
.ausboss-card-range{margin-left:6px;color:#5f7674;font-size:10px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
.ausboss-card-row.muted .ausboss-card-label{color:${T.faint}}
.ausboss-card-range{margin-left:6px;color:${T.faint};font-size:10px;font-weight:600;letter-spacing:.02em;white-space:nowrap}
.ausboss-card.has-corner .ausboss-card-control{margin-right:var(--ausboss-card-corner,0px)}
.ausboss-card-corner{position:absolute;top:${CARD_PADDING}px;right:8px;height:${ROW_HEIGHT}px;display:flex;align-items:center;justify-content:flex-end;gap:5px;pointer-events:auto}
.ausboss-card-gear{width:24px;height:24px;flex:none;display:grid;place-items:center;padding:0;border:1px solid #2a3437;border-radius:6px;background:#0f1516;color:#8ba3a1;cursor:pointer}
.ausboss-card-gear:hover{border-color:${BRAND};color:${BRAND}}
.ausboss-card-chip{height:20px;flex:none;padding:0 8px;border:1px solid rgba(0,180,170,.5);border-radius:10px;background:rgba(0,180,170,.14);color:${BRAND};font:600 10px/1 ${FONT};letter-spacing:.02em;white-space:nowrap;cursor:pointer}
.ausboss-card-chip:hover{background:rgba(0,180,170,.24);color:#fff}
.ausboss-card-gear{width:24px;height:24px;flex:none;display:grid;place-items:center;padding:0;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};color:${T.label};cursor:pointer}
.ausboss-card-gear:hover{background:${T.fillHover};color:${T.inkHover}}
.ausboss-card-chip{height:20px;flex:none;padding:0 9px;border:none;border-radius:999px;background:${T.brandSoft};color:${T.brandInk};font:600 10px/1 ${FONT};letter-spacing:.02em;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer}
.ausboss-card-chip:hover{background:${T.brandSoftHover};color:${T.inkHover}}
.ausboss-card-chip[hidden]{display:none}
.ausboss-card-row.area{display:block}
.ausboss-card-row.panel{display:block}
.ausboss-card-row.area.grow{flex:1 1 auto;height:auto}
.ausboss-card-label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:#8ba3a1;font-size:11px;user-select:none}
.ausboss-card-label{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:${T.label};font-size:11px;user-select:none}
.ausboss-card-control{display:flex;align-items:center;gap:6px;min-width:0;height:100%}
.ausboss-card-field{display:flex;align-items:center;gap:6px;flex:1 1 0;min-width:0;height:100%}
.ausboss-card-field.linked{opacity:.5;pointer-events:none}
.ausboss-card .ausboss-scrub{height:${CONTROL_HEIGHT}px}
.ausboss-card-field>.ausboss-scrub{flex:1 1 0;width:auto;min-width:0}
.ausboss-card-field>.ausboss-scrub .ausboss-scrub-input{width:100%}
.ausboss-card-sep{flex:none;color:#5f7674;font-size:11px}
.ausboss-card-suffix{flex:none;color:#5f7674;font-size:10px;letter-spacing:.06em;text-transform:uppercase;user-select:none}
.ausboss-card-section{height:${SECTION_HEIGHT}px;flex:none;display:flex;align-items:flex-end;color:#5f7674;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;user-select:none}
.ausboss-card-sep{flex:none;color:${T.faint};font-size:11px}
.ausboss-card-suffix{flex:none;color:${T.faint};font:600 10px ${FONT};letter-spacing:.1em;text-transform:uppercase;user-select:none}
.ausboss-card-section{height:${SECTION_HEIGHT}px;flex:none;display:flex;align-items:flex-end;gap:8px;color:${T.faint};font:600 10px/1 ${FONT};letter-spacing:.1em;text-transform:uppercase;user-select:none}
.ausboss-card-section::after{content:"";flex:1;height:1px;margin-bottom:4px;background:${T.rule}}
.ausboss-card-section.hidden{display:none}
.ausboss-card-group{height:${GROUP_HEIGHT}px;flex:none;display:flex;align-items:center;gap:6px;padding:0 4px;border:1px solid transparent;border-radius:6px;background:transparent;color:#78908e;font:600 10.5px/1 ${FONT};letter-spacing:.06em;text-transform:uppercase;cursor:pointer;text-align:left;pointer-events:auto}
.ausboss-card-group:hover{color:#fff;border-color:#2a3437}
.ausboss-card-group{height:${GROUP_HEIGHT}px;flex:none;display:flex;align-items:center;gap:6px;padding:0 4px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:transparent;color:${T.faint};font:600 10px/1 ${FONT};letter-spacing:.1em;text-transform:uppercase;cursor:pointer;text-align:left;pointer-events:auto}
.ausboss-card-group:hover{color:${T.inkHover}}
.ausboss-card-group .glyph{display:inline-block;width:10px;transition:transform .12s}
.ausboss-card-group.open .glyph{transform:rotate(90deg)}
.ausboss-card-seg{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;width:100%;height:${ROW_HEIGHT}px;padding:2px;box-sizing:border-box;border:1px solid #2a3437;border-radius:6px;background:#0f1516}
.ausboss-card-seg button{min-width:0;border:none;border-radius:4px;padding:0 2px;background:transparent;color:#8ba3a1;font:600 10.5px/1 ${FONT};cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ausboss-card-seg button:hover{color:#fff}
.ausboss-card-seg button.on{background:${BRAND};color:#04201d}
.ausboss-card-select{width:100%;height:${ROW_HEIGHT}px;padding:0 24px 0 8px;box-sizing:border-box;border:1px solid #2a3437;border-radius:6px;background:#0b0f10 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238ba3a1' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 8px center;color:#d8ecea;font:600 11.5px ${FONT};appearance:none;-webkit-appearance:none;cursor:pointer;overflow:hidden;text-overflow:ellipsis}
.ausboss-card-select:hover{border-color:${BRAND}}
.ausboss-card-select:focus-visible{outline:2px solid ${BRAND};outline-offset:1px}
.ausboss-card-text{width:100%;height:${ROW_HEIGHT}px;padding:0 8px;box-sizing:border-box;border:1px solid #2a3437;border-radius:6px;background:#0b0f10;color:#d8ecea;font:12px ${FONT};min-width:0}
.ausboss-card-text:focus{outline:none;border-color:${BRAND}}
.ausboss-card-text::placeholder{color:#4f6462}
.ausboss-card-area{display:block;width:100%;height:100%;padding:5px 8px;box-sizing:border-box;border:1px solid #2a3437;border-radius:6px;background:#0b0f10;color:#d8ecea;font:12px/1.4 ${FONT};resize:none;overflow:auto;min-width:0}
.ausboss-card-area:focus{outline:none;border-color:${BRAND}}
.ausboss-card-area::placeholder{color:#4f6462}
.ausboss-card.look-big .ausboss-card-row.area .ausboss-card-field{position:relative;display:block;border:1px solid #2a3437;border-radius:6px;background:#0b0f10;cursor:text}
.ausboss-card.look-big .ausboss-card-row.area .ausboss-card-field:focus-within{border-color:${BRAND}}
.ausboss-card.look-big .ausboss-card-area{position:absolute;left:0;right:0;top:0;bottom:0;margin:auto 0;max-height:100%;padding:${BIG_PAD_Y}px ${BIG_PAD_X}px;border:none;border-radius:5px;background:transparent;color:#6fe0d6;text-shadow:0 0 .35em rgba(0,180,170,.55);font:600 ${BIG_FONT}px/${BIG_LINE}px ${FONT};text-align:center;scrollbar-width:thin;scrollbar-color:#2f3d3c transparent}
.ausboss-card.look-big .ausboss-card-area::placeholder{color:#4f6462;text-shadow:none}
.ausboss-card-seg{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;width:100%;height:${ROW_HEIGHT}px;padding:2px;box-sizing:border-box;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill}}
.ausboss-card-seg button{min-width:0;border:none;border-radius:${RADIUS_XS}px;padding:0 2px;background:transparent;color:${T.label};font:600 11px/1 ${FONT};cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ausboss-card-seg button:hover{color:${T.inkHover}}
.ausboss-card-seg button.on{background:${T.brand};color:${T.onBrand}}
.ausboss-card-bool button.on:first-child{background:${T.switchOff};color:${T.ink}}
.ausboss-card-select{width:100%;height:${ROW_HEIGHT}px;padding:0 24px 0 8px;box-sizing:border-box;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill} ${SELECT_CHEVRON} no-repeat right 9px center;color:${T.ink};font:500 12px ${FONT};appearance:none;-webkit-appearance:none;cursor:pointer;overflow:hidden;text-overflow:ellipsis}
.ausboss-card-select:hover{background-color:${T.fillHover}}
.ausboss-card-select:focus-visible{outline:2px solid ${T.brand};outline-offset:1px}
.ausboss-card-text{width:100%;height:${ROW_HEIGHT}px;padding:0 8px;box-sizing:border-box;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};color:${T.ink};font:12px ${FONT};min-width:0}
.ausboss-card-text:focus{outline:none;border-color:${T.brand}}
.ausboss-card-text::placeholder{color:${T.faint}}
.ausboss-card-area{display:block;width:100%;height:100%;padding:5px 8px;box-sizing:border-box;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};color:${T.ink};font:12px/1.4 ${FONT};resize:none;overflow:auto;min-width:0}
.ausboss-card-area:focus{outline:none;border-color:${T.brand}}
.ausboss-card-area::placeholder{color:${T.faint}}
.ausboss-card.look-big .ausboss-card-row.area .ausboss-card-field{position:relative;display:block;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};cursor:text}
.ausboss-card.look-big .ausboss-card-row.area .ausboss-card-field:focus-within{border-color:${T.brand}}
.ausboss-card.look-big .ausboss-card-area{position:absolute;left:0;right:0;top:0;bottom:0;margin:auto 0;max-height:100%;padding:${BIG_PAD_Y}px ${BIG_PAD_X}px;border:none;border-radius:5px;background:transparent;color:${T.brandGlow};text-shadow:0 0 .35em rgba(0,180,170,.55);font:600 ${BIG_FONT}px/${BIG_LINE}px ${FONT};text-align:center;scrollbar-width:thin;scrollbar-color:${T.fillHover} transparent}
.ausboss-card.look-big .ausboss-card-area::placeholder{color:${T.faint};text-shadow:none}
.ausboss-card.look-big .ausboss-card-row:not(.area){display:block}
.ausboss-card.look-big .ausboss-card-row:not(.area) .ausboss-card-label{display:none}
.ausboss-card.look-big .ausboss-card-row:not(.area) .ausboss-card-control{margin:0}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub{height:100%;justify-content:center;align-items:center;gap:4px;border-color:#2a3437;border-radius:6px;background:#0b0f10}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub:focus-within{border-color:${BRAND}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-input{order:1;flex:0 0 auto;width:auto;min-width:2ch;max-width:60%;field-sizing:content;color:#6fe0d6;text-shadow:0 0 .35em rgba(0,180,170,.55);font:600 ${BIG_NUMBER_FONT}px ${FONT}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub{height:100%;justify-content:center;align-items:center;gap:4px;border-color:transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub:focus-within{border-color:${T.brand}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-input{order:1;flex:0 0 auto;width:auto;min-width:2ch;max-width:60%;field-sizing:content;color:${T.brandGlow};text-shadow:0 0 .35em rgba(0,180,170,.55);font:600 ${BIG_NUMBER_FONT}px ${FONT}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-unit{order:2}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-unit:empty{display:none}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step{display:contents}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step>button{flex:none;width:30px;height:30px;border-radius:15px;color:#6f8886}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step>button{flex:none;width:30px;height:30px;border-radius:15px;color:${T.label}}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step>button:first-child{order:3}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step>button:last-child{order:0}
.ausboss-card.look-big .ausboss-card-field>.ausboss-scrub>.ausboss-scrub-step>button>svg{transform:rotate(90deg) scale(1.6)}
.ausboss-card.look-big .ausboss-card-corner{top:${CARD_PADDING + 4}px;right:12px;height:22px}
.ausboss-card.look-big .ausboss-card-gear{width:22px;height:22px;border-color:transparent;background:transparent}
.ausboss-card.look-big .ausboss-card-gear:hover{border-color:${BRAND}}
.ausboss-card-swatch{flex:none;width:${ROW_HEIGHT}px;height:${ROW_HEIGHT}px;padding:0;border:1px solid #2a3437;border-radius:6px;background:#0b0f10;cursor:pointer}
.ausboss-card.look-big .ausboss-card-gear:hover{background:${T.fillHover}}
.ausboss-card-swatch{flex:none;width:${ROW_HEIGHT}px;height:${ROW_HEIGHT}px;padding:0;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};cursor:pointer}
.ausboss-card-swatch::-webkit-color-swatch-wrapper{padding:3px}
.ausboss-card-swatch::-webkit-color-swatch{border:none;border-radius:3px}
.ausboss-card-btn{flex:none;height:${ROW_HEIGHT}px;padding:0 10px;border:1px solid #2a3437;border-radius:6px;background:#16201f;color:#d8ecea;font:600 11px/1 ${FONT};cursor:pointer;white-space:nowrap}
.ausboss-card-btn:hover{border-color:${BRAND};color:#fff}
.ausboss-card-btn{flex:none;height:${ROW_HEIGHT}px;padding:0 10px;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};color:${T.inkSoft};font:600 11px/1 ${FONT};cursor:pointer;white-space:nowrap}
.ausboss-card-btn:hover{background:${T.fillHover};color:${T.inkHover}}
.ausboss-card-btn:focus-visible{outline:2px solid ${T.brand};outline-offset:1px}
`;
document.head.append(style);
}
+3 -2
View File
@@ -14,6 +14,7 @@ import { castChips } from "../shared/h3_cast_chips.mjs";
import { clipSetsDuration } from "../shared/h3_duration.mjs";
import { momentStrip } from "../shared/moment_strip.mjs";
import { mountWidgetCard } from "../shared/widget_card.mjs";
import { FONT_UI, THEME } from "../shared/theme.mjs";
import { SOCKETLESS_CHOICE, SOCKETLESS_TEXT, commitWidgetValue, markSocketless, resetUnknownValues } from "../shared/widget_card_math.mjs";
import { mediaViewQuery } from "../shared/media_list.mjs";
import { gearIconSvg, openSettingsMenu } from "../shared/settings_menu.mjs";
@@ -59,7 +60,7 @@ function seamCorner(node, card) {
const note = document.createElement("div");
note.append("not used", document.createElement("br"), "by blend in");
note.style.cssText = "position:absolute;top:33px;right:2px;text-align:right;white-space:nowrap;"
+ "color:#6f8886;font:9px/11px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
+ `color:${THEME.faint};font:9px/11px ${FONT_UI};`
+ "pointer-events:none";
tools.append(chip, gear, note);
return {
@@ -416,7 +417,7 @@ const CARDS = {
],
},
AUSBOSS_NODES_SaveVideo: {
// step_preview is the STEPS button on the player (js/save_video).
// step_preview is the Preview steps switch on the player (js/save_video).
minWidth: 320, first: true, hide: ["step_preview"],
rows: [
{ widget: "filename_prefix", label: "Prefix", placeholder: "video" },
+11 -1
View File
@@ -340,7 +340,9 @@ def write_prompt(script, seconds=15.0, camera="", look="", sound="", language="E
cast.clip_sound = True
# With no words typed, the clip's own speech is still heard, so the new person says it as the video does.
cast.typed_lines = any(moment.speech for moment in scene.moments)
cast.speaks_as_video = bool(speech) and not cast.typed_lines
# Only a voice makes them speak: a train or a beat is loud in the voice band too, but has no clear pitch,
# and the line made the new person mouth along to it (subway clip, 2026-11-07: 0.0 s of clear voice).
cast.speaks_as_video = heard_a_voice(speech) and not cast.typed_lines
time_from_sound(scene.moments, speech)
scene.moments = [moment for moment in scene.moments if not (moment.voice or moment.other)]
if not scene.moments:
@@ -438,6 +440,14 @@ LONGEST_LINE = 12 # Stretches of speech one line can take.
GAP_WEIGHT, INNER_GAP_WEIGHT, NOISE_WEIGHT, PITCH_WEIGHT = 1.0, 1.0, 1.5, 6.0
SPEAKING_VOICE = 0.5 # Seconds of clear voice before the clip counts as someone talking.
def heard_a_voice(speech):
"""Whether find_speech heard someone talk: enough clear voice, not just loud sound in the voice band."""
return sum(stretch[3] for stretch in speech or ()) >= SPEAKING_VOICE
def find_speech(samples, rate):
"""[(start, end, pitch, voiced), ...]: the stretches of a mono track loud
enough in the voice band to be words, in seconds, with each stretch's
+35 -9
View File
@@ -212,19 +212,19 @@ def _server():
return None
def _tell(node_id, note: dict, end: bool) -> bool:
def _tell(node_id, note: dict, end: bool, stopped: bool = False) -> bool:
"""Tell the node's face what a look has found, over ComfyUI's own channel
to its page. It goes to the page that asked for the run. True when it
was sent; a failure never stops the look."""
was sent; a failure never stops the look. `stopped`: the run stops at
this node, and the face is only shown the picture that came in."""
server = _server()
if server is None or node_id is None:
return False
event = {"node": str(node_id), "note": note, "end": bool(end)}
if stopped:
event["stopped"] = True
try:
server.send_sync(
LOOK_EVENT,
{"node": str(node_id), "note": note, "end": bool(end)},
getattr(server, "client_id", None),
)
server.send_sync(LOOK_EVENT, event, getattr(server, "client_id", None))
except Exception: # noqa: BLE001
return False
return True
@@ -720,10 +720,10 @@ class AusBossMaskByTouch:
"fill_holes": (
"BOOLEAN",
{
"default": True,
"default": False,
"tooltip": (
"Closes pinholes inside the picked area. Larger openings "
"stay open, like a mug's handle."
"stay open, like a mug's handle. Off keeps every hole you pick."
),
},
),
@@ -1105,6 +1105,28 @@ class AusBossMaskByTouch:
entry = self._keep(("picture", key), self._write(picture, "picture", "jpg", format="JPEG", quality=90))
return entry
def _show_picture(self, session, image, key: str, unique_id) -> bool:
"""Send the face the picture that came in, with nothing picked on it,
for a run that stops at this node. A failure never changes why the
run stops."""
if unique_id is None or _server() is None:
return False
try:
height, width = int(image.shape[1]), int(image.shape[2])
small_width, small_height, scale = small_size(width, height)
picture = self._picture_file(image, key, small_width, small_height)
if picture is None:
return False
note = {
"key": key, "width": width, "height": height, "scale": scale, "stamp": "",
"picture": picture, "selection": None, "items": [], "share": 0.0,
"pieces": self._pieces_note(_MEMORY.get_pieces((session.token, session.reader, key))),
"fine": None, "areas": None,
}
except Exception: # noqa: BLE001
return False
return _tell(unique_id, note, True, stopped=True)
def _pieces_note(self, pieces):
if pieces is None:
return None
@@ -1239,6 +1261,10 @@ class AusBossMaskByTouch:
raw = torch.stack(selections)
soft = raw.dtype != torch.bool
if not bool(raw.any()) and not asked and str(if_nothing) != IF_NOTHING[1]:
# The run stops here, but the face is shown the picture that
# came in first: a picture that changed upstream is there to
# pick from at once, without unplugging it.
self._show_picture(session, image, key, unique_id)
# Picks the face may still show, made before the picture
# changed: say so, not that nothing is picked.
raise RuntimeError(OTHER_PICTURE if elsewhere else NOTHING_PICKED)
+1 -1
View File
@@ -3,7 +3,7 @@ name = "ausboss-nodes"
# Manager/registry search matches against this text, so it names the actual
# nodes instead of describing the collection abstractly.
description = "ComfyUI-AusBoss (AusBoss nodes): image and video nodes with compact controls: Image and Video Crop + Rotate + Pad (frame or clip), Load Image + Pad, Image Folder, Image Resize, Align Image, Image Size, Color Match, Image Compare A/B, Realign to Source (experimental), Save Image (PNG, WebP/JXL, captions), Load Video, Load Audio, Save Video, Select Frame, Frame Interpolate, Mask Refine, Mask by Name and Mask by Touch (type a name or click a thing, get its mask), LaMa Inpaint, Crop For Inpaint, Stitch Inpaint, Tiled Upscale and Tiled Upscale Stitch, LoRA Loader, Krea 2 Encode and Outpaint Model Patch, Latent Size, Layout Boxes, Seed, Select Every Nth, Split Batch, Merge Batches, Text, H3 Prompt Writer, Integer, Float, Math Expression, Show Text, Free Memory, Workflow Note, Workflow Note Tile, Callout, Callout Tile, Workflow Switches, Run Timer, and Step Preview (stop bad runs). Example workflows for Qwen Image 2.1, Krea 2, Klein 9B and LTX 2.3."
version = "2.21.2"
version = "2.22.0"
# comfy-cli serializes this to JSON verbatim, so `file` publishes the literal
# string {"file": "LICENSE"} to the registry listing. `text` puts a readable
# name there instead. Both forms are supported; the LICENSE file is unchanged.
+10
View File
@@ -1478,6 +1478,16 @@ class ClipSoundTests(unittest.TestCase):
self.assertNotIn("lips", line)
self.assertNotIn("speaks as in", line)
def test_loud_sound_with_no_voice_does_not_make_them_speak(self):
# The subway clip: one long stretch of train noise, loud in the voice band, no clear pitch.
noise = heard([(1.1, 5.42, None, 0.0)])
line = parts_of("@1 replaces the woman on the platform.", clip_sound=True, speech=noise, **SWAP_JOBS)["detailed_description"]
self.assertNotIn("speaks as in", line)
self.assertNotIn("lips", line)
a_word = heard([(1.0, 1.3, 150.0, 0.2)]) # A cough or one short sound: under half a second of voice.
line = parts_of("@1 replaces the woman on the platform.", clip_sound=True, speech=a_word, **SWAP_JOBS)["detailed_description"]
self.assertNotIn("speaks as in", line)
def test_a_typed_sound_wins_over_the_clips_sound(self):
parts = parts_of("@1 replaces the man in the hat.\nsound: rain on the roof", clip_sound=True, speech=plain([(0.4, 1.9)]), **SWAP_JOBS)
self.assertIn("rain on the roof", parts["overall_soundscape"].lower())
+31 -1
View File
@@ -1775,6 +1775,36 @@ class TellTests(TouchCase):
self.swap("_server", lambda: None)
self.assertEqual(self.note(self.run_node("", ask="scope", unique_id=7))["pieces"]["list"], told["pieces"]["list"])
def test_a_run_that_stops_still_shows_the_face_the_picture_that_came_in(self):
# Issue 301: the picture changed upstream, the run stopped, and the
# face kept the old one until the image was unplugged and plugged in.
other = touches(tap(75, 34), key="160x120-0000000000000000")
for text, message in ((other, OTHER_PICTURE), ("", NOTHING_PICKED)):
self.sent.clear()
with self.assertRaises(RuntimeError) as stopped:
self.run_node(text, unique_id=7)
self.assertEqual(str(stopped.exception), message, "the run still stops, for the same reason")
[(event, data, sid)] = self.sent
self.assertEqual((event, data["node"], data["end"], data["stopped"], sid), (node_module.LOOK_EVENT, "7", True, True, "the page that asked"))
note = data["note"]
self.assertEqual((note["key"], note["width"], note["height"]), (picture_key(self.picture), W, H))
self.assertEqual((note["items"], note["selection"], note["stamp"]), ([], None, ""), "nothing picked on it")
self.assertEqual(self.opened(note["picture"]).size[0] > 0, True)
self.assertEqual(FakeSam.calls, [], "showing the picture asks SAM 3 nothing")
def test_a_stop_that_cannot_tell_the_face_stops_for_the_same_reason(self):
def broken(*_args):
raise ConnectionError("the page is gone")
self.server.send_sync = broken
with self.assertRaises(RuntimeError) as stopped:
self.run_node("", unique_id=7)
self.assertEqual(str(stopped.exception), NOTHING_PICKED)
self.server.send_sync = lambda *args: self.sent.append(args)
with self.assertRaises(RuntimeError):
self.run_node("")
self.assertEqual(self.sent, [], "no id: nobody to tell")
def test_a_page_that_cannot_be_told_never_stops_the_look(self):
def broken(*_args):
raise ConnectionError("the page is gone")
@@ -2048,7 +2078,7 @@ class ContractTests(unittest.TestCase):
self.assertEqual((required["soften"][0], soften["default"], soften["min"], soften["max"], soften["step"]), ("FLOAT", 0.0, 0.0, 100.0, 0.1))
smooth = required["smooth"][1]
self.assertEqual((required["smooth"][0], smooth["default"], smooth["min"], smooth["max"]), ("INT", 0, 0, 64))
self.assertEqual((required["fill_holes"][0], required["fill_holes"][1]["default"]), ("BOOLEAN", True))
self.assertEqual((required["fill_holes"][0], required["fill_holes"][1]["default"]), ("BOOLEAN", False))
self.assertEqual((required["if_nothing"][0], required["if_nothing"][1]["default"]), (list(IF_NOTHING), "stop the run"))
self.assertEqual((optional["ask"][0], optional["ask"][1]["default"]), ("STRING", ""))
self.assertEqual((optional["invert"][0], optional["invert"][1]["default"]), ("BOOLEAN", False))
+43
View File
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { THEME } from "../js/shared/theme.mjs";
// The card, the number box, the switch and the picture bar take every colour
// from theme.mjs, so the palette is changed in one place.
const ON_THEME = [
"js/shared/widget_card.mjs", "js/shared/scrub_input.mjs", "js/shared/slide_switch.mjs",
"js/input_preview/index.js", "js/shared/panel_controls.mjs", "js/shared/picture_stepper.mjs",
"js/shared/step_preview.mjs",
];
test("the card, number box, switch and picture bar carry no colour of their own", () => {
for (const file of ON_THEME) {
const source = readFileSync(new URL(`../${file}`, import.meta.url), "utf8");
const hex = source.match(/#[0-9a-fA-F]{3,8}\b/g) ?? [];
assert.deepEqual(hex, [], `${file} has colour literals; add them to js/shared/theme.mjs`);
}
});
const luminance = (hex) => {
const channels = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
};
const contrast = (a, b) => {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
test("text a person reads holds 4.5:1 on the card and on a control", () => {
const pairs = [
["ink", "fill"], ["inkSoft", "card"], ["label", "card"], ["label", "fill"],
["faint", "card"], ["faint", "fill"], ["onBrand", "brand"], ["ink", "switchOff"], ["warnInk", "warnSoft"],
["dangerInk", "dangerSoft"], ["faint", "well"], ["inkSoft", "fill"],
];
for (const [text, ground] of pairs) {
const ratio = contrast(THEME[text], THEME[ground]);
assert.ok(ratio >= 4.5, `${text} on ${ground} is ${ratio.toFixed(2)}:1`);
}
});