The Angelo — Overrides node now carries an optional second model stack
(gen_model / gen_positive / gen_negative / gen_vae connection inputs
plus gen_steps / gen_cfg / gen_sampler_name / gen_scheduler settings).
When wired, Sampler Mode generates the base with the gen stack, decodes
with the gen VAE, and re-encodes the pixels with the main edit VAE so
the whole editing session runs on the edit model — generate with FLUX
Krea / SDXL for the look, click-to-edit with Klein / Qwen-Image-Edit.
Behaviour:
- Edit Mode never reads the gen bundle; unwired = byte-identical to 2.5
- Seed + Smpl Denoise stay on the toolbar (seed controls keep working;
denoise < 1.0 = img2img with the gen model, incl. off a loaded image)
- gen_negative optional: falls back to a zeroed gen_positive
(ConditioningZeroOut maths)
- Preview/history pixels are the gen VAE decode (not the edit-VAE
round-trip); base pixels cropped to /16 for any edit VAE ratio
- Cached-base / loaded-image Sampler runs derive the gen latent by
encoding the base pixels with the gen VAE
- guider/sampler/sigmas trio deliberately not applied to the gen model
(a wired GUIDER wraps the edit model's conds)
Hardening from the bug-check pass (2 inline + 3 agent-review findings):
- gen widgets live in optional inputs, not required: ComfyUI validates
required inputs server-side BEFORE Python defaults apply, which would
have rejected every pre-2.6 API-format export of the Overrides node
- explicit None checks in overrides parsing (gen_cfg 0.0 is falsy and
an `or` fallback silently sampled at the default)
- partial gen-bundle wiring (any subset incl. gen_negative-only) raises
a clear ValueError in Sampler Mode instead of silently falling back
- wired-latent guard: wrong-channel latents raise a clear not-in-the-
gen-model's-latent-space error; 5D video latents into a 4D gen model
get their singleton frame axis squeezed instead of crashing deep in
the UNet
- console prints the gen resolution each run (an empty latent authored
for a 16x edit VAE silently means half-size through an 8x gen VAE —
undetectable from the latent alone, so make it visible)
Fingerprint stays edit-model-fixed so Edit Mode never spuriously
resets; _STATE untouched until the whole gen chain succeeds
(cancel/OOM safe).
Also: example_workflows/Krea_plus_Klein-workflow.json (FLUX Krea 2 +
turbo LoRA at 1504x1504 through the Wan 2.1 VAE, Klein 9B editing) and
full README coverage (Quick start section, Overrides wiring rules,
Krea 2 + Wan 2.1 VAE pairing note).
New:
- ▶ Queue button beside the Mode dropdown — the in-node twin of
ComfyUI's Queue button (same code path, so Persistent Mask re-runs
work identically) usable from both modes and fullscreen.
- Re-roll after ✨ Quick Photo Refine now re-runs the ✨ pass (same as
pressing ✨ again) instead of silently corrupting the session.
- Outpaint Accept shows the approved canvas instantly (optimistic swap
from the already-loaded review image; commit lands in the background,
and image_w/h update immediately so edits in the gap map correctly).
- One-shot migration for v2.0.0–v2.1.0 workflow saves: the lir_seq
widget removed in 2.2.0 shifted positional widgets_values, corrupting
quick_prompt_mode/shrink_seq and failing Queue validation. Detected
semantically (a number on a combo), repaired on load, self-deactivates
on the next save.
Fixed (three-agent audit of backend flows, frontend flows, and the
JS↔Python contract):
- Reset was a silent no-op with Persistent Mask ON (queue hook cleared
the reset flag pre-serialization; hook now skips reset-pending nodes).
- Internal action queues bumped EVERY persistent-mask node — editing
node A re-ran node B's held mask. Initiator-scoped now.
- Re-roll / repeat-✨ popped history before sampling: a Cancel/OOM
permanently lost the last result. Both are now sample-first,
replace-on-success.
- Re-roll/Vary after Undo/Redo/✨ re-ran stale mask widgets (worst
case: a bogus corner-circle edit from click_x=-1). History entries
now carry provenance; Re-roll/Vary gate on it.
- Reset after _HISTORY_CAP eviction rebased onto a mid-session image
and overwrote the session base. Base fallbacks prefer source_latent.
- FLUX 2 large-canvas decodes missed the anti-NaN tiled path (threshold
assumed an 8x VAE); ratio now queried from the VAE.
- Vary chooser surviving an Undo could commit onto the wrong history
entry (stash cleared on undo/redo; JS closes the chooser).
- No-op edits (Remove without CLIP, empty mask) pushed junk history and
killed Redo.
- Esc was dead over the Vary chooser / Outpaint review (hover lost to
the overlay); Esc in a fullscreen popup exited fullscreen and
destroyed the popup (defer + rescue on exit + listener leak fixed).
- Remove's Xtra-Fine restore state now survives workflow reloads
(node.properties).
- Pan speed under graph zoom, context menu behind fullscreen,
Smpl Denoise 0.0 failing server validation, mode fallback order.
Smart Phrasing / Load Image / Shrink popups attach to document.body at
z-index 10000, but the fullscreen overlay is z-index 99990, so they
rendered behind it and were invisible until you exited fullscreen. Parent
these modals into the fullscreen overlay when it's active (it owns the
stacking context, so a fixed backdrop inside it still covers the viewport
and renders above the editor); unchanged outside fullscreen.
Paint over an object (or Detect / click it) and the edit model rebuilds
the background behind it. Avoids the usual 'ghost' by cutting the masked
region OUT of the reference the model anchors to (a real hole in the
reference latent), so it continues the surroundings instead of redrawing
the object — same idea as the Outpaint protect brush.
- Single full-denoise pass on the Xtra-Fine crop path (forced on;
respects Ctx Pad / MP / Method) so it rebuilds fine background detail.
- Fixed background-fill instruction; main/Area prompt and Denoise box
are ignored. Feather forced 0, mask auto-grows ~10%.
- Toggling Remove auto-enables Xtra-Fine and restores it on the way out.
- Edit models only; Refine mode only; mutually exclusive with Restore.
README: new 'Remove brush' section + feature bullets + toolbar row;
elevator pitch and registry description updated.
Pop the whole editor (toolbar + area-prompt box + canvas) into a
viewport-filling overlay for a much bigger canvas. The container is
reparented intact, so every click/paint/zoom/pan/detect/outpaint
handler keeps working — mapping via getBoundingClientRect at the new
size — and the ResizeObserver auto-refits the image.
- New '⛶ Fullscreen' toggle pinned top-left of the Mode row.
- Best-effort true browser fullscreen; falls back to a viewport overlay.
- Exit via the button, a floating '✕ Exit Fullscreen' button, or Esc.
- Layered Esc: first dismisses any open Outpaint/Vary/Detect review,
second leaves fullscreen.
- Tears down cleanly on node removal; restores container to its exact
DOM slot on exit.
README: new Fullscreen section + screenshot, feature bullet, and
keyboard-shortcut row. pyproject description refreshed (also drops the
stale 'Large Image Refine' mention).
Removed the ▦ Large Image Refine button entirely (constants, lir_seq widget,
handler, JS button + trigger + hide-list entry). In its place, a "Lite"
toggle sits beside the prompt selector in the Quick Actions bar.
Lite mode is the EXACT same Quick Photo Refine recipe (reference 1.0, 1.3MP
target, prompt selector, >1.6MP tiling with dual-grid seam-erase, re-roll-
from-original) with one change: denoise 0.49 instead of 1.0 — a lighter
restore that re-renders less and stays closer to the input. New quick_lite
BOOLEAN widget drives it; qr_denoise picks 0.49 vs 1.0 and feeds both the
tiled and whole-image paths.
Note: lir_seq was a released (v2.1.0) widget sitting before quick_prompt_mode;
removing it shifts quick_prompt_mode's positional slot, so a v2.1.0 saved
workflow with a NON-default prompt mode may reset it to "Identity + Quality"
on load. Clean cut per project convention; flagged for the release notes.
README still documents Large Image Refine (8 mentions) — needs updating
before release. Both files parse.
Dragging/loading a big image (e.g. 7680x2160) showed it visibly modified the
moment it loaded, while small images just loaded cleanly. Cause: a loaded
image is only VAE-ENCODED to a base latent (_encode_loaded_image returned
just the latent), so history[0] was (latent, None). With no stored pixels,
the preview path decodes the latent — i.e. shows the VAE ROUND-TRIP of the
file, not the file. That round-trip is near-lossless on small images (reads
as a plain load) but visibly lossy on large ones (and now tiled), so it
looked like Angelo edited the image on load.
Fix: _encode_loaded_image now also returns the original pixels; the load
reset stores them in history[0] and source_pixels. The preview path already
prefers current_pixels when present, so a freshly loaded image now shows
exactly as the file. Non-load resets still pass None (decode as before).
Bonus: the first edit after a load now builds on the real pixels rather than
the round-trip, and hold-to-compare / the source_image output show the true
file. No extra memory — the pixels tensor is shared across history[0],
source_pixels and current_pixels.
Large Image Refine (and any path that VAE-encodes/decodes a whole large
canvas) produced a fully black image with "invalid value encountered in
cast" on a 7744x2176 (16.8MP) input. Cause: _vae_encode/_vae_decode always
used the plain whole-canvas vae.encode/vae.decode. A single VAE pass at that
size OOMs or numerically overflows to NaN — silently (no exception) — so the
whole-canvas big_latent came back NaN, every tile cropped from it sampled to
NaN, and the blended pixels were NaN → np.clip leaves NaN → astype(uint8) =
black.
The tiling engine only ever tiled the SAMPLING; the whole-canvas VAE passes
(initial big_latent encode, final re-encode, the optional input decode) were
still single huge ops. Fix at the chokepoint: above _VAE_TILE_MP (4MP),
route through vae.encode_tiled / vae.decode_tiled (which also handle 5D
video VAEs). Below it, the plain exact/faster path as before — ~1.4k-px
sampling tiles and normal canvases are unaffected.
This also fixes the same latent bug for 2× Pixel and Xtra-Fine when their
output canvas is very large.
New Quick Actions button after Large Image Refine. Opens a popup showing the
current dimensions, a percent input with 75/50/33/25% quick presets, and a
LIVE "New: W × H" readout (snapped to a multiple of 16, computed with the
same formula the backend uses so it matches exactly). On confirm it sets
shrink_scale + bumps shrink_seq.
Backend downscales with AREA resampling (comfy common_upscale "area") — box
averaging, the right method for shrinking: it averages the discarded pixels
so it anti-aliases, unlike lanczos (great for UPscaling) which can ring on
heavy reduction. No AI; committed straight as a fresh session base
(dimension change = Load-Image semantics, history resets), mirroring 2× Pixel.
New widgets shrink_seq / shrink_scale declared LAST (positional-compat for
old workflows) and added to all wholesale state-reset dicts + the hide list.
Also refreshed the now-stale Large Image Refine tooltip/comment/toast (still
said "chess-pattern boxes" from before the seam fix → "overlapping tiled").
Python + JS parse. Untested on a live model — needs a click in ComfyUI.
Two seam fixes for the tiling paths:
1. Large Image Refine now routes through the shared overlapping tiled
engine (_tiled_restore_pass: feathered ramp blend + one shared noise
field) instead of the old exact-tile, hard-mask, chess-ordered boxes
with bit-exact latent compositing. The old design left seams because
the box boundaries were hard AND each box used a decorrelated seed
(lir_seed + k*9973), so texture statistics jumped at every edge. The
overlapping engine removes both causes. (option 1)
2. The engine gains a dual_grid mode: after the normal grid, a SECOND tile
grid offset by ~half a tile (starts at the midpoints of the first grid's
starts) is accumulated into the same weighted-blend buffers. The offset
tiles are centred on the first grid's seams, where the first grid's
tiles taper to low ramp weight — so the offset tiles dominate exactly
there, overwriting any residual seam with continuous tile interior, and
vice versa. Both grids crop the same absolute global noise field, so
their renders agree in shared regions and the blend stays sharp. Enabled
for BOTH Quick Photo Refine's >1.6MP path and Large Image Refine.
(option 4) Costs ~2× tiles.
Removed the now-unused _LIR_CTX_PAD / _LIR_BOX_MP constants and the box/
chess machinery; updated the comments. AST parses clean. Untested on a live
model — needs an A/B in ComfyUI.
README was stale on the upscale redesign — it still documented the deleted
2× Restore / 2× Upscale / face-polish buttons. Rewritten to match what
ships:
- Opening one-liner now mentions one-click photo refine + the 2× Pixel /
Large Image Refine upscale pair (the elevator pitch was missing them).
- Quick Photo Refine: the v2 recipe (whole image through Xtra-Fine, 1.3MP
target, ref 1.0 / denoise 1.0), the prompt selector (Identity + Quality /
Restore Photo / Identity + Colours / Use Area Prompt), and re-roll-from-
original semantics.
- The upscale pair: ⬆ 2× Pixel (pure lanczos, no AI, history resets) +
▦ Large Image Refine (chess-pattern Xtra-Fine boxes over the canvas).
- The load-small-at-high-MP -> Quick Photo Refine super-resolution
workflow (degrade-then-reconstruct on the edit model's own reference
branch, no extra models).
- Fine-print note that a whole-image semantic change runs as one global
pass, not tiled.
Also removed the ✨/✦ star glyphs from CASUAL PROSE sentences in the README
only. Button labels keep them (toolbar diagram, feature-table name,
restoration heading + bold lead) to match the UI. No code/UI changes —
angelo.js and angelo_nodes.py are untouched.
Two users reported the optional SAM 3 (Detect) installer wrecking their
ComfyUI install and then crashing at detect time. Root causes:
- sam3_requirements.txt pinned `numpy>=1.26,<2`, and the installer ran
pip WITHOUT a constraint, so pip downgraded ComfyUI's numpy 2.x to
1.26 to satisfy it -> broken ComfyUI. opencv-python (full) dragged it
down too. Fix: drop the numpy pin entirely and have the installer write
a constraints file pinning numpy to the version already present, passed
to every pip call, so a downgrade is impossible (fails loud if a dep
truly needs <2, instead of silently breaking the env).
- OpenCV: only install opencv-python-headless when cv2 isn't already
importable, so we never clobber a full opencv another node provides.
- pycocotools is on SAM 3.1's image import path now; added to requirements.
- SAM 3.1's perflib/fused.py::addmm_act returns bfloat16 unconditionally,
which collides with the fp32 weights Angelo builds SAM3 with
("mat1 and mat2 must have the same dtype, BFloat16 and Float"). New
sam3_postinstall.py idempotently patches addmm_act to cast its result
back to the caller's dtype. vitdet.py binds the symbol at import time so
a runtime monkey-patch can't reach it - the file on disk must be edited.
The installer runs the patcher on both the fresh-install and the
already-installed paths, so existing installs get fixed on a re-run.
Gold tip box in the load dialog: low-quality/small photos -> load at
2.5-3MP via Resize-to, then press ✨ Quick Photo Refine. The dialog
is the natural moment of discoverability — it's where every
restoration session starts.
If the latest history entry is itself a ✨ result, a new press POPS
it and re-draws from the same source — replace semantics, exactly
like Re-roll. Mashing on randomize now gives independent variations
of the same image instead of a converged fixed-point chain. Any
other operation (manual edit, LIR, undo/redo, vary pick — and the
base-swap ops implicitly) clears the quick_last flag, so manual work
between presses is never discarded.
The broken half-changed-person failure: above 1.6MP the auto-tile
branch gave each ~1.4MP tile the semantic instruction independently,
so each tile changed the person its own way. The manual brush worked
because its whole-canvas bbox exceeds the 1.3MP target -> scale=1.0
shortcut -> ONE global sample (the '[Angelo fine-upscale] scale=1.0'
console line). Area-Prompt mode now always takes that same global
path at any canvas size; the restoration presets keep the tiled
fallback, where per-tile locality is correct.
Dropdown beside the ✨ button with four modes (persisted, default
unchanged):
- Identity + Quality (default; Qwen keeps its tuned variant on 5D)
- Restore Photo — identity clause + the proven restore instruction
- Identity + Colours — adds a colour hold for palette-critical work
- Use Area Prompt — the user's own text drives the pass (falls back
to the default preset when the box is empty, with a console note)
Console logs the active mode per press. New hidden widget
quick_prompt_mode declared LAST.
New recipe: the whole canvas (mask = all ones, feather 0) runs through
the Xtra-Fine pipeline at a 1.3MP working target — sub-1.3MP images
get internally supersampled to 1.3MP, refined there, and composited
back (free supersampling); larger ones take the in-place path. Ref
1.0, denoise 1.0, ctx pad 128 (no-op at full coverage). Prompt is now
multi-ref aware: 'Keep the identity from image 1. make the image high
quality.' ('identity' spelling corrected from the spec). The >1.6MP
auto-tile branch is left as-is (model-tuned restore prompts) — big
canvases have the dedicated Large Image Refine button now.
The tuning knobs did their job: winners folded into the _LIR_*
constants (box 1.3MP, ref 0.2, ctx pad 128, denoise 0.5, feather 0,
chess order). TEMP widgets, inputs, and sync removed from both files;
tooltip updated to the settled numbers.
Three small inputs on the Quick Actions bar drive lir_ref /
lir_ctx_pad / lir_denoise widgets (defaults 0.4 / 128 / 0.55),
overriding the _LIR_* constants per run. The Pad knob is separate
from Xtra-Fine's regular Ctx Pad. Everything is marked TEMP in both
files — remove once the recipe is settled and fold the winners back
into the constants.
New ▦ button beside ⬆ 2x Pixel. The canvas divides into ~1MP boxes
that exact-tile it (no overlap); each box runs the Xtra-Fine pipeline
(ref 0.4, denoise 0.55, 176px ctx pad, hard feather-0 masks) under
'restore the image. make it clear and sharp.' — in CHESS order, so
the second colour's boxes refine with already-refined neighbours
visible in their context pad and match them. Compositing is
Xtra-Fine's bit-exact latent blend per box — no pixel feathering
anywhere in the path. Whole pass = ONE history entry; per-box seeds
derive from the Seed widget. The MP-target nudge forces the crop
path so the whole-canvas shortcut can never fire. Box size is one
constant (_LIR_BOX_MP) for later tweaking.
The pixel-2x and outpaint-accept state dicts carried over
refine_seed_at_run from the prior state — None when no refine had
run yet — and the ui message's int() choked on it. Producers now
fall back to the current seed widgets; the consumer is None-safe
regardless.
The fused AI-upscale pipelines (2x Restore, 2x Upscale, the tiled
upscale pass tuning, face polish + Faces toggle) are removed. One
button remains: ⬆ 2x Pixel — decode, lanczos 2x, re-encode, committed
IMMEDIATELY as a fresh session base (deterministic, so no review;
dimension change = Load-Image semantics). AI enhancement becomes the
user's next, separate move — ✨ Quick Photo Refine auto-tiles on the
resulting large canvas, Xtra-Fine spot-edits at any size.
✨ Quick Photo Refine and its tiled big-canvas path are untouched;
the tiled engine remains as its machinery. Outpaint's review overlay
returns to single-purpose. upscale_mode/face_polish widgets deleted
(never publicly released — no workflow compatibility impact).
The week of fused-recipe tuning is preserved on branch
tiling-experiments-abandoned and in history if ever wanted.
Full anchor + high denoise on the 1400px tiles (the Quick Photo
Refine recipe shape, applied to upscaling). At ref 1.0 the blend
collapses to a single cond — faster than the fractional 0.3 it
replaces. Applies to the upscale tile pass on both buttons (2x
Restore stage 2 + 2x Upscale).
Back on the proven baseline (single self-ref per tile at 0.3/0.55,
proportional min overlap, uniform spacing) — bigger square tiles are
now the context lever: ~1.9x the scene per tile vs 1024, while every
dimension stays at or under 1400px. The abandoned strips/neighbour-
refs/colour-match line is parked on branch tiling-experiments-
abandoned for reference.
Minimum overlap now scales with the canvas long edge (88 + L/46,
floor 128): calibrated to the tested points — a 3872px canvas gets
min 172, a 7744px canvas min 256. And tiles are now UNIFORMLY spaced
per axis instead of flush-to-end: every seam in an image shares the
same overlap (>= the minimum), replacing the old lopsided layout
where one boundary got 128px and the flush pair got 900px.
The low-pass swap's cutoff (tile/16 ~= 64px) traded structural
frequencies for the BLURRY lanczos base's, haze-washing every tile —
the 1936x544 2x went from great to broken. Reverted along with the
256px overlap; back to the proven 128px blend. KEPT: the proportional
face-polish context pad (max(64, 25% of bbox edge)) — the part that
was actually asked for.
The bands (e.g. 3872x1088 -> 2x showed them; 1936x544 -> 2x didn't):
at big canvases each ~1MP tile sees only a sliver of the scene, so
neighbouring renders drift slightly in tone — and a 128px crossfade
can't hide a tone STEP between tile centres, especially in smooth
regions (sky, gradients).
Three-part fix:
1. TONE LOCK (the cure): each upscale tile's low-frequency component
is replaced with the base's — the single lanczos upscale has
perfectly consistent global tone, so every tile inherits it while
keeping its own refined high-frequency detail. Implemented as a
cheap bilinear down/up low-pass swap. Applied to the upscale tile
pass only (restore passes may legitimately change tone globally).
2. Upscale tile overlap widened 128 -> 256px (25% of tile) for a
gentler crossfade; engine takes overlap_px per call.
3. Face-polish ctx pad now proportional: max(64, 25% of bbox edge) —
fixed 64px was real context for a 300px face, almost none for an
800px one.
Small lit toggle (default ON) beside the upscale buttons. OFF skips
stage 3 entirely (console notes it) — for non-portrait work or users
who'd rather polish by hand with Detect + Fix All. New hidden widget
face_polish; tooltip carries the per-flavour numbers and the SAM 3
requirement.
A restoration must not change who the person is: restore-mode face
stamps anchor much harder (ref 0.75) while re-rendering more texture
(denoise 0.7). Plain 2x Upscale keeps the freer 0.3/0.55. README
updated; tooltip-level numbers left generic.