275 Commits
Author SHA1 Message Date
malkuthroandClaude Opus 5 229457c057 fix(video): Easy_LoadVideo degrades to silence on sources with no audio track (#284)
* fix(video): Easy_LoadVideo degrades to silence on sources with no audio track

VHS returns a lazy audio map (LazyAudioMap, a collections.abc.Mapping) for
the AUDIO output, deferring `ffmpeg -f f32le` until something reads it. Since
ComfyUI core c01175530, comfy/model_patcher.py walks every linked output
hunting for model patchers and recurses into any Mapping via .values() --
which forces that extraction even when nothing consumes the AUDIO socket.

On a source with no audio stream, ffmpeg has nothing to mux and exits -22
("Output file does not contain any stream"), so VHS raises and the whole
prompt dies. That is every Nuke mov64 write: video plus timecode, no audio.

Easy_LoadVideo now wraps the deferred map at both VHS return paths. When the
ffmpeg report lists no audio stream it substitutes silence and logs once; any
other extraction failure is re-raised untouched so real problems stay loud.
The wrapper stays lazy, so nothing runs ffmpeg that did not run before, and
_is_lazy_mapping targets non-dict Mappings only -- VHS builds video_info as a
plain dict, so that slot is left alone.

Verified against the reporting file (Nuke ProRes 4444, video + timecode, no
audio) with the live install's real ffmpeg and torch, driven the way core
does: raises before the guard, yields [1, 2, 1] @ 44100 after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(video): only degrade to silence on proof the source has no audio

Review of #284 found the guard masking failures it promised to re-raise.

The discriminator tested for ABSENCE OF EVIDENCE -- "the ffmpeg report does
not mention an audio stream" -- and degraded to silence whenever that held.
But a missing ffmpeg binary, a permission error, a subprocess timeout, an
empty message, a truncated log, and any non-ffmpeg exception all lack that
line too, so every one of them was silently converted to silent audio. That
contradicts the PR's own contract that non-no-audio failures stay loud, and
is exactly the silent corruption the guard was written to avoid.

_reports_source_has_no_audio now requires EVIDENCE OF ABSENCE: ffmpeg opened
the input, listed its streams, none of them is audio, and the audio muxer then
had nothing to write. All four must hold. Anything unrecognised re-raises, so
a future ffmpeg wording change surfaces as a visible error rather than silent
corruption.

Second finding, same review: the guard wrapped any non-dict Mapping in the
result tuple rather than the AUDIO slot specifically. No live impact -- VHS
builds video_info as a plain dict -- but an unrelated deferred Mapping would
have had its failures rewritten as missing audio. _guard_lazy_audio now scopes
by RETURN_TYPES and touches only the declared AUDIO output.

Tests: seven ambiguous failure shapes assert both that the predicate rejects
them and that the wrapper re-raises, the latter with torch stubbed so the test
cannot pass merely because silence was unconstructible. The VHS test doubles
gained the RETURN_TYPES every real ComfyUI node declares, rather than the
production code growing a getattr fallback that would quietly skip guarding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tests): drop import orphaned by the module-qualified guard calls

The guard tests now reach through k_video_load so they survive the
importlib.reload other tests in the file perform, which left the top-level
_guard_lazy_audio import unused. Ruff F401 in Lint & Smoke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:41:24 +02:00
malkuthroandCursor 7050f2328e Release v0.5.0 (#283)
* Release v0.5.0: stability fixes for drift status and draft toasts

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): stop hardcoding pack version in version-route assertion

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): parse pyproject independently in version-route assertion

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
v0.5.0
2026-08-10 16:06:58 +02:00
malkuthroandClaude Opus 5 090eb460eb fix(snapshot): boot drift check no longer false-positives on unsaved edits (#278)
* fix(snapshot): boot drift check no longer false-positives on unsaved edits (#276)

The #161 boot-time drift guard flagged any canonical difference between
the tracked snapshot file and the live /userdata+picks state — which is
also what ordinary "kept working after the last named save" looks like,
so nearly every session booted into a false "drifted" warning and
periodic autosaves were misrouted to _unsaved_autosave/.

detectBootDrift() is now three-way: a mismatch counts as drift only when
the live state's fingerprint also matches the persisted last-named-save
baseline (it claims to be saved, yet the file disagrees — pinning the
out-of-band change to the file side). Otherwise it reports the honest
"unsaved". First-session baseline seeding in koolook_sidebar.js moved
after the drift check so a freshly seeded baseline can't fabricate the
claims-saved condition.

Tests: two new cases (unsaved-edits, no-baseline); drift-expecting cases
now establish the claims-saved baseline; the states-match and
canonicalization tests baseline first and assert reason is absent so
they pin the match branch rather than the new early exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(snapshot): gate first-session baseline seeding on a proven boot check (#276)

Review follow-up. Ordering the drift check before the seeding was not
enough: seeding still ran whenever the fingerprint key was absent, so a
session whose tracked-preset name outlived its baseline (upgrade from
before the key existed, or a persist that lost to localStorage quota)
baselined whatever the live state happened to be. That both showed
"saved" for state never saved to the named file and handed the NEXT boot
a live state claiming saved against a file it never matched — a fresh
false "drifted", i.e. #276 re-opened from the other side.

Seeding is now gated on bootCheckProvedAligned(), a pure predicate
exported from snapshot.js (the entry point imports ComfyUI's app.js and
can't be loaded headlessly — same split as extension_guard.js's
claimSidebarRegistration). Only a boot check that reached the
fingerprint comparison and matched authorizes seeding; null and
reason-carrying outcomes prove nothing.

Also: branch the !claimsSaved log so the no-baseline case stops claiming
"unsaved edits since the last named save" when there is no such save,
and refresh the _autosaveSubdir redirect rationale, which still framed
the live state as the corrupt side.

Tests: predicate truth table, an end-to-end blocking case carrying a
counterfactual leg that proves the gate is load-bearing (ungated seeding
does flag drift on the next boot), and the allow case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:23:49 +02:00
malkuthroandCursor cf32beb8b0 fix(install-guard): skip duplicate scan in linked git worktrees (#281) (#282)
* fix(install-guard): skip duplicate scan in linked git worktrees

Linked worktrees outside custom_nodes/ are not ComfyUI installs; stop treating sibling worktrees as #162 duplicates so pytest can register nodes. Closes #281.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(install-guard): keep #162 active for custom_nodes→worktree symlinks

Use the unresolved load path for the worktree skip so Path.resolve() cannot drop a custom_nodes parent and disable the duplicate scan.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 11:27:46 +02:00
malkuthroandClaude Fable 5 3608ab5a21 fix(ltx): scrub base64 posters from persisted Director timelines (draft-quota toast storms) (#277)
* fix(ltx): scrub base64 posters from persisted Director timelines

The vendored upstream 2.0.2 timeline editor stamps a base64 poster JPEG
onto every video segment (canvas.toDataURL) and kept it across serialize
round-trips, so timeline_data - and with it the workflow draft ComfyUI
1.44+ writes to localStorage ~512ms after every edit - grew by ~50-150k
chars per video segment. Large timelines then exceeded the origin quota
no matter how many old drafts koolook_draft_guard.js evicted, and the
frontend latched its session-wide storage-unavailable flag: every edit
toasted "Failed to save workflow draft" until reload.

Reinstate a minimal media scrub as a third documented Koolook delta,
applied at all six persist/restore maps (commitChanges + parseInitial):

- video / motion_video posters with a backing file (imageFile on the
  main track, videoFile on the motion track) are dropped; the seeked
  handler re-derives them from the video element on load
- legacy pre-2.0.2 image segments carrying full inline data-URLs are
  rewritten to the /view URL form 2.0.2 itself writes (loadMedia renders
  image previews from imageB64 only - deleting would blank them)
- legacy inline audioB64 (raw base64) is dropped when audioFile exists;
  playback only falls back to it when no file resolves
- blobs with no backing file are left untouched

Disk Save / Save As payloads stay unscrubbed on purpose: exported .json
keeps upstream's format and meets the restore scrub on load. On-screen
previews are unaffected - only persisted copies are scrubbed.

Covered by Node behavior tests that evaluate the delta block against a
stubbed api, plus a source contract that all six maps route through the
scrub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ltx): persist the scrubbed timeline on restore; review follow-ups

Addresses the verified findings on #277.

The restore-time scrub only updated the editor's in-memory timeline. The
node's timeline_data widget kept the original payload, and that is what
onSerialize and LiteGraph's widgets_values read - so an already-fat saved
workflow still fed the draft autosave that fires on load, before any edit
triggers commitChanges. Both restore paths (the onConfigure sync block and
the editor constructor) now rewrite the widget from the scrubbed timeline.
The round-trip is lossless: parseInitial whitelists exactly the keys
commitChanges persists. Chose this over calling commitChanges during
configure, which would also recompute the prompt/length/strength widgets on
every node's configure - far wider blast radius than the defect warrants.

Also:
- keep the data-URL when a malformed imageFile yields no /view URL, instead
  of blanking the preview with an empty string
- rewrite the stale provenance-header paragraph that still claimed the
  base64-scrub deltas were obsolete (it now contradicted the third delta)
- update the v2.0.2 entry in forks/forks_manifest.yaml from "two retargeting
  edits" to the three current Koolook edits, with the scrub's rationale
- note the audioB64 leg and the widget rewrite in the CHANGELOG entry

The bare catch in parseInitial is left alone: pre-existing upstream
behavior, and the scrub introduces no realistic new throw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ltx): trigger the widget rewrite on legacy raw-base64 audio too

The restore-time widget rewrite gated on the payload containing "data:",
which catches the poster/image blobs that caused the quota storms but not
the scrub's third removal: legacy audioB64, which is raw base64 with no
"data:" prefix. An audio-only fat timeline would have its memory scrubbed
while the widget stayed fat until the next commitChanges.

Gate on either token instead, so the trigger covers everything the scrub
actually removes. A false positive still costs only a lossless rewrite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:16:37 +02:00
malkuthroandCursor 11fb08b9a3 feat(skills): Cursor PR-review skills — review-pr-comfy-cursor (#279) (#280)
* feat(skills): add Cursor Koolook PR-review skills (batched-verify + Grok)

Port the K-Memory v2 Cursor review pipeline with Koolook invariants briefs, PR-head refresh, and fast/deep entry points for issue #279.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(skills): shorten PR-review skill names to review-pr-comfy-cursor

Rename deep/fast Cursor review skills from *-koolook-cursor to *-comfy-cursor.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): update triggers and paths to review-pr-comfy-cursor

Keep skill body text, menu-comfy, and Cursor mirrors aligned with the shortened names.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): keep review worktrees in-repo and guard Cursor mirrors

Use .review-worktrees/ so Cursor Tasks can Read/Grep the PR head, and assert .claude/.cursor SKILL mirrors stay byte-identical.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 10:41:44 +02:00
malkuthro 6ab4476169 feat(metrics): publish registry summary (#275) 2026-08-06 20:28:04 +02:00
malkuthro 2babbafb6b chore(security): pin Actions and harden test lock (#273)
* chore(security): pin Actions and update aiohttp

* fix(security): generate universal test lock

* fix(security): harden relock safeguards
2026-08-06 20:02:37 +02:00
malkuthro 7b00a551d9 feat(publish): independent output type for published setups; fix(pipeline): auto-versioning re-runs each queue (#267)
Publish: Koolook_PublishOutput output_mode/input_switch/switch; setupSurface.app.outputSwitch; routers auto-bound as the output selector; unwired output types hidden; 'Same as input' offered only when every input type has a wired writer; result path follows the output switch. Pipeline: EasyAIPipeline IS_CHANGED keeps version=auto/next perpetually dirty so the vNNN disk scan re-runs every queue; literal versions stay cacheable.
2026-07-19 23:34:30 +02:00
950f15e569 feat(ltx): Soften Schedule — sigma-range level + info readout + runtime hardening (#266)
* docs(ltx): clarify Soften Schedule tooltips (amplitude vs sigma-progress)

max_soften: it's an amplitude (schedule-independent) — default 0.5 ≈ 10%
unpinned at the peak. crossover: measured in denoise sigma-progress, NOT
step count, so the number of steps it covers shifts with the sampler/sigma
schedule. No behavior change — tooltip text only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): Soften Schedule — predictable step-fraction window + info output

Replace the sigma-progress `crossover` knob (which covered a wildly different
number of steps per sigma curve — flat front-loaded curves stretched it across
~half the run, steep curves collapsed it to ~1 step) with `protect_until`: a
FRACTION OF THE STEPS, resolved against the sampler's ACTUAL schedule at run
time via resolve_soften_window(). 0.4 now means "soften the first 40% of the
steps" on any curve / step count — no per-schedule re-tuning.

Add an optional SIGMAS input + an `info` STRING output so the resolved window
(softened steps + sigma range) is visible in-graph via a text node. A coverage
line is also logged at run time. max_soften (amplitude) is unchanged.

Pure resolve_soften_window unit-tested (6 cases incl. curve-independence);
model wiring render-validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): anchor soften ramp to the schedule's start sigma, not sigma_max

Some schedules start below model_sampling.sigma_max (e.g. a curve whose first
sigma is 0.931). Anchoring the gain ramp to sigma_max under-softened step 0 on
those. Use the run's actual first sigma (sigmas[0]) as the reference so the ramp
spans step 0 -> cutoff cleanly on any curve. Drops the now-unused sigma_max
lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: update CHANGELOG for Soften Schedule protect_until + info output

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — soften_range is a LEVEL (top % of sigma range), simpler text

The step-fraction `protect_until` was a mistake: it re-interpreted a tuned value
like 0.03 as "3% of the steps" (~0 steps = off), when it had always meant the
LEVEL "top 3% of the sigma range" (~7 steps on a front-loaded curve). Revert to
the level: `soften_range` directly = the top fraction of the sigma range to ease
(sigma-progress). Drop the confusing internal value scaling on both widgets so the
numbers mean what they say (max_soften 0.10 = 10% unpin; soften_range 0.03 = top
3%). Tooltips + info readout trimmed to one plain line. `steps_in_range` resolves
the level -> actual steps for the `info`/log readout (display only). Same slots,
so a placed node keeps its values under the new names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — read anchor sigma fresh each step

Runtime robustness fix from PR #266 review (findings #1 + #4, one root
cause). The denoise_mask_function anchored the schedule's start sigma
once into a cached state["ref"]:

- If the first runtime lookup failed, it cached ref=0.0 and returned the
  original mask for the rest of the run — the node silently off (#1).
- The one-time anchor went stale if the patched clone was later run under
  a different sampler schedule (#4).

Now read the anchor FRESH each step from extra_options["sigmas"], with
the connected SIGMAS input as fallback. Constant within a run (no
per-step drift), re-derives for a different run, and can never latch off.
This also gives the wired SIGMAS input a real runtime role beyond the
info readout. Two log lines collapse into one "active … = first N of M".

Tests unchanged (21/21 pass) — they cover the pure functions; no test
asserted the old caching behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — warn once when patched but no usable sigmas

PR #266 review finding #2. When the hook has no reference sigma from the
run AND none wired to the 'sigmas' input, it returns the original mask —
correct, but silent, so a patched node on a sampler path that doesn't
pass sigmas looked disabled with no signal. Emit a single warning
("soften inactive this run — connect SIGMAS") the first time it happens.

Behavior unchanged (still a clean no-op); this only surfaces the state.
Tests unchanged (21/21).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — log real step count from the wired schedule

PR #266 review finding P3 (diagnostic accuracy). The fallback kept only
connected_ref = sigmas[0] from the wired SIGMAS, so when a run omitted
extra_options["sigmas"] the coverage log fabricated a [ref, 0.0] schedule
and reported "first 1 of 1 steps" — even though the wired 16-step curve
covers "first 7 of 16". That contradicted the PR/changelog promise that
the same coverage line is logged each run.

Keep the WHOLE wired schedule (connected_sigmas) and report coverage
against whichever schedule is actually in force: the runtime one, else
the wired one we anchored to. Anchoring behavior is unchanged.

Verified: with extra_options={} and a wired 16-step front-loaded curve,
the log now reads "first 7 of 16 steps", matching the info output; the
mask still softens (0.2 -> 0.28 at peak gain). 21/21 tests, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — coverage log follows the anchor's schedule

Review P3: when runtime sigmas were present but malformed/empty while a
valid SIGMAS input was wired, behavior fell back to the wired anchor but
the coverage log still read the malformed runtime object and could
report "first 0 of 0 steps". Bind cov to whichever schedule the anchor
actually came from, and extend the fallback to a zero-start runtime
schedule (unusable) instead of going inert while a wired schedule is
available. Tooltip now states the wired input's runtime-fallback role.

Adds wiring tests for the denoise-mask hook: runtime-anchor coverage,
malformed/zero-start runtime fallback to wired, and warn-once inertness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: CHANGELOG — note wired sigmas' runtime-fallback role

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ltx): Soften Schedule — non-finite sigma anchors are unusable

Review P2: float() accepts nan/inf, and the fallback guards (ref <= 0.0,
not ref) are both False for nan — so a runtime schedule anchored at nan
kept ref=nan, computed gain=nan, and returned a nan-poisoned mask
instead of falling back. Route every anchor read (runtime and wired)
through _usable_ref(), which requires a finite positive float; soften
gain also hard-returns 0 on non-finite progress, and a non-finite
per-step sigma falls back to the anchor. Adds unit + wiring tests for
nan/inf anchors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:06:23 +02:00
040b30733d feat(matte): first-party mask-guided one-step video matting nodes (#269)
* feat(matte): first-party mask-guided one-step video matting nodes

Adds matte/ — an independent implementation of the published VideoMaMa
one-step SVD method (matte/pipeline.py) plus Koolook's temporal windowing,
subject-focused crop/stitch, resolution control (/64, shortest-side) and
auto OOM resolution-backoff. Nodes: KoolookMatteLoader, KoolookMatteSampler,
KoolookMatteFocusCrop, KoolookMatteFocusStitch.

Registered via the existing guarded _merge_node_group("Koolook Matte",
".matte"), so a missing diffusers disables only these nodes. diffusers is
already pulled in by the radiance/whatdreamscost forks, so no new dependency.

Loads (never bundles) the VideoMaMa UNet (CC-BY-NC) + SVD VAE at runtime;
matte/NOTICE records the model licenses and user responsibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(dev-sync): include matte/ in RUNTIME_PATHS

So `sync_to_dev` copies the new matte node package into the live ComfyUI
install alongside the rest of the koolook pack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): raw cond latents; fp16 default; preserve checkpoints on dev-sync

pipeline.py: _encode_latents must NOT divide by vae scaling_factor. The
VideoMaMa UNet is trained on raw VAE latents (matching okdalto's
VideoInferencePipeline, which multiplies by scaling_factor then divides it
back out -> net x1). Dividing over-scaled the conditioning ~5.5x and produced
a systematically blurry matte on every frame. Verified 5/5 vs the desktop /
vanilla reference on identical single-frame inputs.

nodes.py: harden the loader precision fallback so fp16 is the true default -
only an explicit "bf16" opts out; a stale "auto" widget now resolves to fp16
instead of bf16.

sync_to_dev.py: the rmtree+copytree mirror deleted matte/checkpoints/ (the
gitignored ~10GB SVD+VideoMaMa weights) on every matte sync, forcing a
re-download. Add PRESERVE_IN_DEST + selective _clean_dest so runtime-
downloaded model data survives a sync.

docs: add docs/maintainers/matte-pipeline.md (latent-scaling invariant,
black-output causes, fidelity levers, single-frame + vanilla-oracle debug
loop, checkpoint preservation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 review

- license: align matte to the repo's GPL-3.0 (was self-contradictory: claimed
  "MIT licensed - see LICENSE" while LICENSE is GPL-3.0). pipeline.py header,
  NOTICE, and the maintainer doc updated. NOTICE still credits VideoMaMa
  (CC BY-NC) + SVD (Stability Community License).
- nodes.py _ensure_2d_mask: squeeze first, then handle [H,W,C]/[C,H,W]
  explicitly and RAISE on ambiguous shapes -- previously [H,W,3] silently
  became [W,3] (wrong geometry).
- nodes.py OOM backoff: catch RuntimeError (OutOfMemoryError subclasses it, but
  cuBLAS/cuDNN/older paths raise plain "RuntimeError: CUDA out of memory"); the
  existing message guard still filters non-OOM errors.
- nodes.py: warn when the guide mask is empty instead of silently falling back
  to full-frame.
- sync_to_dev.py: PRESERVE_IN_DEST is now an exact relative path
  ("matte/checkpoints"), not a bare name matched at any depth; handle symlinks
  before is_dir() in _clean_dest so a symlinked dir isn't recursed/rmtree'd.
- CHANGELOG: add the Koolook Matte nodes under [Unreleased] with the
  non-commercial model-license caveat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 re-review

- sync_to_dev.py [BLOCKING]: a top-level symlinked dest (e.g. target/matte -> X)
  was passed straight into _clean_dest()/copytree(), which follow the link and
  can mutate files outside the target. Now unlink a symlinked dest and own the
  path as a real dir (checked before exists(), which follows symlinks). Verified
  with a repro: the symlink target's file survives.
- nodes.py: force float32 on a CPU fallback (CUDA unavailable). Half precision
  is unreliable on CPU; fp16 kernels would error. Warn that CPU is very slow.
- docs: correct the preserved-path guidance to the target-relative
  "matte/checkpoints" (was a bare "checkpoints" name); reword "near-verbatim
  reimplementation" -> "independent implementation targeting behavioural parity"
  to match the NOTICE/pipeline framing.
- nodes.py: also warn on a degenerate (<8px) guide-mask region before the
  full-frame fallback (was silent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(matte): address PR #269 re-review (CI red + FocusCrop frame check)

- nodes.py: KoolookMatteFocusCrop now validates images vs coarse_mask frame
  counts (raises like KoolookMatteSampler.run_inference). A mismatch previously
  made cropped_images/cropped_mask diverge and silently drop/misalign frames at
  FocusStitch.
- pipeline.py: split "vae.to('cpu'); _clear_cache(...)" onto two lines (Ruff
  E702 was failing Lint & Smoke).
- constraints-test.txt: bump pillow 12.2.0 -> 12.3.0 (pip-audit CVE, fixed in
  12.3.0). Pillow is a leaf dep so the closure is unaffected; CI re-audits.
- README: note the Koolook Matte nodes need diffusers (already via the LTX /
  Radiance forks) and huggingface_hub for runtime weight downloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:03:12 +02:00
961a95b9bd feat(ltx-director): reference-image identity — Ghost Mask + schedulable guide attention (#264)
* feat(ltx-director): Ghost Mask reference image (REF / character sheet)

Add a no-LoRA reference-image identity anchor to LTXDirector__koolook, the
way a WAN VACE reference works: attach reference image(s) and the model keeps
the character's face / mouth consistent even when keyframes show a closed (or
different) mouth than the audio-driven motion needs. Targets the mouth /
identity drift seen in the talking-goat lip-sync workflow.

Mechanism (Ghost Mask): the reference image(s) are appended as masked guide
frames *after* the clean video region, so the transformer attends to them for
identity but they never appear in the output; the trailing reference frames
are then sliced off the result.

- LTXDirector__koolook: new optional `reference_images` (IMAGE) +
  `reference_strength` inputs, and appended `clean_latent_frames` /
  `clean_pixel_frames` outputs. All additive/optional -> saved workflows
  unaffected. Refs are appended AFTER snap/ease so they are never grid-snapped
  or ease-expanded; the auto latent is grown to hold them. ltx_director.py is
  under ruff's forks/ exclude.
- reference_ghost.py (new, Koolook-original, pure stdlib): trailing-reference
  geometry (insert frames, grown latent length, slice bounds). Unit-tested.
- k_clean_latent_slice.py (new root node `CleanLatentSlice`): torch.narrow
  slice to drop the trailing reference frames (start=0,
  length=clean_latent_frames). torch imported lazily; clamp math unit-tested,
  slice path tested when real torch is present (skips under the CI stub).
- Registered the node; added to sync_to_dev RUNTIME_PATHS.
- Attribution: the Ghost Mask APPROACH is adapted (idea only, reimplemented)
  from CGlide/WhatDreamsCost-CSGlide (main_cs @ 21dd076, GPL-3.0, license
  verified 2026-06-24, GPL->GPL OK). THIRD_PARTY.md, forks_manifest.yaml,
  UPSTREAM_PIN.yaml, CHANGELOG updated.

Phase 1: Ghost Mask only. The LoRA-backed Licon MSR prefix mode and the
editor's character-slot UI are intentionally NOT ported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sidebar): action-led star footer ("Give a ★ on GitHub")

Reword the GitHub discovery footer from the passive "Help creators find
us on GitHub" to an action-led "Help creators find us! Give a ★ on
GitHub", with the star as the explicit ask. Render the sentence as one
inline text flow (not separate flex children) so the footer's `gap`
doesn't space out the words; the ★ glyph keeps its gold styling inline and
now carries role="img"/aria-label="star" so it is announced. Verified
rendering in an isolated harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-director): Ghost Mask broke motion in the append-and-crop pipeline

The upstream guide pipeline (LTXDirectorGuide + LTXDirectorCropGuides)
appends every guide_data image as an extra latent frame and crops them
back off via `exact_crop_frames = latent.shape[2] - initial_latent_length`.
The Ghost Mask code (modeled on CGlide's CleanLatentSlice flow) ALSO
pre-grew the video latent by n_refs, so initial_latent_length became
clean+n_refs: CropGuides then left n_refs frames uncropped AND the audio
latent stayed at the clean length, desyncing the A/V joint transformer and
breaking motion.

Fix: don't pre-grow the latent. References are added to guide_data exactly
like timeline keyframes and ride the same append-and-crop path — the guide
node appends them and CropGuides removes them, audio untouched. Auto latent
is sized to the clean length again. CleanLatentSlice/clean_latent_frames
remain available for bare pipelines that don't use LTXDirectorCropGuides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): LTX Guide Reference Strength — per-stage reference boost

Adds a small Koolook node that rewrites the strength of ONLY the Ghost
Mask reference frames in guide_data (the trailing entries the Director now
tags with `reference_count`), leaving the keyframe pins untouched. Insert
it before a stage-2 refinement guide and set ~0.9 to lock identity during
the low-denoise pass without disturbing the motion set in stage 1 — the
deterministic form of "more reference in stage 2".

Chosen over vendoring the 600-line upstream LTXDirectorGuide (which would
force a node-ID rename + graph rewire): this is one small node inserted on
the stage-2 guide_data wire. The sigma-ramp alternative (ramp reference up
at low sigma) is deferred — the reference hook is noise_mask + a core-model
attention term, so it needs a live-model spike, not a clean static patch.

- Director tags guide_data["reference_count"] = n_refs.
- k_ltx_guide_reference_strength.py: pure strength rewrite (absolute /
  multiply, clamped), unit-tested; registered; added to sync paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): LTX Reference Bind Schedule — sigma-ramp the reference attention

Confirmed against core (comfy/ldm/lightricks/model.py): the reference's
guide attention strength is turned into a log-space additive self-attention
bias in `_build_guide_self_attention_mask`, which is rebuilt EVERY denoise
step and explicitly supports strength > 1.0 to amplify. That's a clean
per-step hook — so the "inverse sigma curve" idea is buildable, not just a
stage knob.

New node mirrors LTXAVBindSchedule: a model_function_wrapper captures the
per-step sigma and stashes a gain in transformer_options; an object-patch on
diffusion_model._build_guide_self_attention_mask multiplies the strength of
ONLY the trailing `num_references` guide entries (references are appended
after keyframes) by a smoothstep ramp — neutral early (motion forms), rising
to peak_strength late (identity locks during refinement). Keyframe pins and
noise-mask freezing untouched; orthogonal to reference_strength. One pass, no
re-noise, no LoRA.

- ref_gain ramp + _boosted_entries unit-tested (11 tests); model wiring
  validated by live render.
- Registered; added to sync paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): mark Reference Bind Schedule inert without an IC-LoRA

Live diagnostic on the AV model showed the builder is reached and the sigma
gain flows in, but resolved_guide_entries=None despite 3276 guide tokens.
Root cause: LTXDirectorGuide only records guide_attention_entries when an
IC-LoRA is active (`if is_lora_active:` gates _append_guide_attention_entry).
With no LoRA there is no per-guide attention bias to scale, so the
attention-ramp lever does not exist in the Ghost Mask path — it can only
attenuate/amplify entries that an IC-LoRA created.

Rather than silently no-op, the node now logs a clear INERT warning when the
entry list is empty, and the docstring/DESCRIPTION/CHANGELOG state the
IC-LoRA requirement. The node remains correct for the IC-LoRA path (its
partner). No-LoRA reference strengthening stays on the noise-mask lever
(reference_strength / LTX Guide Reference Strength).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): Reference Bind Schedule works without an IC-LoRA (fabricates entries)

The attention-bias identity channel (_build_guide_self_attention_mask) is
base-model functionality, but LTXDirectorGuide only populates the per-guide
entries when an IC-LoRA is active, so the node was inert in the no-LoRA Ghost
Mask path. Now, when entries are missing, the node FABRICATES them for the
trailing reference tokens: it splits num_guide_tokens across
(num_keyframes + num_references) guide frames and assigns the sigma-ramped
strength to the reference share, then hands off to the core builder. This
gives the reference the same amplified-attention channel an IC-LoRA would use
— inside the Director, no guide-node vendor, no graph rewire (the node is
already in the user's setup). When an IC-LoRA IS active it scales the real
entries as before.

- New `fabricate_reference_entries(num_guide_tokens, num_references,
  num_keyframes, strength)` (pure, unit-tested) + `num_keyframes` input.
- Wrapper now does scale-or-fabricate; clearer one-shot diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx): keep num_keyframes last so pre-existing RefBind widgets stay aligned

num_keyframes was inserted mid widget-order, which shifted ramp_start/ramp_end
for workflows saved before it existed. Move it to the end of the optional
inputs so saved [num_references, peak_strength, ramp_start, ramp_end] values
keep mapping correctly; num_keyframes just defaults to 0 on reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(test): drop unused pytest import (ruff F401)

The guide-reference-strength test uses plain asserts; the stray
`import pytest` tripped ruff F401 and reddened the Lint & Smoke CI job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-director): default keyframe guidance 0.8 (smooth), not 1.0

The timeline editor auto-built the guide_strength string from each clip's
per-segment guideStrength, filling 1.0 for any clip left unset (and the
Python fallback matched). 1.0 is a hard pin, so a clip you forgot to lower
snapped instead of transitioning smoothly. Unset clips now default to 0.8
in the editor (display + serialization) and the node; explicit per-clip
values still override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ltx): Keyframe Soften Schedule — smooth motion early, sharp poses late

New model-patcher node LTXKeyframeSoftenSchedule: sigma-schedules the LTX
keyframe denoise-mask pins so the model forms smooth motion between poses
early (pins loosened at high sigma) and resolves sharp poses late (pins
restored by a crossover). Uses the sampler's per-step denoise_mask_function
hook — no IC-LoRA required — and adds no guide frames (so it can't fabricate
extra poses). Audio-safe: only the video half of the NestedTensor A/V mask is
softened; the audio mask is untouched, so lip-sync is unaffected.

Two normalized 0-1 widgets map onto the sensitive useful range: max_soften
(ease, ->0-0.20) and crossover (invention window, ->0-0.05). Pure soften_gain
ramp unit-tested (11 tests); model wiring render-validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(review): chain denoise hook, optional_latent clean-length, audio test, CGlide manifest

Addresses PR #264 review findings:

- MEDIUM (Soften node): chain a pre-existing denoise_mask_function instead of
  clobbering it (mirrors the sibling LTX patchers' wrapper-chaining), so the
  node composes with anything else that sets that hook.
- HIGH #2 (Director): clean_pixel_frames/clean_latent_frames now derive from the
  ACTUAL optional_latent shape when one is connected, so Ghost-Mask reference
  geometry and the reported counts follow the real latent instead of the
  timeline duration (with a safe fallback + warning).
- LOW (Soften node): reconcile docstring (crossover is sigma-progress, maps to
  internal 0-0.05), one-time warn on the sigma-read fallback, and extract a
  testable soften_denoise_mask() helper.
- LOW: lock the audio-safety guarantee with NestedTensor-stub tests — video
  mask softened, audio mask untouched, part-ordering asserted.
- #6: add a formal forks_manifest.yaml entry for the CSGlide Ghost Mask
  idea-only adaptation (was prose-only in another entry's notes; THIRD_PARTY.md
  already carries the full attribution).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:30:46 +02:00
ba37ec3d41 feat: LTX Director 2.0.2 fork, keyframe grid-snap, and audio→video bind schedule (#259)
* feat(forks): LTX Director v2_0_2 fork + keyframe latent-grid snap (#258)

Bump the WhatDreamsCost Koolook fork to upstream 2.0.2 (pinned fe09f73) in a
new versions/v2_0_2 namespace, and add `snap_keyframes_to_grid` (default on)
to fix hard-pinned image keyframes "jumping" at latent-bucket edges.

Why: a Director image keyframe is injected as a single latent frame at the
segment's left edge, quantized to the model temporal stride s
(L(f)=0 if f==0 else 1+(f-1)//s). Pins near a bucket boundary round
ambiguously and two pins in one bucket collide. Snapping each pin to its
bucket center removes the ambiguity without changing which latent frame it
lands on; colliding pins are bumped to the next free bucket with a warning.

- forks/whatdreamscost_koolook/versions/v2_0_2/: vendored 2.0.2 sources with
  the v1.3.9 Koolook deltas carried forward (relay_overrides,
  audio_transcript_json, per-segment sigma — build_segments is byte-identical
  1.3.9->2.0.2 so the sigma patch drops in verbatim), plus new pure
  keyframe_grid.py snapper wired into the insert_frames build.
- Canonical LTXDirector__koolook rolls to 2.0.2 (entrypoint loads v2_0_2
  last); LTXDirector__koolook_v1_3_2 alias preserved; v1.3.9 kept on disk.
- LICENSE re-verified GPL-3.0 (2026-06-22). Manifest + THIRD_PARTY + CHANGELOG
  + audio-lipsync findings/CHEATSHEET updated.
- 37 new unit tests for the snapper; 107 fork tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(forks): append Koolook LTX Director widgets last for clean load

Move the v2_0_2 Koolook widgets (relay_overrides, audio_transcript_json,
snap_keyframes_to_grid) to the end of the schema, after override_audio. They
were inserted after img_compression, which shifted the last upstream widget
when an upstream-2.0.x-saved workflow is loaded onto LTXDirector__koolook.
With them last, an upstream graph's positional widgets_values map onto the
upstream widgets untouched and only the Koolook widgets append with defaults
(snap_keyframes_to_grid=on). Execution is unaffected (kwargs by name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(forks): make v2_0_2 a clean upstream-2.0.2 replica + grid-snap only

Per maintainer direction: the v2_0_2 Koolook LTX Director should faithfully
replicate upstream 2.0.2 (so all latest features work) rather than carry the
older customizations, which are unused.

- Drop relay_overrides, audio_transcript_json, and the per-segment sigma patch.
  prompt_relay.py and patches.py are now vendored VERBATIM from upstream 2.0.2;
  _relay_overrides.py removed.
- ltx_director.py is upstream 2.0.2 verbatim except two additive changes:
  the namespaced node ID LTXDirector__koolook, and the snap_keyframes_to_grid
  latent-bucket keyframe snap (issue #258), appended last in the schema.
- v2_0_2 registers only LTXDirector__koolook; the legacy
  LTXDirector__koolook_v1_3_2 ID stays backed by the v1_3_9 package. v1.3.9
  (with the older customizations) remains on disk for rollback.
- Manifest / THIRD_PARTY / UPSTREAM_PIN / CHANGELOG updated to the clean scope.
  grid-snap unit tests unchanged (107 fork tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(forks): vendor upstream 2.0.2 timeline editor JS for LTXDirector__koolook

The v2_0_2 Python was upstream-exact but the node still rendered with the old
1.3.9-era timeline editor (fewer toolbar actions, no IC-LoRA track, leaked
*_second widgets, frame_rate NaN). Replace web/whatdreamscost_koolook/
ltx_director.js with upstream 2.0.2's js/ltx_director.js vendored verbatim,
retargeted with only two edits: unique extension name 'LTXDirector_Koolook'
and node guard nodeData.name === 'LTXDirector__koolook'. UX now matches
upstream 2.0.2 exactly; only the Python differs (grid-snap).

The old koolook base64-scrub deltas are dropped as obsolete — upstream 2.0.2
persists timeline media via imageFile/audioFile + /api/view URLs (saved
timelines are already lean), and the localStorage quota guard stays in the
global web/koolook_draft_guard.js (upstream does not override localStorage).

Rewrote tests/sidebar/test_ltx_director_draft_guard.py to the new contract
(no embedded guard, retargeted to the koolook node, pinned-upstream
provenance). 110 fork+contract tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(scripts): resolve .env from main repo when dev-sync runs in a worktree

Unrelated to the LTX work: plain `dev-sync` failed with "KOLOOK_COMFYUI_DEV_PATH
not set" from any worktree because scripts/sync_to_dev.py only loaded
`.env` from the worktree root, where the committed `.env` does not exist
(it lives in the main checkout). sync_to_dev_audio.py already had the
worktree->main fallback, which is why dev-sync-audio worked.

Move that fallback into a shared sync_to_dev.find_dotenv() and use it in both
scripts so they can't drift. Adds regression tests for the worktree fallback,
the worktree-own-.env preference, and the not-a-worktree case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gitignore root-level generated workflow JSONs

Node-swapped/converted workflow copies (e.g. *.koolook.json) are dropped at
the repo root during dev iteration; they're scratch deliverables, not package
content. Ignore root-level *.json only (leading `/`), with a `!config.json`
exception, so the 30+ tracked JSON in subdirs (test fixtures, web/, .claude/,
docs/runs/*/workflow.json) stay tracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTX A/V Bind Schedule node — gate audio→video binding by sigma

New Koolook node LTXAVBindSchedule decouples big motion from lip-sync in LTX
2.3. The AV transformer (comfy/ldm/lightricks/av_model.py, BasicAVTransformerBlock)
adds audio into video via a gated `audio_to_video_attn` per block, run only when
transformer_options["a2v_cross_attn"] is set. This node patches each block's
audio_to_video_attn.forward to scale its output by a per-step gain, and a
unet-function-wrapper computes that gain from the current sigma: early/high-sigma
steps run near `early_gain` (coarse motion settles audio-blind), ramping
(smoothstep) to full at low sigma (lips bind onto settled motion). One pass, no
re-noise — the fix for keyframe transitions jumping on audio peaks.

early_gain=1.0 reproduces stock behavior; a graph without the node is unchanged.
Registered as a root node + added to RUNTIME_PATHS so dev-sync ships it. Pure
ramp (a2v_gain) unit-tested (9 cases); model wiring validated by live render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTX A/V Bind Schedule — add transition-window mode (lips synced + smooth)

The global sigma ramp decoupled motion from lips but desynced the mouth (lip
timing is coarse motion too, so holding audio out globally moved the mouth
audio-blind). Add a WINDOWED mode: audio binds at full strength everywhere
(lips sync across the whole clip) EXCEPT a +/-transition_window band around each
keyframe transition, where the sigma ramp applies — so only those moments are
motion-protected early and rejoined late.

Implemented as a per-latent-frame gain on a2v_out: read grid_sizes from
transformer_options to map frame-major video tokens -> latent frames, build a
gain vector (1.0 outside bands, smoothstep-tapered to sigma_gain at each
transition center), expand per-frame -> per-token, scale. New optional inputs:
transition_frames, total_frames, transition_window. Empty transition_frames =
prior global-ramp behavior. windowed_frame_gains unit-tested (7 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: LTXAVBindSchedule — one-shot diagnostic log (shapes/grid/mode)

Logs out.shape, grid_sizes, transition_fracs, and whether the per-frame mask
fired vs fell back to scalar — so a single render reveals whether the a2v patch
hooks the model and whether the windowed token->frame mapping aligns, instead
of guessing blind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(audio-lipsync): record validated a2v bind-schedule knee (0.35/0.25/0.55)

Promote the finding that LTX 2.3's audio->video coupling must be sigma-ramped
(not full-on) to keep motion smooth AND lips synced, with the validated knee
early_gain=0.35 / bind_start=0.25 / bind_end=0.55 on the 8-step base pass, plus
the trade-axis table and the per-frame-mode caveat (grid_sizes None + appended
guide frames -> scalar fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(forks): keyframe ease — strength-ramped neighbor pins to de-robotic transitions

Adds `keyframe_ease` / `ease_falloff` to the Koolook LTX Director (default off).
Each hard image pin keeps its exact center pose, but emits same-pose neighbor
pins one latent bucket apart at decreasing strength (center*falloff**k), so the
model glides into and out of the locked pose instead of snapping to a dead stop
— softening the "stop/dissolve/stop" robotic feel without softening the pose.

Implemented as a pure list expansion (`expand_keyframe_ease` in keyframe_grid.py,
6 unit tests) applied to guide_data after grid-snap, so the upstream guide node
is untouched (no fork). Center stays exact; neighbors that fall off-clip or onto
another center are skipped. Costs extra guide frames (more compute), hence opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(audio-lipsync): downgrade a2v knee to 'promising' — note lows-placement confound

The 0.35/0.25/0.55 win was measured on a clip whose transitions sit on audio
lows, so 'no jumps' may be the placement, not the node. Mark the controlled
on-peak test (early_gain 1.0 vs 0.35 on the same on-peak layout) as still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: mark vendored WhatDreamsCost fork files linguist-vendored

The koolook Director ships upstream 2.0.2 code (the 8.5k-line timeline-editor JS
+ verbatim prompt_relay/patches) so the pack runs and distributes, but it's
upstream, not authored here. Mark those linguist-vendored (drop from language
stats) and the JS -diff (collapse the large blob in PR diffs). Our edited/new
files stay attributed to us.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ltx-av): drop unused `import math` (ruff F401)

a2v_gain uses an explicit smoothstep polynomial, not math.*, so the
import was dead. Unblocks the Lint & Smoke CI gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(codeql): advanced setup that excludes vendored upstream code

CodeQL default setup cannot read a config file, so it scanned the
verbatim-vendored WhatDreamsCost editor (web/whatdreamscost_koolook/
ltx_director.js) and raised 4 high-sev DOM-XSS alerts in upstream code we
do not own. Switch to advanced setup with a committed config whose
paths-ignore mirrors the .gitattributes linguist-vendored block, so the
scanner treats vendored upstream as out-of-scope while still fully
scanning our own nodes and sidebar JS.

Languages match the prior default-setup matrix (actions,
javascript-typescript, python).

NOTE: requires disabling "default setup" once in repo Settings before
this workflow can run (GitHub rejects advanced runs while default setup
is enabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): scope vendored fork exclusions to v2_0_2

* fix(forks): preserve final LTX keyframe snap bucket

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:15:41 +02:00
malkuthro 24857d5e9c Release v0.4.4 (#263)
* Release v0.4.4: funding metadata

* docs: add GitHub footer to v0.4.4 notes
v0.4.4
2026-06-23 23:14:44 +02:00
malkuthro 29052ed369 ci: update actions for Node 24 runtime (#262) 2026-06-23 22:29:34 +02:00
b9a896041b feat(sidebar): add GitHub discovery footer nudge (#261)
Adds a discreet line directly below the update-available footer in the Kforge Labs sidebar: "Help creators find us on GitHub". Gated on the same showUpdateFooter flag so it shows in the live panel but stays out of the read-only A/B compare columns. Ko-fi/funding deliberately stays out of the sidebar (repo-only, tracked as a follow-up).

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:06:24 +02:00
malkuthro b6b95d8a85 docs: add Ko-fi funding link (#260) 2026-06-23 00:29:00 +02:00
malkuthro 575cfb7f63 Release v0.4.3: publish latest updates (#256) v0.4.3 2026-06-20 11:42:04 +02:00
ba39cb3658 Claude/goofy faraday d23e98 (#255)
* refactor(nodes): drop keyframe_batch alias from Easy Image Batch

The keyframe_batch input was a deprecated alias of keyframes_insert,
added in 0.4.1 during PR #246 review to keep pre-rename workflows
loading. The old name only ever existed in unreleased dev builds before
the rename, so no published workflow depends on it and the alias only
cluttered the node with a second IMAGE input.

Remove the input, its parameter, and the alias-routing fallback; the
node now exposes a single keyframes_insert insert input. Tests and the
behavior spec updated; CHANGELOG notes the removal under [Unreleased].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(policy): make backward compatibility opt-in for Koolook nodes

Invert the default: backward compatibility is no longer a standing
constraint for Koolook-created nodes (the root k_*.py custom nodes).
By default, make the cleanest change — rename/drop/reorder IDs, inputs,
and outputs freely, no deprecation aliases or _v2 suffixes. Compatibility
with saved workflows is only preserved when the maintainer explicitly
says "check backward compatibility".

Forks are explicitly excluded: anything under forks/ (including the
Koolook-original nodes exposed via SKIP_VERSION_SUFFIX) keeps full
back-compat discipline by default, no request needed. The
__koolook_vX_Y_Z collision suffix always applies.

Scoped across the binding instruction files and skills: CLAUDE.md,
copilot-instructions, node-versioning.md, versioning.md, and the
preflight-release / add-external-fork / tdd skills. The preflight
workflows check is reframed as a drift detector for Koolook IDs and a
hard back-compat gate for fork IDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: clarify keyframe_batch alias removal

* fix: retain keyframe_batch compatibility alias

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:16:38 +02:00
472960b919 docs(setups): add publish -> run loop architecture overview (#254)
* docs(setups): add publish -> run loop architecture overview

Entry-point doc that ties the existing published-setup docs together:
the loop in plain terms (Mermaid), a technical map of frontend JS ->
Koolook aiohttp routes/handlers -> ComfyUI engine, and the contract /
schema requirements. All file paths and symbols verified against source.

Diagrams use Mermaid per docs/maintainers/conventions.md. Linked from
the maintainer docs index as the "read this first" entry for the group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(setups): fix publish-run loop references

* docs: codify review-fix push rule

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:04:12 +02:00
malkuthro 543998e026 Release v0.4.2 (#253)
Bump 0.4.1 -> 0.4.2. Ships #251 (Comfy Registry node-list parsing fix) and records #252 (tag-driven automated release pipeline). First release cut via release.yml.
v0.4.2
2026-06-19 09:01:56 +02:00
malkuthro cb19c375c9 feat(ci): tag-triggered release automation (publish + GitHub Release) (#252)
Push a semver tag (vX.Y.Z) at the squash-merge commit on main to publish to the Comfy Registry and create the matching GitHub Release from the CHANGELOG (only on a successful publish). Pre-publish guard refuses any tag that is not on main (merge-base --is-ancestor); per-job least-privilege permissions. publish.yml kept as a manual fallback. Docs synced: releasing.md + release_checklist.md.
2026-06-19 08:49:40 +02:00
malkuthro 3e2b541a61 fix(registry): expose nodes to scanners (#251) 2026-06-19 08:39:44 +02:00
malkuthro bda4df576a Release v0.4.1 (#250)
* Release v0.4.0: update footer notice

* Release v0.4.1
v0.4.1
2026-06-19 01:38:40 +02:00
malkuthro ae7abba73b docs: refresh shipped help and tooltips (#249)
* docs: refresh shipped help and tooltips

* docs: add snapshot transfer guide section

* docs: refresh guide screenshots
2026-06-19 01:01:30 +02:00
88dbfedf86 feat(nodes): Easy Image Batch keyframes_insert + insert-over-source +… (#246)
* feat(nodes): Easy Image Batch keyframes_insert + insert-over-source + clean batch

- Rename optional input keyframe_batch -> keyframes_insert (node ID unchanged).
- Insert-over-source mode: when keyframes_insert AND source_batch are both
  connected, composite the inserts over the source cut window (instead of the
  placeholder background). keyframes_insert alone keeps the offset/reconstruct
  behaviour over placeholder.
- No image source connected -> clean placeholder batch sized by new
  width/height fallback widgets, instead of raising. width/height are appended
  LAST so older saved workflows keep their positional widgets_values alignment
  (fixes a mid-list insertion that shifted placeholder_color into source_frames).
- alpha_batch marks only inserted/placed positions; source background is not
  counted as placed.
- New pure planner plan_source_base_fill (torch-free) alongside
  plan_offset_placements; both unit-tested in CI.
- Docs: behavior matrix spec, interactive HTML demo, and a generated
  "all versions" ComfyUI demo workflow under docs/designs/.

Not yet implemented (specced as DRAFT in docs/designs, pending maintainer
approval + tie-break decision): image1-4 "super-overwrite" layer across all
modes, and removal of the Mode 2 unconnected-slot source_batch fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(deps): raise aiohttp [test] floor to >=3.14.1

The constraints lock already pins aiohttp 3.14.1 (the CVE-2026-54273..54280
fix line), but pyproject's [test] extra still declared aiohttp>=3.9.
Dependabot keys off pyproject, not the lock, so the open declared range kept
eight range-based alerts open even though the installed version is patched.
Raising the floor to >=3.14.1 aligns the declared range with the lock and
clears the alerts once this reaches the default branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Easy Image Batch image1-4 super-overwrite + source passthrough

Make the four manual slots the top compositing layer in every mode (select
and insert): a connected imageN overwrites output frame imageN_frame on top
of the sequence/inserts and the background. Higher slot wins a collision
(image4 > image1); a slot beats a sequence pick / insert at the same position.
Extracted as the torch-free plan_slot_overwrites planner + a shared
_apply_slot_overwrites helper used by both code paths.

Two model fixes ride along, per the finalised behaviour matrix:
- An unconnected slot now contributes nothing -- imageN_frame is only a
  placement target for a wired image, never a source_batch pick. The old
  unwired-slot fallback (which surfaced seemingly-random frames) is removed.
- source_batch + an empty frame list (no insert) now passes the source cut
  window straight through instead of a placeholder batch, matching the
  insert-mode empty-list passthrough.

Tests: new plan_slot_overwrites planner cases + fake-tensor behaviour tests
covering slot-beats-pick/insert, higher-slot-wins, passthrough-not-placed, and
the removed phantom fallback. Docstring, tooltips, user guide, design spec
(promoted DRAFT -> canonical), and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(nodes): assert selected_image_batch = picks/inserts + slots; drop matrix HTML demo

selected_image_batch already packs every placed position -- both the
source_frames picks / inserts and the manual-slot overwrites, with slots
winning on overlap. Add explicit select- and insert-mode tests asserting the
composition and ascending order, locking the behaviour in.

Also delete docs/designs/easy-image-batch-matrix.html (stale interactive demo
built against an earlier draft) and drop its companion reference from the
design spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Easy Image Batch passthrough inpaint mask + frame-range syntax

Two refinements to the Easy Image Batch passthrough/list behaviour:

- Short-source passthrough is now an extend/inpaint mask. When source_batch is
  connected and the frame list is empty, the covered source frames are kept
  content (alpha 0.0) and the uncovered tail becomes the selection
  (selected_image_batch / selected_frames) with alpha 1.0 -- "keep the first N,
  generate the rest". Full coverage -> no gap, empty selection. The selection
  is the gap here (inverse of select mode); insert mode is unchanged.
- The source_frames field accepts inclusive ranges: "1-5, 7, 9, 14-17" expands
  to 1,2,3,4,5,7,9,14,15,16,17, in both select and insert, via a shared
  torch-free parse_frame_tokens (descending/non-integer tokens warn).

Tests cover the gap selection (short/full/cut-start-offset/slot-in-gap), the
parser (ranges, bad tokens), and range syntax end-to-end. Docstring, tooltip,
user guide, design spec, and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(designs): remove draft-era Easy Image Batch demo workflow + builder

Delete easy-image-batch-demo-setup.json and build_easy_image_batch_demo.py
(the leftover draft-era demo, sibling to the already-removed matrix HTML) and
drop the companion reference from the behavior spec. A purpose-built demo will
replace them later; the spec matrix + the node remain the canonical reference.
Also drop the now-stale "(proposed)" from the spec title (it is canonical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-guide): remove stale v0.1.5 Easy Image Batch screenshot

The screenshot predates the node's keyframes_insert / super-overwrite /
passthrough rework and is unreferenced. Removing it to be re-created fresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: point Easy Image Batch docs at the bundled Select-and-Rebuild demo

Note the runnable demo workflow ("Easy Image Batch - Select and Rebuild",
bundled into the Kforge Labs Workflows sidebar folder via #248) in both the
user guide and the design spec, replacing the generated demo removed earlier.
Referenced by path (not a clickable link) since the workflow lands via #248.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nodes): address PR review — keyframe_batch alias + input guards

- Restore keyframe_batch as a deprecated alias of keyframes_insert so
  pre-rename workflows (origin/main exposes the input) keep loading; an explicit
  keyframes_insert wins. Follows docs/maintainers/node-versioning.md
  (alias-then-deprecate); removal deferred to a release boundary.
- Warn instead of silently returning a clean batch when source_frames is
  non-empty but no image source is connected.
- Cap frame-list range expansion at 8192 frames/token to guard against typos
  like 1-99999999.
- Docs: reframe the bundled demo-workflow note as forthcoming (the JSON lands
  via #248, not on this branch); document the alias, cap, and no-source guard in
  the user guide / design spec / CHANGELOG.

Tests: alias routing + precedence, no-source warning, range cap + boundary
(46 node tests). Fresh-eyes review: no correctness/compat issues (the IMAGE
alias input adds no widget-position shift).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: point Easy Image Batch docs at the now-merged Select-and-Rebuild demo

#248 merged to main and is now in this branch (via the main merge), so the
demo workflow exists at docs/workflows/kforge-labs-workflows/. Repoint the user
guide and design spec at it directly (present tense, real folder link) instead
of the earlier "coming soon / pending" framing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:51:16 +02:00
malkuthro 4383c2fc0b feat(sidebar): bundle Kforge demo workflows (#248) 2026-06-19 00:24:08 +02:00
6f158c9020 fix(nodes): Easy_LoadVideo empty-branch degrade (bundles #243 publish-tab cleanup) (#244)
* docs(setups): add setup runner quickstart runbook

Standalone reference for opening the published-setup mockup frontend
(setup runner simulator): the `<comfy-address>/koolook/setup_runner_simulator.html`
URL pattern, the live-registry vs setup-file (inspect-only) load paths,
and demo mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sidebar): close throwaway workflow tabs left by publish capture

Publishing a setup captures ComfyUI's API-format prompt by loading the
graph onto the canvas (graphToPrompt) and reloading the prior canvas.
In current Comfy each loadGraphData spawns an "Unsaved Workflow" tab, so
every publish left two stray tabs behind.

captureWorkflowApiPrompt now snapshots the open workflows before capture
and, afterward, re-activates the user's original tab and closes whatever
the capture opened, via app.extensionManager.workflow. All of it is
guarded and best-effort: the workflow-store API is Comfy-version
specific, so if it is absent capture still succeeds and behavior falls
back to today (no crash, no broken publish).

Covered by a source-pattern test. Needs live verification against the
1.45 frontend (dev-sync) to confirm the closeWorkflow API shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nodes): Easy_LoadVideo degrades to empty on an unselected folder branch

When an upstream mode switch routes a path into Easy_LoadVideo, both the
EXR sequence loader and the video loader execute eagerly. On the EXR
branch the video loader was handed a folder (no filename) and raised
ValueError, crashing the whole prompt -- even though that branch is
discarded downstream. Lenient sequence loaders just ignore a wrong-type
path; the video loader was the only one that hard-failed.

_compose_input_video_path now returns EMPTY_BRANCH_SENTINEL instead of
raising when input_path resolves to an existing directory. load_video
turns that into a correctly-shaped empty result (IMAGE zeros,
frame_count 0, empty AUDIO, empty video_info) matching VHS_LoadVideoPath
returns, so the dead branch no longer crashes the prompt. VALIDATE_INPUTS
and IS_CHANGED handle the sentinel too. A path that does not exist is
still a genuine mistake and keeps raising.

torch is imported lazily in the empty-result builder so the path logic
stays importable/testable without torch.

Covered by unit tests for the sentinel (absolute + relative dirs), the
still-raises case, and a load_video integration test that asserts the
empty result and that the loader never runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(setups): expose prompt as an always-on field in the publish frontend

The publish surface modelled `prompt` as a 4th source mode (mutually
exclusive with EXR/QT/Img) and hid it (visible:false), so a published
setup couldn't offer "pick a source AND describe the shot". This makes
prompt a standalone, always-on input.

- published_surface.js: prompt becomes visible + standalone + multiline,
  rendered independently of the source switch; the author's prompt-widget
  text is surfaced as a `placeholder` hint (submitted default is empty so
  an untouched hint is never sent), with a `help` line. The switch
  excludes standalone fields from its source options.
- setup_runner_simulator.js: renders always-on standalone inputs alongside
  the selected source (textarea + placeholder + help), includes them in the
  run payload, and updates the demo setup.
- setup_runner_simulator.html: .field-help styling.
- contract doc: documents the always-on prompt behaviour.
- launch.json: portability fix (hardcoded C:\Python313 -> python).

Contract test updated + a new test asserting prompt is always-on, carries
the placeholder, and is excluded from the switch source options (runs on
CI where Node is available). Behaviour verified in the runner simulator
(demo mode) in a browser: textarea + hint, EXR/QT/Img-only switch, prompt
stays visible across source changes, payload includes the typed prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): mirror always-on prompt field in the Python publish inference

The publish surface is inferred server-side in Python (koolook_setups.py
_infer_app_surface, called by publishSetup), which overrides the client.
The earlier change only updated the JS (published_surface.js), so
re-publishing still produced the old surface with a hidden prompt.

Mirror the JS here: prompt is a standalone, always-on input (visible +
multiline) with the author's prompt-widget text as a placeholder hint and
an empty submitted default; it is excluded from the source switch options.
Keeps koolook_setups.py and published_surface.js in sync.

Registry test updated to the new shape. Server + publish-contract suites
green (115); full non-sidebar suite green (530).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): keep the runner simulator polling through long renders

The poller used a fixed 30s total budget, so a multi-minute render (e.g.
428s) timed out mid-run with "Timed out waiting for run" even though
ComfyUI was still rendering and the run later landed as succeeded.

timeoutMs is now a "no-progress" window: while ComfyUI keeps reporting the
run as queued/running, the deadline is pushed forward, so a long-but-active
render never spuriously times out; only a stalled/vanished run gives up.
Default raised 30s -> 120s. The run status line shows elapsed seconds while
active so a long wait visibly ticks instead of looking frozen.

Happy path (queued -> running -> succeeded) unchanged; verified in the
runner simulator (demo mode) in a browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(setups): serve simulator assets with no-store so dev-syncs take effect

The runner-simulator HTML/JS route returned no cache headers, so browsers
cached the assets and kept running stale JS after a dev-sync -- e.g. the
old 30s poller persisted client-side even though the server was serving
the long-render fix, which looked like "the fix didn't land". Serve these
dev-synced assets with Cache-Control: no-store so a normal reload always
picks up the current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(setups): add live run/poll smoke check for the published-setup API

tools/check_setup_run.py queues a published setup through the same routes
the runner simulator uses and polls the run endpoint with the same
deadline-extension logic, printing the status timeline. Lets us self-verify
end-to-end run tracking against a live ComfyUI (e.g. "does a long render
reach succeeded?") without driving the browser UI.

Used it to confirm the run/poll server flow is correct (terminal status
detected in ~2s; run-000001 reported succeeded), isolating the simulator's
earlier "Timed out" to stale browser cache rather than a server bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): Koolook_NextVersion auto-detects the next output version

Adds filesystem-aware version auto-detection so a re-run lands on a fresh
version instead of failing with "Output file already exists and overwrite
is disabled" (the real EasyAIPipeline failure seen on the RMGB setup).

- koolook_versioning.next_version_token(directory, name, ...): scans for
  existing <name>_vNNN files AND sequence-folders (handles name_v002.png,
  name_v002.0001.exr, name_v003/), returns highest+1 zero-padded; missing/
  empty dir -> start (v001); exact base-name match so other shots never
  bump this one; empty name matches bare vNNN; configurable prefix/padding.
- Koolook_NextVersion node (Koolook/Publish): folder + name -> next version
  token. Wire once into both the EXR and video version inputs so branches
  stay in sync. IS_CHANGED returns NaN to re-scan every run.

Tested: helper edge cases + node registration/run. Preflight clean (20
node IDs). Full non-sidebar suite green (539).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(nodes): 'auto' version keyword for EasyAIPipeline + Easy_VideoCombine

No new node or wiring needed: type 'auto' (or 'next') in the version field
and the node scans its own output folder for existing <name>_vNNN entries
and writes the next free version -- fixing "Output file already exists and
overwrite is disabled" without manual version bumps.

- koolook_versioning.is_auto_version(): detects the auto/next keyword.
- EasyAIPipeline (build_pipeline_outputs): on auto, scans the version-folder
  parent (base, or base/shot/method when subfolders are on) for vNNN dirs and
  picks the next; version computed after path segments so the scan dir is known.
- Easy_VideoCombine: on auto, resolves the token after the output dir + name
  root are composed, scanning that dir for <name>_vNNN outputs.
- Non-auto/typed/wired tokens and disable_versioning are unchanged.

Tests: is_auto_version keywords + EasyAIPipeline auto (v003 after v001/v002,
v001 first run). Full non-sidebar suite green (542), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(nodes): drop redundant Koolook_NextVersion node

The 'auto' version keyword on EasyAIPipeline + Easy_VideoCombine covers
auto-detection without any new node or wiring, so the standalone
Koolook_NextVersion node is redundant. Remove it (and its node test) to keep
the node list clean; the shared next_version_token helper stays (the 'auto'
keyword uses it).

Node-ID count back to 19; full non-sidebar suite green (541), preflight clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): satisfy security gate for setup smoke check

* fix(pr244): address verified review findings

* fix(video): fail if relative auto-version scan root is unavailable

---------

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
2026-06-19 00:10:59 +02:00
malkuthro e165c96fcc fix(ci): enforce dependency audit gate (#247) 2026-06-18 19:40:31 +02:00
malkuthro 30d51cf365 fix(loop): harden Koolook_LoopStatus auto-queue (#241)
Auto server_url detection (widget defaults to 'auto'); the connected index wire is authoritative for frame-index detection (a stale numeric label no longer overrides it); fold in #207 (string-bool auto_queue_next + detected index-node logging); review hardening (IPv6 / bare --listen, Bandit B104, blank server_url, debug diagnostics).
2026-06-18 18:16:08 +02:00
malkuthroandClaude Opus 4.8 df34ddcf1a feat(nodes): Easy Image Batch offset mode via keyframe_batch input (#242)
Add the inverse of the node's select behaviour. When the new optional
keyframe_batch IMAGE input is connected, the node scatters a packed
sequence back onto the source_frames positions (i-th frame -> i-th listed
number, ascending), placeholder fill in between, at total_frames length.
This closes the select -> process -> reconstruct loop: feed a processed
selected_image_batch plus the same selected_frames list back in to rebuild
the original spacing.

The switch is wire-driven (source_batch and image1-4 are ignored while
keyframe_batch is connected, with a console note); count mismatches and
outside-cut positions are reported in the end-of-run summary. Placement
logic is a pure, torch-free planner (plan_offset_placements) so it is
unit-tested in CI, where torch is unavailable. Fully backward-compatible:
unconnected keyframe_batch preserves the existing select path exactly.

Tests: 10 new torch-free tests; full suite 630 passed, 1 skipped; preflight_release green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:45:10 +02:00
malkuthroandClaude Opus 4.8 ae2480eee9 feat(setups): publish success feedback + published-setup discoverability (#227) (#239)
* feat(setups): publish success feedback + published-setup discoverability (#227)

Post-publish feedback (issue #227 Part 1):
- POST /koolook/api/setups now returns the on-disk storagePath
- new POST /koolook/api/setups/reveal opens the published-setups folder
  (shared _open_dir_in_file_manager helper; the snapshot reveal route was
  refactored onto it, behaviour-identical)
- the publish modal renders a success card reusing the snapshot Save dialog's
  "Saved to ... Open folder" language (Open folder / Copy path / Close)
  instead of closing silently; a draft publish still gets a confirmation

Published discoverability (reuses the tag system, no new schema):
- publishing auto-tags the source workflow `published` -> a "published" badge
  in the Workflows tree + the Tags-section "published" pool
- a new "P" toggle in the sidebar Tools row prunes the Workflows tree to
  published-only, folders force-expanded, so setups show in their origin
  structure; toggling off restores the full tree
- workflowHasTag() shared helper (unit-tested)

Deferred, still open on #227: separate destination/library setting (Part 2),
relocating the storage folder beside the snapshot library, and clearing the
filter when entering Compare mode.

Tests: +server (storagePath, reveal route happy + 404), +client (reveal
client, buildPublishSuccessView view-model), +sidebar (workflowHasTag).
Full suite: 610 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(setups): store published setups beside the snapshot library (#227)

- the published-setup registry path now derives from the configured snapshot
  library (`_configured_dir().parent / koolook-published-setups/setups.json`)
  instead of a fixed user-dir folder, so it follows libraryPath /
  KFORGELABS_PRESETS. The default config resolves to the same path as before
  (target == legacy), so existing default installs are unchanged.
- non-destructive one-time migration: copy a legacy user-dir registry to the
  new location when the new one is absent, leaving the original in place.
- sidebar: clear the "P" published filter when entering Compare mode so it
  doesn't render both compare columns filtered with no toggle to undo it.

Tests: +tests/server/test_published_setups_relocation.py (path beside library;
migration copy / no-op-when-present / no-op-when-no-legacy). Full suite: 614
passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(tests): remove unused pathlib.Path import (ruff F401)

The relocation test only uses the `tmp_path` fixture, not `Path` directly, so
`from pathlib import Path` was unused and failed the Lint & Smoke (ruff) gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): address PR #239 review — migration masking + tag persist

- HIGH: a failed legacy->new migration no longer masks real setups with the
  bundled sample. The default registry keeps the legacy file as its primary
  storage when migration could not produce the new file, so stranded data
  stays visible. (+2 tests: keeps-legacy-on-copy-failure / uses-new-on-success)
- MEDIUM/LOW: the publish flow now awaits the `published`-tag persist so the
  success card reflects a committed tag (persist failures already surface via
  persistMutation's critical toast), and warns when the source workflow could
  not be tagged (vanished mid-publish) rather than on a harmless republish.

Full suite: 616 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(setups): harden migration against partial/corrupt relocation target

Addresses the remaining HIGH from the PR #239 consolidated re-review: a
zero-byte/partial/corrupt setups.json at the relocated path could shadow the
intact legacy registry (existence was treated as proof of migration).

- migration now copies via temp file + atomic os.replace (the same pattern
  FileSetupStorage.save_setups uses), so an interrupted copy can never leave
  a partial setups.json at the target; failed-copy temp files are cleaned up.
- primary selection is corruption-aware: when the relocated file exists but
  does not parse as a setups document while the legacy file is intact, the
  registry keeps serving the legacy file and logs a loud diagnostic. The
  unreadable file is left untouched (never overwritten) as evidence. A valid
  relocated file stays authoritative; if both are corrupt, the relocated path
  stays primary so storage diagnostics point at the current location.

Tests (+4): corrupt-target-prefers-intact-legacy (file left untouched),
valid-target-stays-authoritative, both-corrupt-keeps-target, and
interrupted-copy-leaves-no-partial-target-and-no-tmp-litter.

Full suite: 620 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:54:12 +02:00
malkuthro 91cee1daae docs(setups): add external frontend kit (#236)
External frontend kit handoff for published-setup consumers; refreshes full-circle/status docs and neutralizes stale current-PR apiPrompt wording per review #4485848881.
2026-06-12 16:43:51 +02:00
malkuthroandClaude Fable 5 c6a155a033 fix(web): generation-agnostic Comfy draft-quota guard (#237)
* fix(web): generation-agnostic Comfy draft-quota guard

"Failed to save workflow draft" toasts returned on ComfyUI frontend
1.44+ because draft storage moved to new localStorage key names
(per-workspace V1 keys, then a V2 index+payload scheme) that the old
exact-name guard embedded in ltx_director.js never matched - and the
1.44 V2 layer latches a session-wide storage-unavailable flag after one
failed save+evict cycle, re-toasting on every edit until reload.

Move the guard to its own global extension, web/koolook_draft_guard.js:

- match draft keys by prefix (Comfy.Workflow.Draft) across all known
  generations, with unknown future keys as last-resort evictions
- rescue quota failures inside localStorage.setItem (evict the oldest
  draft across generations, retry) before Comfy's own catch sees the
  error, so the save succeeds and the latch never trips
- boot prune: drop the suffixed-V1 dead weight the frontend's V1->V2
  migration never deletes, scope corrupt-key cleanup to the offending
  key, drop oversized entries, enforce a total draft budget
- ship the guard with dev-sync-audio so a scoped sync cannot strand a
  dev install guard-less

Behavior is covered by tests/js/test_draft_guard.mjs (12 scenarios,
wired into CI next to the existing node smoke test); static integration
contracts live in tests/sidebar/test_draft_guard.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): address deep-review findings on the draft guard

- latch the "still full" hint toast once per session so a genuinely
  full origin cannot re-toast on repeated failing autosave attempts;
  regression assertion added to the exhaustion scenario
- document the bounded-eviction termination contract and the
  post-rethrow storage-unavailable latch at the wrapper, plus the
  budget's V1-blob-only (1.5M) -> all-generations (2M) scope widening
  (also now stated in the CHANGELOG entry)
- version-robust wording for the suffixed-V1 cleanup rationale: the
  shipped 1.44 migration leaves the keys behind (verified against the
  installed 1.44.19 dist - no removeItem exists there), later
  frontends clean up on successful migration, and an interrupted
  migration can leak the keys on any version
- comment the intentional best-effort swallow in the toast helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:29:25 +02:00
malkuthroandClaude Fable 5 5da590120c fix(tests): canonicalize expected writer filepath on Windows (#238)
* fix(tests): canonicalize expected writer filepath on Windows

build_pipeline_outputs canonicalizes output paths to forward slashes on
every platform, but the execution-map writer test built its expectation
from str(tmp_path), which yields backslashes on Windows — failing there
while ubuntu CI stayed green. Normalize the expected prefix the same way
test_easy_ai_pipeline.py already does. No-op on POSIX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: run pytest job on a windows-latest matrix lane

The suite is now Windows-clean (previous commit), but only ubuntu ran in
CI - the path-separator regression this PR fixes was invisible until a
local Windows run. Add windows-latest to the pytest job so that class of
break gates PRs. fail-fast off so a Windows-only failure still reports
alongside the green ubuntu lane.

Safe rename: Protect-main requires only "Lint & Smoke" and "Security
Gate", so Pytest -> "Pytest (os)" breaks no required check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:41:42 +02:00
malkuthro 1732030175 fix(setups): resolve switch-selected run results (#235)
* fix(setups): resolve switch-selected run results

* fix(setups): harden switch result reporting

* fix(setups): reject stale execution map writers
2026-06-09 00:27:45 +02:00
malkuthro c3c34f0171 Capture API prompt when publishing setups (#233)
* feat(sidebar): capture API prompt when publishing setups

* feat(setups): preview and load canonical setup records

* fix(setups): close publish review gaps
2026-06-08 20:28:35 +02:00
malkuthro 85206ab321 feat(setups): add external runner simulator (#231)
* feat(setups): add external runner simulator

* feat(setups): render app-surface simulator form

* docs(setups): capture publish-to-run frontend plan

* docs(setups): preserve sidebar inventory breadcrumbs

* fix(setups): address runner simulator review findings

* fix(setups): validate simulator run payload shape
2026-06-08 08:30:10 +02:00
malkuthro 16c1b42760 Add published setup execution API (#230)
* feat(setups): add published setup execution API

* fix(setups): expose app surface run results

* test(setups): harden execution API failures
2026-06-07 23:08:12 +02:00
malkuthro 83b170bdae feat(setups): add publish contract nodes (#228)
* feat(setups): infer app contract from named fields

* feat(setups): add publish contract nodes

* feat(setups): split publish result contract

* fix(setups): resolve publish output reroutes in preview

* fix(setups): validate app surface on read

* fix(setups): harden app contract validation
2026-06-07 22:02:16 +02:00
malkuthro 7cffe0702e fix(setups): publish simple widget-only workflows (#223)
* fix(setups): publish simple widget-only workflows

* fix(setups): read subgraph and named widget defaults

* fix(setups): allow optional publish descriptions

* fix(setups): reject incomplete widget-only values
2026-06-07 21:50:54 +02:00
malkuthro 3b35e6a195 feat(setups): infer setup surface from workflow groups (#222)
* feat(setups): infer setup surface from workflow groups

* fix(setups): validate inferred setup surfaces on read

* fix(setups): reject boolean setup geometry
2026-06-07 14:45:16 +02:00
malkuthro b4314f3b3d feat(sidebar): preserve workflow groups in saved selections (#220) 2026-06-07 12:41:14 +02:00
malkuthro 98c204b119 feat(setups): convert visual workflows to API prompts (#217)
* feat(setups): convert visual workflows to api prompts

* fix(setups): reject corrupt visual graph links
2026-06-07 11:36:35 +02:00
malkuthro 16023662e6 feat(setups): publish sidebar workflows (#216) 2026-06-07 11:07:52 +02:00
malkuthro 1cf2fd31a2 feat(setups): add published setup catalog API (#214)
* feat(setups): add published setup catalog API

* fix(setups): report registry storage diagnostics
2026-06-06 16:12:21 +02:00
malkuthro df1d66402a fix(deps): relock test aiohttp (#215)
* fix(deps): relock test aiohttp

* docs: clarify review approval default
2026-06-06 15:50:30 +02:00
malkuthro 8c8546c17c Add queue-driven loop status controller (#205)
* Add queue-driven loop status node

* Document ComfyUI server API testing

* Harden loop queue controller

* Guard loop controller HTTP requests
2026-06-03 12:34:15 +02:00
malkuthroandClaude Opus 4.8 38d1f8425d feat(snapshot): Compare Phase 2 — bidirectional path-preserving copy + folder merge + diff filter (#197) (#202)
* feat(snapshot): Compare Phase 2 — bidirectional path-preserving copy + folder merge + diff filter (#197)

Makes the read-only A/B Compare view (live kit A ↔ loaded snapshot B) fully
actionable, in both directions, with orientation signage and a diff browser.

Copy. Right-click a node/workflow on the read-only SOURCE side → "Copy to
<target>". Path-preserving: items land at the SAME folder path they came
from (new copyWorkflowIntoStore auto-creates/merges dirs, deep-clones the
graph, applies a skip-identical / keep-both collision policy; tags + module
ride along). Right-click a folder → "Copy folder (with contents)" bulk-copies
everything under it (the Workflows root copies the whole tree — merging two
snapshots) with an added/kept-both/already-there summary. Archived versions
are excluded from the diff and from bulk copy.

Both directions. B→A writes the live kit via persistMutation (Save round-
trips it). A→B writes back into the snapshot FILE on disk (writePreset),
targeting the exact file/dir it was loaded from (autosave dir threaded
through onChoose). The A→B path is serialized + epoch-guarded: it re-clones
the latest snapshot inside the queued task and only swaps the in-memory
snapshot in after a successful write while the same Compare session is open
(no resurrecting a closed view, no concurrent-copy lost update).

Orientation + diff browser. Each column shows a SOURCE/TARGET footer stripe;
the swap flips which side is the editable target and moves the read-only
guard with it. The bottom green/red legend doubles as filters (new/modified)
that collapse the SOURCE panel to just those items and back. The highlight,
counts, and filter all follow the source side and mean "in the source, not
the target" (the copy candidates).

The two panels keep independent folder-expansion maps so a copy's re-render
never collapses the other side; copies open the destination folder (not the
Archive child) so you can see where they landed.

Non-destructive throughout: no applySnapshot on the Compare path; the read-
only side only writes through the explicit copy affordance; graphs are deep-
cloned so neither store aliases the other.

Tests: copy-engine behaviour (path create/merge, skip/keep-both, deep-clone,
bulk folder, archived exclusion) + structural invariants (read-only guard,
bidirectional additive copy, path-preserving, serialized/epoch-guarded
write-back, source-following diff, panel-isolated expansion, folder pull-in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): guard the Compare copy engine against prototype pollution (#197, #203)

copyWorkflowIntoStore writes destination keys from user-controlled workflow
names / directory segments (a loaded snapshot can be hand-crafted), so reject
__proto__ / constructor / prototype via the same isSafeObjectKey() guard the
other name-keyed store mutators adopted in #203. copyFolderIntoStore inherits
the guard through it. Added after rebasing onto main so the new merge surface
matches the hardened sinks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(snapshot): Compare A→B is an in-memory working copy with an explicit Save (#197)

A→B no longer writes to disk per copy. The loaded snapshot (B) is treated as
an in-memory working copy: copying into it mutates `compareSnapshot` and marks
it dirty — no writePreset, so a rotating autosave (which "Load latest" can
hand you) is never a write target. The user Saves explicitly via a footer
button: writePreset to a NAMED snapshot in the default library dir, pre-filled
with the shadowed/loaded name (compareDefaultSaveName strips the `_autosave`
suffix to reach the named parent). Exiting Compare with unsaved edits prompts
to discard first.

Each column footer now has two lines: line 1 = SOURCE/TARGET + name; line 2 =
save behavior — "Live — changes auto-save" for your kit, and for the snapshot
either "Read-only file", "edits need Save", or "● Not saved" + a Save button
(shown whenever the snapshot is dirty, so it survives a swap). The word "live"
disappears from the UI; the footer carries the meaning.

Removes the per-copy persistSnapshotEdit + compareEpoch + snapshotWriteChain
machinery (no longer needed — there's a single explicit Save). B→A is
unchanged (auto-persists into the live kit). Non-destructive guarantees hold:
no applySnapshot; sources are clones; the snapshot panel stays read-only apart
from the pull-in. Fresh-eyes review: no Critical/Important findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): Compare bottom = two separate stripes; clean autosave name (#197)

Per maintainer feedback, split the bottom into two distinct stripes instead
of cramming save state into the SOURCE/TARGET footers:
  - Stripe 1: the SOURCE/TARGET footer under each column — orientation only.
  - Stripe 2: a dedicated, full-width SAVE stripe with the snapshot's save
    state, instructions, the dirty indicator, and the Save button.

Also fix the long autosave name: an autosave-loaded target showed the raw
`Foo_autosave/pre_load_...` path in the header and footer. compareSnapName()
now derives a clean name (the named parent + " · autosave"); the Save button
reads "Save as file…" and still pre-fills the named parent so A->B merges
land in a named file, never the rotating autosave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(snapshot): guard Compare snapshot Save against silent overwrite (#197)

PR #202 review (data-safety): saveCompareSnapshot wrote `writePreset(name, …)`
with no existence check and no overwrite confirm. Since the Save name is
pre-filled with the snapshot's own name and the server route ends in an atomic
os.replace, accepting the default could silently overwrite a DIFFERENT
same-named snapshot file. Now mirrors the live Save flow (modals.js
promptForName): sanitizeName the input, presetExists() → abort on null
(library unreachable) → confirm on true (Overwrite?) → write only on false.
Adds a `compareSaving` in-flight guard so rapid Save clicks can't fire
overlapping writePreset POSTs.

Also dedup: copyWorkflowIntoStore's collision check now reuses the existing
`normalizedStoresEqual` instead of the byte-identical local `graphsEqual`
helper (removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:26:26 +02:00
malkuthro 7f8c09ad29 feat(sidebar): clean up workflow archives (#201) 2026-06-02 22:25:09 +02:00