170 Commits
Author SHA1 Message Date
WildAi 5d1b178937 bump version v2.0.1 2026-11-08 19:47:43 +03:00
WildAi f932a6419a Merge branch 'opt1' 2026-11-08 19:37:14 +03:00
WildAi 6db632da5d update 2026-11-08 19:37:14 +03:00
WildAi 6c03985533 Round 21: the vendored trees load under the pack-private _keep_vendor namespace
The flat fallback of the old dual-mode import pattern bound the shared
top-level 'modules' name, so on a sys.path where another root ships a
modules/ package, first-importer-wins decided which vendored tree - and
with it which ARCH_REGISTRY object - the pack binds.

New src/keep_vendored.py (stdlib-only, pip-_vendor semantics): one
sys.modules root '_keep_vendor' anchored to this pack's modules/deps via
spec_from_file_location, single instantiation by construction, a loud
ImportError on a foreign occupant of the private name, sys.modules
cleanup on a failed exec, never touches sys.path. The four vendored
import sites in src/ repoint to vendor(); the lazy loader sites stay
lazy. tools/scan_compat.py models vendor("...") string-literal calls
(and static _keep_vendor.* imports) so the static import closure keeps
covering the vendored tree. Guard pins: the static rot scan (no absolute
self-imports, no dynamic-import machinery in the vendored tree), the
namespace ban over src/nodes/tools, the flat-decoy probe (a foreign
modules/ tree on sys.path binds nothing shared) and the registry-identity
probe (one instantiated tree per process across every import mode).
modules/deps/__init__.py carries the docstring-only contract update - the
only modules/deps file touched.
2026-11-08 03:29:40 +03:00
WildAi edff3b16bf Merge the stale v1.x remote tail into the v2.0.0 line
origin/master had advanced past the fork point (fcb4f2d 'bump ver 1.1',
2025-07-03) with six v1.x commits (ver 1.1.1, multiface tracking, README/
pyproject updates up to the v1.2.0 tag, the wm_basicsr/wm_facelib rename)
that the local v2.0.0 rewrite supersedes. The 'ours' strategy keeps the
v2.0.0 tree byte-identical and records the v1.x tail as ancestry, so the
push to the registry branch is a plain fast-forward and no remote commit
is destroyed.
2026-11-08 01:32:54 +03:00
WildAi a9237d7b60 CI: simplify the publish workflow, drop the tests workflow
Owner-made edits (2026-11-08): publish.yml trimmed (actions/checkout v7,
Comfy-Org/publish-node-action@main, the repository_owner guard and the
issues permission dropped), .github/workflows/tests.yml removed (GitHub
runners cannot handle the test suite), pyproject.toml package
description shortened to "Video Face Super-Resolution" (version stays
2.0.0).

tests/test_ci_workflow.py is removed with it: every pin in it asserted
the shape of the deleted tests.yml (its module fixture hard-fails on the
missing file), so keeping it would turn the gate red on every run. The
stale pyproject comment that named tests.yml is updated. tools/
ci_env_sim.py stays (harmless, still runnable locally).
2026-11-08 01:27:23 +03:00
WildAi bae74a47e8 Round 19: the paste's last whole-clip transients are gone (measured attribution)
The owner's live profile on the 30 s clip (17 -> 32 -> ~52 GB) is now
attributed by measurement (a per-stage RAM sampler outside the tree:
psutil RSS/commit + StageBar marks, 128-frame CPU fixture, x6.07):

- the paste stage's gradual per-tick growth is the round-18 output
  buffer's lazy page commit (16 x 19.44 MB per 16-frame group) - the
  contract floor being paid, not a leak;
- the first stage's +15 GB is the caller-owned input IMAGE materializing
  into the working set as the detection loop reads it (dRSS +2.3 GB,
  dCommit +0 B on the raw-input run) - not pack-owned;
- the static audit found no whole-clip float32 copy of the input in the
  pack (the only advanced-indexing gather of the input is the
  max_clip_length-bounded :1132).

Two pack-owned O(N) items remained and are removed:

- FIX 1: the mixed-case faces np.stack gather (~611 MB owner-scale) is
  replaced by LazyFaceSource - the caller's face stack served per group
  (a view when contiguous, a group-sized copy when scattered) through a
  faces_provider seam mirroring frame_provider; the all-eligible
  pass-through gate keeps handing the raw stack.
- FIX 2: the fallback tail converts per gray group (_flush_fallback_group,
  the indexed write) - one group's stack + transient (<= 20 x 4.86 MB),
  independent of the fallback count, instead of a whole-fallback stack
  plus its ~10x conversion transient; the device-canvas and host tails
  keep their whole-stack form.

Gate: 6 failed, 580 passed, 16 skipped, 7 deselected - the 6 are the
owner's own asset/example edits, unchanged; 11 new pins (byte identity,
no-copy censuses, the cross-group boundary, the one-whole-clip-buffer
count).
2026-11-07 21:21:31 +03:00
WildAi 24af6e68a1 Round 18: the paste stage holds ONE whole-clip float32 buffer
The paste core owned a float32 buffer and the processor then allocated a
second whole-clip one and copied the eligible rows into it while the
first was still alive. On the owner's clip (777 x 1500 x 1080 x 3,
float32 = 15.10 GB per buffer) the mixed case - any face-less frame -
peaked at input + two such buffers (~45.3 GB); the all-eligible case
aliased and was already at the ~30.2 GB contract floor.

- paste_faces_batch_chunked gains keyword-only out= / out_rows=: the
  processor's final buffer and the destination row of each canvas row.
  The group convert-out writes by INDEX (in-place index_put_, not the
  read-side advanced-indexing copy) - a scattered destination stays
  scattered. Both None keep today's behaviour exactly.
- comfy_convert_out gains out_rows= with the same indexed write plus the
  loud-failure validation (pair, length, range, uniqueness, dtype,
  canvas, capacity).
- both processor paste twins allocate the final whole-clip buffer BEFORE
  the paste and hand it to the core; the scatter is deleted. The
  all-eligible case keeps its alias - no copy is reintroduced.
- draw_box now lands on the final rows of the final buffer.

Measured (a torch.empty census over both paths, base vs this tree): the
mixed case goes from 2 canvas-shaped float32 buffers to 1 in both the
single-face and the multiface path; the all-eligible case stays at 1; the
returned IMAGE is that buffer (weakref identity).

Gate: 569 passed, 16 skipped, 7 deselected, 6 failed - the 6 are the
owner's own asset/example edits, unchanged; the 7 new pins are the
indexed-write byte identity (both paths), the validation contract, the
mixed-case census (both paths) and the draw_box rows.
2026-11-06 23:27:36 +03:00
WildAi 4d8ffb088d Round 17: pin the byte identity across the converter's chunk boundary
The round-17 byte-identity pin used a 5x7x9 tensor, which never splits
COMFY_BATCH_CHUNK_PIXELS - it compared a single-chunk whole-clip
conversion with a single-chunk group. Production splits: at 1500x1080 the
budget holds 2 frames per chunk, so the whole clip and a group are
converted on different chunk boundaries.

The new pin patches the budget down (the split is the point, the size is
not) and asserts that the chunked whole-clip conversion and every
per-group conversion - including the lazy view's - are byte-identical to
the single-chunk rows.

Gate: 562 passed, 16 skipped, 7 deselected, 6 failed - the same six
owner-tree failures, unchanged.
2026-11-06 02:29:51 +03:00
WildAi 2b03887aec Round 17: make the paste stage's background lazy (the whole-clip uint8 stack is gone)
The final stage materialized the clip twice as uint8 BGR: the whole-clip
stack (~3.78 GB on the owner's 777-frame 1500x1080 clip) and the
eligible-gather copy of it (up to another ~3.78 GB). Both are gone:

- BgrFrameSource (src/keep_image_utils.py) wraps the caller's float32
  IMAGE tensor and converts rows on demand; the conversion is elementwise,
  so a row is byte-identical to the same row of the whole-clip conversion.
- paste_faces_batch_chunked (src/keep_gpu_paste.py) asks the background
  per group through one provider protocol; the numpy-array path is served
  by NumpyFrameProvider and keeps the old zero-copy slicing.
- both paste twins (src/keep_processor.py) take the source; the eligible
  rows are a lazy view, so the gather copy is gone and the rows that are
  not pasted are never converted.
- the fallback rows arrive in groups of their own, and their gray flags
  are computed with their group (the whole-clip gray pass is gone too);
  the eligible rows' flags were never read.

The float32 output buffer stays (it is the node's returned IMAGE) and the
float32 input stays (the caller holds it). The full and host paste modes
still materialize the eligible stack - the disclosed limitation.

Gate: 561 passed, 16 skipped, 7 deselected, 6 failed - the 6 are the
owner's own asset/example edits, unchanged; the 9 new pins are the byte
identity, the laziness, the group-sized conversions and the gray flags.
2026-11-05 21:34:44 +03:00
WildAi 59c9cd0c25 Round 16: chunk the gray-flag upload and the tensor2img chain 2026-11-04 22:27:06 +03:00
WildAi 5c0402492d Round 16: chunk comfy_batch_to_bgr_u8 conversion by the pixel budget 2026-11-04 22:14:11 +03:00
WildAi a90a927f98 Round 16: snapshot of master + the owner's asset/example edits (opt1 base) 2026-11-04 21:50:09 +03:00
WildAi 91f8d356ee Round 15: route the stage progress through the logger (no console writes in the pack) 2026-11-04 13:24:56 +03:00
WildAi d19dc14cab Merge branch 'feat/v3-modernization': the V3 modernization (v2.0.0) 2026-11-04 01:36:36 +03:00
WildAi bfede01dd5 chore(examples): drop the standalone sequence example workflow 2026-11-04 01:30:20 +03:00
WildAi ed1c492b9b Round 14 follow-up: detect the running ComfyUI server first in the logger probe 2026-11-03 23:55:17 +03:00
WildAi 422f0f7428 Round 14 regression fix: restore the video keep_net forward to FP32 (FP16 all-NaN owner-measured) 2026-11-03 23:55:17 +03:00
WildAi c771bcdb76 Round 14: make the upscaler helpers spandrel-descriptor-aware (keep-fix5-implement2)
_call_spandrel routes all three model calls through the spandrel
descriptor (size_requirements padding), the FP16 autocast is gated on
supports_half, the re-entrant _spandrel_on_device manager owns the
stage-level GPU lifecycle, and the bg tiled_scale halves its tile under
OOM (512 -> 256 -> 128, sticky). The plan's literal torch.cuda.empty_cache()
is substituted with the guard-(g)-sanctioned _soft_empty_cache at both
sites (disclosed deviation, owner-answered 2026-11-03).
2026-11-03 22:10:14 +03:00
WildAi aba4cfa739 Round 13: implement keep-fix5 (Phases 1-4; 4.2 skipped as optional)
Implements .dev/docs/keep-fix5-implement1.txt against the 9e6c50f tree:

Phase 1 - the paste pipeline: the face upscaler joins the batched core
(the _batched_paste/_batched_paste_multiface aborts are deleted), mode
is forced chunked with a bg model, face_upscale_model threads through
the paste chunk bodies (parse masks stay on the 512 faces, the ROI
grid_sample warps the upscaled faces), upscale_faces_batch_gpu (B=1,
FP16 autocast on CUDA), and _tiled_scale_and_resize_batch replaces
_tiled_scale_batch + the round-12 bg grouping (B=1 FP16 loop, one
bicubic-antialias resize straight to the paste canvas; the no-model
branch keeps _resize_bg_frames). bg_chunk_size and its pins are removed
as dead machinery. parse_soft_masks and _run_upscaler run under
autocast(enabled=(device.type == 'cuda')).

Phase 2 - facelib: RetinaFace caches priors by (h, w, device) and
detect_faces filters by confidence and runs torchvision.ops.nms on the
device before the single CPU transfer (output format preserved);
read_image gains check_gray=True, only the three sequence call sites
pass False.

Phase 3 - archs: SDPA in CrossAttention._attention, AttnBlock and both
gmflow attention forms; get_flow pinned to float32 with autocast
disabled; pos_emb cast to query_emb.dtype; keep_net under autocast at
both sequence call sites.

Phase 4 - the vendored basicsr pruning: data/, losses/, models/,
train.py, test.py, 23 unused archs, 7 unused utils deleted (78 files);
DCNv2Pack removed from arch_util; the three __init__.py rewrites;
enumeration manifests regenerated. metrics/ and ops/ stay on disk
byte-identical, off the import path. 4.2 skipped (explicitly optional).

Autocast is CUDA-only everywhere, so the CPU gate keeps exercising the
FP32 paths; no model is loaded. Gate: 556 passed, 0 failed (embedded
python; 557 -> 556 = the dead bg_chunk_size pins out, new pins in).
GPU throughput and the FP16/SDPA numerics stay owner-measured.
2026-11-03 20:23:44 +03:00
WildAi d5f93e64d5 Round 12: chunk the background upscaler into VRAM-aware frame groups
The bg upscaler's whole-clip tiled_scale call materialised the x4
intermediate of the entire clip (~0.37 GiB per 720p frame - two float32
stacks of the scaled size plus the uint8 one) and OOMed a 16 GB card on
a 48-frame 720p clip before the first paste; the owner measured the
video path unusable with the bg upscaler alone (worse than face x4 or
both x4).

batched_bg_frames now runs the tiled_scale calls over bg_chunk_size
frame groups sized from the x4 intermediate's bytes (27 B per scaled
pixel, the staging budget), each group resized to its target before the
next group starts - the x4 intermediate dies with its group instead of
materialising for the whole clip. The model work is unchanged
(tiled_scale still processes every image on its own); the no-model and
CPU branches keep their exact resize routes through the extracted
_resize_bg_frames. New pins: grouped == whole-batch pixels across the
host, device-staging and canvas variants, and the bg group budget math.
Gate: 557 passed (555 + 2), 0 failed.

GPU throughput stays owner-measured; the CUDA Lanczos resize's batch
composition is the design's disclosed owner-gated residual.
2026-11-03 01:30:21 +03:00
WildAi 58d8b11ade Round 11: resize the single-face no-face short-circuit to the paste canvas
Round-11 upscaler-pipeline audit (owner task: check whether the bg/face
upscaler inputs are applied correctly or erroneous, and how they compose
with final_upscale_factor):

- VERDICT: the two upscaler models are quality operators only - their
  output scales are always absorbed by resizes (batched_bg_frames /
  vendored paste / aligned path); final_upscale_factor alone fixes the
  output geometry (the paste canvas x factor), exactly as the node
  tooltip states.  Slowdown with x4 models is the documented design
  (a face upscaler disqualifies the batched paste; bg model runs per
  fallback frame; the canvas is model-widened x factor).
- CONFIRMED ERROR (the one real bug found, reproduced CPU-only in three
  configurations): the single-face no-face/affine-None short-circuit
  appended the raw _run_upscaler output without the factor/canvas
  resize, so a bg model with scale != factor or a sub-512 clip mixed row
  sizes and crashed the convert-out (np.stack ValueError).  The
  multiface sibling already resized (round 10); this applies the same
  paste_canvas_hw + LANCZOS4 resize to the single-face site.
- The twin size check (:1352) is dead code (all_restored_faces_cv2 is
  preallocated to num_input_frames) - no fix required.
- One pin's paste stub corrected to the real 512x512 canvas with a
  dated docstring; no fixture re-capture needed (only rows that crashed
  the run change pixels).

Gate: 555 passed, 6 skipped, 7 deselected, 0 failed (baseline 555).
2026-10-31 13:51:39 +03:00
WildAi f9c10eb6e5 Round 10: the owner's keep-fix4 (ROI grid_sample paste, boxes on canvas)
Implements the owner's .dev/docs/keep-fix4.txt precisely:

- Block A (keep_gpu_paste.py): _compute_roi_theta composes the paste's
  inverse affine with the ROI window; _paste_chunk_roi and
  _paste_chunk_faces warp each face straight into its canvas ROI with
  one F.affine_grid / F.grid_sample pair per face and blend ROI-local
  with the plain round_().clamp_(0, 255) tail - the fix-2.1 full-canvas
  batched warp and the t3 sequential per-row accumulator are replaced
  (overlapping ROIs no longer see an earlier face's blend; the later
  face's scatter wins).  warp_face_roi and _paste_frame_full_canvas_multi
  lose their production call sites and stay live for their op-level pins;
  _quantize_and_clip stays live for _paste_chunk and the full-canvas
  fallbacks.
- Block B (keep_processor.py): _draw_boxes_on_canvas draws the 2-px
  green box per face on the device canvas / chunk buffer; draw_box (and,
  for the multiface, the gray skip) no longer disqualifies the batch;
  stage_bar is threaded into every paste call of both batched bodies.
- Block C: the multiface paste stage rides the same cadence as the
  single-face one - batched rows tick inside the paste core (the per-
  group tick extended to the multi-face branches), fallback rows tick in
  the loop, and a frame with no face (or every affine lost) short-circuits
  to the background, resized to the paste canvas (adapted from the
  snippet's raw append: a sub-512 clip would stack-mismatch - disclosed
  in the dated comment).
- Disclosed limitations, as written in the spec: the boxes land on the
  device-output canvases only (a CPU draw_box run keeps no boxes on the
  batched frames), and the uint8 color casts to 255.0 on the float32
  chunk buffer; the dead "Device output assembly disengaged" host log is
  gone with the wholesale body replacement.

Both sequence goldens re-captured (owner-authorized pixels; only
output_frames changed); the paste equivalence / multiface / bar-cadence
pins reworked to measured values with dated docstrings.

Gate: 555 passed, 6 skipped, 7 deselected, 0 failed (baseline 555,
pins reworked 1:1).
2026-10-31 01:43:07 +03:00
WildAi 84a9908387 Round 9: the owner's keep-fix3 (five paste-stage fixes)
Implements the owner's .dev/docs/keep-fix3.txt precisely:

- Fix 1.1: a frame with no detected face (affine None) short-circuits
  the fallback paste to the upscaled background - no vendored
  paste_faces_to_input_image on a face-less frame; paste_bar threaded
  into _batched_paste; the canvas/chunk buffers alias when every frame
  is batched (no copy); progress.update(1) added to the skipped rows.
- Fix 1.2: the grayscale eligibility filter is gone - the device blend
  pastes a gray frame with the same tensor logic; _batched_paste gains
  stage_bar and forwards it into all three paste modes.
- Fix 2.1: _paste_chunk_roi is one vectorized GPU tail - the faces and
  the parse masks warp to the full canvas in one warp_affine_batch call
  each (grid_sample), one-tensor alpha blend, the vendored quantizing
  tail over the whole-canvas maximum; the per-face ROI machinery
  (warp_face_roi, fixed-point terms, ROI patch transfers) is gone from
  the single-face path.  warp_face_roi stays live for the multi-face
  sibling _paste_chunk_faces.  Pixel change owner-authorized; the
  retired transfer counters are disclosed in the dated docstring.
- Fix 2.2: device_output_mode - CUDA never returns "host": full while
  the whole batch's canvas fits the budget, else always chunked (the
  OOM handler is the net, the owner-authorized trade); non-CUDA keeps
  "host".
- Fix 2.3: paste_faces_batch_chunked ticks the stage bar per completed
  GPU group; paste_faces_batch does the same per chunk (the full/host
  modes) - nobody ticks twice, the bar totals num_input_frames exactly
  on every path; the multi-face groups get no tick (the multiface
  stage owns its bar).

The single-face sequence golden is re-captured (owner-authorized
pixels); the paste equivalence and cadence pins are reworked to the
measured behavior with dated docstrings.

Gate: 555 passed, 6 skipped, 7 deselected, 0 failed (baseline 552).
2026-10-31 00:13:46 +03:00
WildAi 3417d79a54 Round 8: per-stage console progress bars (StageBar)
The four sequence stages (detecting face landmarks, cropping and
aligning faces, restoring faces with KEEP, pasting faces and finalizing
frames) logged only their start line and went silent until the next
stage.  StageBar (src/keep_stage_progress.py, stdlib-only) is a second,
console-only progress channel beside the V3 execution-API reporter:

- the line "{label} {done}/{total} ({pct}%) {rate} fps ETA {mm:ss}
  elapsed {mm:ss}" - overall-average rate, floored percent (never 100
  early), ETA from the average rate;
- carriage-return rendering on a real console (no ANSI escapes, the
  [KEEP] pack tag, writes to sys.__stdout__ so the D4 console capture
  is never corrupted); one complete logger line per render otherwise;
- the cadence is a 2 s time gate - O(1) per tick, no per-frame spam;
- one bar per stage per run: constructed before the chunk loop, ticks
  accumulate across chunks, one finish() on the normal path (an OOM or
  a cancel leaves without a final line - the exception is the report).

Hooked at nine sites across both sequence paths: detection ticks per
frame (face-less frames too), the single-face crop per affine, the
multi-face crop once per face batch (unit=faces - the per-face LMEDS
loop is opaque), restore per chunk, paste per frame iteration.  The
early returns (all-NaN, zero faces) carry their stage's final line.

Gate: 552 passed, 6 skipped, 7 deselected, 0 failed (baseline 535; the
new pins live in tests/test_keep_stage_progress.py).
2026-10-30 20:20:53 +03:00
WildAi 57c6f59be4 Round 7: fix the paste-stage stall (release the VRAM budget before the paste verdict)
The paste-mode verdict (device_output_mode) reads the free VRAM through
ComfyUI's get_free_memory(..., torch_free_too=True), which counts what
the torch cache can give back. Round 6 (Opt 6) deleted the per-chunk
_soft_empty_cache() of the restore loop, so cached restore blocks shrank
the paste budget (the min of the 1 GiB cap and the free share) and the
verdict flipped from full/chunked to host: the clip took the host-CPU
assembly - the owner's "Pasting faces and finalizing frames" stall.

One release before the verdict in both sequence paths restores the
pre-Opt-6 budget once per run, without re-paying the per-chunk sync cost
Opt 6 removed. The cadence pin records the measured twice-per-run clean
cadence (pre-paste, then the finally's); the OOM cadence is unchanged
and pinned; a new pin proves the release precedes the paste stage.

Gate: 535 passed, 6 skipped, 7 deselected, 0 failed (baseline 534 plus
the new ordering pin).
2026-10-30 18:36:03 +03:00
WildAi 8c10bc5118 Round 6: the owner's six GPU optimizations (keep-fix1)
Opt 1 - the fused crop loops warp on the device (crop_faces_batch per
source-shape group) instead of one cv2.warpAffine per row.
Opt 2 - warp_face_roi uploads its four terms once and composes
x_full/y_full on the device; the fixed-point math is unchanged (bitwise).
Opt 3 - bgr_u8_batch_to_normalized runs in float32 end to end and
_gaussian_smooth convolves in float32; the float64 transient is gone.
Opt 4 - the Lanczos axis matrices are cached per (in_size, out_size); the
cache holds device-free numpy, so one entry serves both devices.
Opt 5 - the aligned single-image path restores on the device
(_restore_aligned_torch): no detection, no vendored img2tensor tail, no
vendored paste.  The io schema is untouched.
Opt 6 - the chunk loops no longer call _soft_empty_cache(); the release
stays in the finally blocks and in the OOM handler.

The two sequence golden fixtures are re-captured (the owner's documented
override of the capture tool's do-not-rerun warning) - Opt 1 and Opt 3
change pixels by design.  The image fixtures are unchanged.

Tier-1 gate: 534 passed, 6 skipped, 7 deselected, 0 failed.
2026-10-30 01:46:08 +03:00
WildAi fd372d3276 test(paths): skip .npz in the machine-local-path scan
The round-4 golden fixtures are compressed archives: scanning them as
text trips on coincidental C:\ / C:/ byte pairs. The pin passed at
round 4 only because the fixtures were still untracked at gate time
(git ls-files did not list them); the decompressed members were
verified path-free during that round's QA.
2026-10-29 00:55:54 +03:00
WildAi e2fdbbf453 perf(keep): derive the chunk's detection frames in one grouped pass
The round-4 fused crop loops re-derived each frame's detection image with
one single-row converter call per frame - N host sync points where the
round-3 crop did one batch. The grouped form converts the chunk's gathered
rows in one elementwise pass (bitwise the per-row rows) and is bounded by
the caller's chunk; the paste-fallback row form stays. Pins guard the
conversion count. The paste stage itself is proven byte-equivalent to
32e6628 (dispatch, cores, tails); timed CPU stubs show no superlinear
term in either tree - the real-box burn needs the owner's log to attribute
(the device-mode verdict under real VRAM pressure is the only paste-stage
quantity this diff can move).
2026-10-29 00:55:54 +03:00
WildAi 3af08f4357 perf(keep): stream the clip through detection/restore and place the face nets on the pack device
- R1: the face helper is constructed on the pack device (was the offload
  device); load_device() explicitly places the detector and the parser with
  a logged CPU fallback, offload() mirrors the restore, the YOLO .device
  skew is closed (P-D1..P-D5).
- R2: detection streams per max_clip_length group (no full-clip convert-in
  retention), crop is fused into a chunk-bounded restore loop with
  preallocated staging, the paste tail keeps one contractual float32
  buffer, the helper is released after every run; golden fixtures pin the
  outputs bitwise (P-M1..P-M7).
- The diagnosis: the OOM was O(frames) whole-clip host buffers, not the
  faces (the detector/landmark path was already GPU-placed);
  max_clip_length never bounded convert-in or the paste tail.
- Gate: 523 passed, 6 skipped, 7 deselected, 0 failed (baseline 505).
2026-10-28 23:39:53 +03:00
WildAi 317f503c6d feat(keep): make Cancel work and restore every detected face
Two owner-reported bugs, both verified as gaps that never closed (the old
code on master had them too).

Cancel: the node never polled ComfyUI's interrupt flag, so the Cancel
button had no effect. A new src/keep_interrupt.py wraps
comfy.model_management.throw_exception_if_processing_interrupted() (lazy
import, a no-op outside ComfyUI); it is called at every long-loop
boundary - detection, crop, restore chunks, paste frames, and the paste
module's group/chunk loops. InterruptProcessingException is a
BaseException, so it escapes the pipeline untouched.

Multi-face: the pipeline collapsed to one face per frame - the detector
call passed only_keep_largest=True (the vendored selector reduces to the
largest face) and the processor then took all_landmarks_5[0]. With
only_center_face=False the new branches keep every detected face:
detection returns all faces, the crops/restore/paste flatten to the
ragged per-frame face count, the device canvas composites several ROIs
per row, and the vendored fallback hands the helper all of a frame's
faces. The near-black tail follows the real vendored reference (one
convert per image after a forward face loop). only_center_face=True and
the aligned path keep their bodies byte-for-byte.

44 new pins: cancel (the exception escapes the sequence and the node, no
work after it, unchanged output without a cancel), multi-face image and
video (all faces restored and pasted, faces appearing/disappearing,
None-affine and black-face drops, per-row near-black tail against the
vendored composite), and the paste core (face-to-frame map, row
compositing, chunk boundaries that never split a frame's faces).

Verified on the CPU device only - no model loads, no GPU runs. Gate: 505
passed, 6 skipped, 7 deselected, 0 failed (461 baseline + 44 new pins).
2026-10-28 18:11:25 +03:00
WildAi fdc1d6f03a perf(paste): keep the output assembly GPU-resident at any upscale factor
The round-1 device canvas disengaged past its fixed 1 GiB budget, so at
final_upscale_factor=4 (~44 MB/frame at 720p) typical clips fell back to
the host assembly at 16x the pixels. The engagement is now three-way:

- device_canvas_budget: a share of the free VRAM (0.25) under the hard
  cap, the cap when free memory is unavailable - the mirror of
  paste_chunk_size.
- device_output_mode: full | chunked | host. The chunked mode assembles
  frame groups on the device (the same group size the full mode derives
  internally, so the batch compositions coincide) and downloads the
  per-group convert-outs into one preallocated host IMAGE buffer - the
  device footprint stays bounded for any clip length.
- comfy_convert_out: the BGR->RGB float convert-out runs on the device
  in budget-sized sub-chunks (the out-of-place chain peaks at ~9x the
  canvas bytes of transient), so the budget bounds what it gates.
- a "host" verdict on a CUDA device is logged - the silent fallback
  that hid the regression must not be silent again.

Also: the paste loop's NaN-landmark short-circuit never recorded its
frame in fallback_indices - in a device-output mode the tail's indexed
assignment length-mismatched (pinned red before the fix; the branch is
defensive today, the production interpolation fills NaN landmarks).

8 new bit-exactness pins (budget, selector, convert-out sub-chunking,
factor-4-scale, chunked assembly incl. group-boundary independence,
sentinels, end-to-end modes); the instrument gains the factor-4-scale
output-mode scenario. Verified on the CPU device only - no GPU run (the
CUDA before/after stays owner-gated). Gate: 461 passed, 6 skipped,
7 deselected, 0 failed.
2026-10-27 22:19:53 +03:00
WildAi fadee7c91f chore(tools): add the F1 output-mode comparison to the paste attribution
The instrument now times the same paste_faces_batch call in the host and
the device output mode, one session, one fixture, and asserts the two
are bit-identical. On the CPU fixture (8 frames, 360x640, stub nets):
the output assembly's host<->device traffic is gone (1.06 MB each way
becomes 0, the ROI-sized staging recorded as roi_staged_bytes), wall
-8.8%, process CPU -8.7%. The delta dilutes the CUDA effect - on the
owner's GPU the kernel work stays on the GPU and the removed portion is
pure host time - so the CUDA measurement of the same comparison stays
the owner-gated V2.
2026-10-27 19:23:21 +03:00
WildAi c7e0674086 fix(paste): download the one frame the near-black branch measures
A self-review of the device-output canvas found the rare near-black
branch calling the numpy _background_max_outside with a device tensor -
it happened to work through the numpy/torch API overlap, but it made an
undocumented per-frame device read inside the branch and would break on
a torch-only API change. The branch is rare by construction, so this one
frame is downloaded explicitly, exactly like the full-canvas fallback
next to it.

Also adds the device-output pin of the near-black branch itself
(test_the_device_canvas_keeps_the_near_black_full_canvas_fallback): the
rare path now runs in the canvas mode in the gate instead of only in
production.
2026-10-27 19:01:19 +03:00
WildAi 3bfeed9478 perf(paste): keep the pasted output stack on the device until the convert-out
The paste-stage attribution (tools/prof_paste_host_cpu.py, the CPU-device
stub run) showed the batch always takes the device path - the vendored
per-frame fallback never runs in the default configuration - and that the
remaining host cost of the step is the output assembly: the per-frame ROI
download, the whole-canvas host copy, the whole-stack np.stack and the
float math of the convert-out, all of which exist only because the output
stack is assembled in host memory.

F1 keeps the stack on the device: batched_bg_frames(return_device=True),
paste_faces_batch(device_output=True) scatters the blended ROI patches
into one uint8 device canvas, and the paste step assembles the batch with
one indexed copy, one stacked upload of the vendored-fallback frames and
a single bgr_u8_batch_to_comfy on the device. Nothing is downloaded or
copied per frame. The mode is off on a CPU device, over the 1 GiB
PASTE_DEVICE_STACK_MAX_BYTES budget and when nothing is eligible, so the
host path - and every existing pin - runs byte-for-byte as before; the
bytes that would have travelled are kept as roi_staged_bytes.

The new pin test_the_device_output_stack_is_bit_identical_to_the_host_stack
pins the canvas against the host stack on a CPU device. Gate: 451 passed,
6 skipped, 7 deselected.
2026-10-27 18:06:48 +03:00
WildAi 0858b41a73 perf(paste): run the background LANCZOS4 resize on the device
The owner's route decision (the tolerance-accepted GPU resample):
resize_lanczos_u8 implements PIL's three-lobe Lanczos with the
scale-aware support and the drop-and-renormalize edge rule as dense
axis matrices + matmul, chunked by LANCZOS_CHUNK_PIXELS pixels.

batched_bg_frames keeps the result of _tiled_scale_batch on the device,
resizes with the new op when CUDA and downloads once at the end; a CPU
device keeps the per-frame cv2 loop (pixel-identical there - the
equivalence pin still holds exactly).

Pinned in test_image_op_equivalence.py against PIL (the semantic
reference: mean <= 0.3, max <= 12) and against cv2 (the accepted drift:
upscale mean <= 1.0 / max <= 8, downscale mean <= 8.5 / max <= 54),
plus chunk transparency and a GPU-vs-CPU pin; the profiler wraps the
op as p6_bg_resize.
2026-10-26 22:19:54 +03:00
WildAi 52de0b1afc perf(paste): compute the gray flags on the device
The paste stage's largest remaining host-side item was the gray flag: one numpy variance pass per frame (41 ms of process CPU per frame, 4.7 cores). On a CUDA device the three channel-difference variances now run on the device in float64 (the vendored int16 differences, chunked to bound the transient), and only the frames whose result lands on the threshold fall back to the host computation - so the flags are the host path's flags, not a tolerance.

A CPU device and a caller without a device keep the numpy computation unchanged. Pinned in tests/test_is_gray_device.py: random, flat and saturated frames, a constructed borderline frame that actually hits the threshold, the CPU device, the empty batch, a bad shape, and both processor call sites.
2026-10-26 21:07:57 +03:00
WildAi 40d6deefa3 chore(tools): add the paste sub-stage breakdown to the stage probe
The probe now measures the paste stage's sub-stages (the wall time, the process CPU time and the core count of every step) and counts the host-device copies of the paste, so a change of the paste can be judged on the CPU load it leaves, not on the wall time alone.
2026-10-26 18:23:29 +03:00
WildAi a612061fcd perf(paste): paste faces on ROI-sized device data
The paste stage spent its host time on three full-canvas passes per frame. The face warp now runs on the device with cv2's own fixed-point convention (the matrix inversion of warpAffine, the 1/1024 pre-scale, the 1/32 tap grid, the (+512)>>10 rounding), the mask warp reuses the one batched grid sliced to the face ROI, and the blend and the quantizing tail run inside that ROI. The frame-global maximum branch is kept: a near-black frame falls back to the full-canvas path, which stays bit-exact by construction.

Measured over 48 frames at 720p on CUDA: the paste call's process CPU time falls 20% (38.74 -> 30.92 ms/frame), the host-device transfers shrink 6.66x (the blended canvas download by 85%), and the result is bit-identical to the full-canvas path on the smooth and the noise corpus, at interior and edge-touching ROIs.
2026-10-26 18:23:29 +03:00
WildAi 6362b9cb4c chore(tools): add the stage-timing probe of the sequence pipeline
Times every stage of process_image_sequence on synthetic frames with a same-session control clone of the base implementation, plus micro benches for the parse chunk sweep and the KEEP-stage allocator question. The evidence behind the redesign ranking and the before/after tables of the internal design review.
2026-10-26 08:04:09 +03:00
WildAi 4770354f25 feat(video): run the sequence pipeline read-once, affine-once, batched on the device
process_image_sequence converts the batch once, detects per frame while capturing the frames the detector saw (the read_image products, including the min(h,w)<512 upscale), estimates each affine once with the vendored call, crops with cv2.warpAffine (decision D2: the uint8-bound warp stays bit-exact; the batched grid_sample helper stays parked and pinned), normalizes the whole crop batch on the device, keeps the KEEP chunk contract byte-identical, converts the restored faces with one batched tensor2img, pastes through the batched paste with the vendored fallback, and stacks the output in one transfer. The paste canvas is the detection frame, so small frames keep the base's upscaled canvas (the round-1 QA defect F1, fixed and pinned). A failed affine degrades to the neutral crop with a warning instead of crashing like the base. Progress still reports num_frames * 4 in the four families. Pins: the read-once/affine-once/chunk-contract AST guards, the progress contract, and the new end-to-end base-vs-shipped suite (tests/test_gpu_sequence_e2e.py) with the real vendored helper - measured drift 0/255 on the vendored branch, 1/255 on the batched branch.
2026-10-26 08:04:00 +03:00
WildAi a613d9ed45 feat(paste): batched face paste-back with the vendored path as fallback
New src/keep_gpu_paste.py: the chunked ParseNet forward (chunk cap 8, the measured VRAM sweet spot), the batched parse-mask postprocess, the batched blend with the vendored quantizing tail, one tiled_scale call for the whole background group, is_gray without the PIL round trip, and VRAM-aware chunk sizes from model_management.get_free_memory. The frames the batched paste cannot handle (draw_box, face upsampler, grayscale, no parse net, no face) keep the vendored paste_faces_to_input_image. Equivalence against the real vendored paste: <=1/255 on the parse mask, blend and tail bit-exact.
2026-10-26 08:04:00 +03:00
WildAi 69b14b8a0e feat(ops): batched device-side image ops with measured equivalence pins
New src/keep_gpu_ops.py: affine capture with the vendored cv2 calls, batched crop/inverse warps (grid_sample, in-bounds compositing of the gray border, uint8 requantization), a batched form of the vendored tensor2img chain, the ParseNet colormap/blur/postprocess, and the batched ComfyUI converters in keep_image_utils.py (one host transfer per batch; the /255 reproduces img2tensor's double rounding bit for bit). Every op carries a measured-tolerance pin in tests/test_gpu_batch_equivalence.py; the uint8-bound warps measure up to 8/255 against cv2's fixed-point sampling on adversarial content - the numbers that drive the tolerance decision D2.
2026-10-26 08:03:45 +03:00
WildAi ba503f2068 build(deps): declare only what the ComfyUI host does not ship
The host's requirements.txt provides torch, torchvision, numpy, einops, pyyaml, Pillow, scipy, tqdm and requests; the pack now declares only opencv-python-headless - the vendored architectures and the four LANCZOS4 resizes need cv2, and no GUI call exists anywhere in the pack or the vendored tree, so the headless wheel suffices (no libGL/Qt in a server install). requests was a phantom: only the vendored basicsr download helper imports it. The dependency guards move to the host-guarantee policy (closure coverage = declared + host-provided, pinned to the host's requirement lines, exhaustive in both directions).
2026-10-25 23:07:07 +03:00
WildAi d18bad3186 feat(download): report a finished weight to the host progress bar
download_weight's progress flag becomes real: one guarded comfy.utils.ProgressBar step per completed download (a failed one stays at 0), lazy import so the engines load without a server. The torch.hub transport stays bar-free - its own bar is untagged stderr noise.
2026-10-25 23:06:47 +03:00
WildAi f17ed67297 refactor(images): run the pack's image ops on torch, not cv2/scipy/torchvision
The channel conversions, the INTER_LINEAR resizes, the GRAY2BGR lift and the temporal landmark smoothing (the former gaussian_filter1d) become torch-native helpers; tensors keep their device and the host copy happens once, as uint8. The four LANCZOS4 resizes keep cv2 (torch has no lanczos) - that is why OpenCV stays declared. Equivalence is pinned in tests/test_image_op_equivalence.py: conversions exact, bilinear resize within 1/255 of cv2 over a 20-geometry matrix, smoothing within 1e-10 (float64 measured ~4e-14, float32 bit-equal).
2026-10-25 23:06:38 +03:00
WildAi 162d86323a fix(paths): do not repeat the block of the host on the console
The loader of ComfyUI logs one line per yaml entry. The host already printed that block at its startup, so the pack raises the level of the root logger while it asks the host to read the file again - the block is not printed a second time.
2026-10-25 18:53:33 +03:00
WildAi a5226da5cb fix(paths): read the yaml files of the command line too
ComfyUI reads the default extra_model_paths.yaml and every file of --extra-model-paths-config (main.py:142-144). The pack now follows the same list, so a user who supplies the extra roots only on the command line gets the layout convention as well and the checkpoint is found where it is instead of being downloaded again.
2026-10-25 18:41:17 +03:00
WildAi 99fa9888db fix(download): never destroy a weight that the pack does not own
The hash mismatch path deleted the file it found - and since the extra model roots are resolved now, that file can live in the original directory of the user. The download is atomic now (the transport writes into a temporary file that is only put in place after it was verified), a file outside the primary root of the installation is never deleted or overwritten, and the co-location of the face weights keeps a file of the user as a backup instead of overwriting it.
2026-10-25 18:27:41 +03:00
WildAi 4abcfe0856 test: guard the five production defects
Per-defect regression guards (the extra model roots, the logger of ComfyUI, the KEEP tag, the routed console output, the selective download) plus the host modules app and utils in the lazy-import guard.
2026-10-25 17:33:08 +03:00