* 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>
159 KiB
Changelog
All notable changes to this project should be documented in this file.
The format is inspired by Keep a Changelog and SemVer.
[Unreleased]
Fixed
Easy_LoadVideono longer fails a prompt on video with no audio track. VHS returns a lazy audio map for theAUDIOoutput that shells out toffmpeg -f f32leonly when something reads it. Since ComfyUI corec01175530,comfy/model_patcher.pywalks every linked output hunting for model patchers and recurses into anyMapping, which forces that extraction even when nothing consumes theAUDIOsocket. On a source with no audio stream the ffmpeg call exits-22(Output file does not contain any stream) and took down the whole prompt — which is every Nukemov64write, since those carry video plus timecode and no audio.Easy_LoadVideonow wraps the deferred map on its declaredAUDIOoutput. Silence is substituted only on evidence of absence: ffmpeg opened the input, listed its streams, none was audio, and the muxer then had nothing to write. Every other failure — a missing ffmpeg binary, a permission error, a timeout, a truncated log, any non-ffmpeg exception — proves nothing about whether the source has audio and is re-raised untouched, so real problems stay loud. An unrecognised ffmpeg message re-raises by design, so a future wording change surfaces as a visible error rather than silent corruption. The wrapper stays lazy, so nothing runs ffmpeg that did not before.
[0.5.0] - 2026-08-10
Stability update: the Kforge Labs status no longer shows a false "drifted" warning after normal work, and large LTX Director timelines no longer trigger endless "Failed to save workflow draft" toasts.
Fixed
- "Failed to save workflow draft" toast storms with LTX 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, sotimeline_data— and with it the workflow draft ComfyUI 1.44+ writes to browser storage ~512ms after every edit — grew by ~50–150k characters per video segment. A big timeline then exceeded the storage quota no matter how many old draftsweb/koolook_draft_guard.jsevicted, and the frontend latched its session-wide storage-unavailable flag: every subsequent edit toasted "Failed to save workflow draft" until the page was reloaded. The 2.0.2 vendoring had dropped the 1.3.9-era Koolook media-scrub as obsolete; a minimal scrub is reinstated as a third documented delta: video poster blobs are dropped from persisted timelines (they rebuild from the video element on load), legacy pre-2.0.2 image segments carrying full inline data-URLs are rewritten to the/viewURL form 2.0.2 itself writes, legacy inlineaudioB64is dropped when a backingaudioFileexists (playback only falls back to it when no file resolves), and blobs with no backing file path are left untouched. Restoring a timeline also rewrites the node'stimeline_datawidget from the scrubbed copy, so an already-fat saved workflow can't feed the draft autosave that fires on load before any edit. On-screen previews are unaffected — only persisted copies are scrubbed.
Security
- Supply-chain hardening. Raised the test-dependency floors to patched
aiohttpand Pillow releases, regenerated the audited universal Python 3.11 test lock with a forced upgrade, pinned every GitHub Action to an immutable commit SHA, and enabled one grouped weekly Dependabot update for those pins.
Added
-
Registry metrics summary. The daily Comfy Registry collector now writes
metrics/summary.jsonbeside its CSV history, giving downstream consumers a small stable contract for the v0.3.0 downloads, daily-average, peak-day, and latest-version summary. -
Independent output type for published setups. A
Koolook_PublishRouteris auto-detected as the setup's output selector: whichever writer branches you wire become the app's "Output type" options, and the external user can pick any of them (defaulting to follow-input, or theKoolook_PublishOutput.output_modewidget). No dedicated output-switch wiring is required — the router is bound to the output control regardless of what itsselectoris wired from. So a setup can read one type and write another (EXR sequence in, QT movie out).Koolook_PublishOutputgainsoutput_mode/input_switch/switchfor authors who want explicit control, but they're optional. The app surface exposessetupSurface.app.outputSwitch; output types with no wired writer are hidden (visible: falsefrom the execution map'swriterNodes), and the default falls back to the first wired type so an EXR-in/QT-only setup defaults to QT. The runner prunes/validates writer branches by the chosen output type, and a mode-switchedKoolook_PublishResultreports the branch of whichever switch drives its result index-switch, so a divergent EXR-in/QT-out setup reports the QT movie path it actually wrote. The setup runner simulator renders the "Output type" control and offers only wired types. The runner enforces the contract for direct API callers too: hidden (visible: false) switch options are rejected with a 400 listing only the visible choices, and omittingoutput_switchwhen the default is the "Same as input" sentinel resolves server-side to the input type instead of failing validation. -
Auto-versioning re-runs each queue (
EasyAIPipeline). Added anIS_CHANGEDthat marks the node dirty whileversionisauto/next, so the next-free-vNNNdisk scan runs every queue instead of being memoized on the first run (which froze the version and made saves skip as "already exists"). Literal versions stay cacheable. -
Koolook Matte nodes (
KoolookMatteLoader/KoolookMatteSampler/KoolookMatteFocusCrop/KoolookMatteFocusStitch). First-party, mask-guided one-step video matting — an original GPL-3.0 reimplementation of the published VideoMaMa method (single-step SVD, 12-channel UNet), with temporal windowing, subject "hero-crop" ROI, and shortest-side//64resolution control with OOM auto-backoff. Model caveat: the nodes load (never bundle) the VideoMaMa UNet (CC BY-NC 4.0 — non-commercial) and SVD's VAE (Stability Community License) at runtime; the user is responsible for model-license compliance (seematte/NOTICE). Maintainer notes:docs/maintainers/matte-pipeline.md. -
LTX Keyframe Soften Schedule node (
LTXKeyframeSoftenSchedule). Smooths motion between keyframes without adding any guide frames: a model patcher that sigma-schedules the keyframe denoise-mask pins — loosening them early (high sigma) so the model forms smooth motion between poses, then restoring them so the poses sharpen as denoise finishes. Uses the sampler's per-stepdenoise_mask_functionhook; works with no IC-LoRA. Audio-safe: the A/V mask is aNestedTensor((video, audio)), so only the video keyframe pins are softened and the audio mask passes through untouched (lip-sync unaffected). Two plain controls:max_soften(ease strength — the fraction the pins unpin at the peak; 0 = off, ~0.1 = gentle, 1 = max) andsoften_range(ease the TOP % of the sigma range where coarse motion forms — e.g. 0.03 = top 3% of the range; a noise-level, not a step count). An optionalsigmasinput + aninfoSTRING output report which steps that level actually spans in-graph (the level is curve-consistent, the step count it covers depends on the schedule); the same line is logged each run, and the wiredsigmasdoubles as the runtime fallback anchor when the run doesn't supply usable sigmas. Puresoften_gain+steps_in_rangeunit-tested; model wiring render- and unit-validated. -
LTX Director fork bumped to upstream v2.0.2 with a new
forks/whatdreamscost_koolook/versions/v2_0_2/namespace (pinned to upstream commitfe09f73). This version is a faithful replica of upstream 2.0.2 — the older Koolook customizations (relay_overrides,audio_transcript_json, per-segment sigma) are intentionally dropped as unused, so all latest 2.0.2 features (Prompt Relay, audio, motion) behave exactly as upstream. The canonicalLTXDirector__koolooknode ID rolls to this implementation;LTXDirector__koolook_v1_3_2stays backed by v1.3.9, which remains on disk. The timeline-editor web extension (web/whatdreamscost_koolook/ltx_director.js) is now upstream 2.0.2's editor vendored verbatim and retargeted to the Koolook node, so its UX matches upstream exactly (Add Video / Add IC Video / Retake Mode / IC-LoRA track, correct widget visibility) — replacing the old 1.3.9-era editor. The localStorage quota guard stays in the globalweb/koolook_draft_guard.js. -
Keyframe latent-grid snapping and ease controls (issue #258). The v2.0.2 node adds a
snap_keyframes_to_gridtoggle (default on) that snaps each image keyframe to the center of its LTX latent-time bucket, so hard-pinned keyframes land cleanly on a single latent frame and two pins never collide in one bucket (the later pin is bumped to the next free bucket with a logged warning). It also adds opt-inkeyframe_ease/ease_falloffcontrols that emit strength-ramped neighbor pins of the same pose, smoothing into and out of hard keyframes without weakening the exact center pose. -
LTX A/V Bind Schedule node (
LTXAVBindSchedule). Decouples big motion from lip-sync in LTX 2.3: a model patcher that scales the audio→video cross-attention (audio_to_video_attn) by a per-step gain ramping fromearly_gain(early/high-sigma steps, where coarse motion settles audio-blind) up to full at low sigma (where lips bind). Targets the keyframe-transition jump when a cut lands on an audio peak — one pass, no re-noise.early_gain=1.0reproduces stock behavior. Ramp math is unit-tested; the model wiring is validated by rendering against a live LTX 2.3 AV model. -
LTX Director Ghost Mask reference (REF image / character sheet). New optional
reference_images(IMAGE) +reference_strengthinputs onLTXDirector__koolook(plus informationalclean_latent_frames/clean_pixel_framesoutputs). Reference image(s) are added as guide frames just past the clean timeline, so the model anchors identity (face / mouth shape) the way a WAN VACE reference does — without the references appearing in the output and without a LoRA. Targets mouth/identity drift when keyframes show a closed (or different) mouth than the audio-driven motion needs. The references ride the same append-and-crop path as the timeline keyframes:LTXDirectorGuideappends each as an extra latent frame andLTXDirectorCropGuidesremoves it, so the video latent is never pre-grown and the audio track stays in sync (pre-growing it desyncs A/V and breaks motion). A general-purpose root nodeCleanLatentSlice("Clean Latent Slice (Koolook)") is also included for bare pipelines that don't useLTXDirectorCropGuides. The trailing-reference approach is adapted (idea only, reimplemented) from CGlide's WhatDreamsCost-CSGlide (GPL-3.0); seeforks/THIRD_PARTY.md. Geometry is unit-tested; model behavior is validated by rendering against a live LTX 2.3 model. (Phase 1: Ghost Mask only — the LoRA-backed Licon MSR prefix mode is intentionally not ported.) -
LTX Guide Reference Strength node (
LTXGuideReferenceStrength). Companion to the Ghost Mask reference: rewrites the strength of only the reference frames inguide_data(the trailing entries the Director tags withreference_count), leaving the keyframe pins untouched. Intended for two-stage pipelines — feed stage 1 the Director'sguide_dataunchanged (light reference, motion forms freely) and route stage 2's guide_data through this node at a higher reference strength, so identity locks during the low-denoise refinement without disturbing stage-1 motion. Strength math is unit-tested. -
LTX Reference Bind Schedule node (
LTXReferenceBindSchedule). Ramps the Ghost Mask reference's attention up as sigma falls — neutral early (motion forms freely) rising topeak_strengthlate (identity locks during refinement). The inverse of the motion curve and the mirror ofLTXAVBindSchedule. Mechanism: amodel_function_wrappercaptures the per-step sigma and an object-patch on the core LTX_build_guide_self_attention_mask(which is rebuilt every denoise step and supportsstrength > 1.0to amplify) scales only the trailingnum_referencesguide entries' attention strength — keyframe pins and the noise-mask freezing are untouched, so it's orthogonal toreference_strength. Single pass, no re-noise. Theref_gainramp is unit-tested; model behavior is validated by rendering against a live LTX 2.3 model. Works without an IC-LoRA: the attention-bias machinery is base-model functionality, butLTXDirectorGuideonly populates the per-guide entries when an IC-LoRA is active. So when the entries are missing this node fabricates them itself for the trailing reference tokens (setnum_keyframesso it knows the keyframe/reference split ofnum_guide_tokens), giving the reference the same amplified-attention channel an IC-LoRA would use — inside the Director, no guide-node changes, no graph rewire. When an IC-LoRA is active it scales the real entries instead. The split/fabrication math is unit-tested.
Fixed
- Duplicate-install guard no longer false-positives in git worktrees (#281).
Linked worktrees (
.gitis agitdir:file) outside acustom_nodes/parent skip the sibling scan, so agent/Cursor worktrees no longer treat each other as competing ComfyUI installs and blankNODE_CLASS_MAPPINGS. A worktree deliberately placed undercustom_nodes/still gets the #162 guard. The skip uses the unresolved load path so acustom_nodes/<name>→ worktree symlink cannot defeat the check viaPath.resolve(). - Status pill no longer shows "drifted" on every ComfyUI start (#276). The
#161 boot-time drift guard flagged any difference between the tracked
snapshot file and the live state — which is also what ordinary "kept working
after the last snapshot save" looks like, so nearly every session booted into
a false corruption warning and periodic autosaves were misrouted to
_unsaved_autosave/instead of the preset's own recovery folder. The check is now three-way: a mismatch only counts as drifted when the live state's fingerprint matches the persisted last-named-save baseline (it claims to be saved, yet the file disagrees — which pins the out-of-band change to the file side: a cross-machine library sync, a hand-edit, another install writing it); otherwise it reports the honest unsaved. The first-session baseline seeding now runs after the drift check and only when that check proved the file and live state agree — seeding on an unproven boot baselined whatever the live state happened to be, which both showed "saved" for state that was never saved to the named file and handed the next boot a live state that "claims saved" against a file it never matched, re-opening the same false positive from the other side (reachable whenever a tracked preset name outlives its fingerprint: upgrades from before the baseline key existed, or a persist that lost to localStorage quota). Trade-off: out-of-band changes to the live/userdataside — including corruption — now read "unsaved"; the #162 duplicate-install vector (the known live-side corruptor) keeps its own dedicated guard. - Keyframe guidance now defaults to 0.8 (smooth), not 1.0 (robotic). The
timeline editor auto-generated the
guide_strengthstring from each clip's per-segmentguideStrength, filling 1.0 for any clip left unset — and the Director's Python fallback did the same. 1.0 is a hard pin, so any clip you forgot to lower snapped instead of transitioning smoothly. Unset clips now default to 0.8 in both the editor (display + serialization) and the node; per-clip values you set explicitly still override (e.g. a hard 1.0 pin on a critical pose). Clips with no explicit strength shift from 1.0 → 0.8 on load. dev-syncfrom a git worktree.scripts/sync_to_dev.pynow resolves.envwith the worktree→main-repo fallback (the committed.envlives only in the main checkout), matchingsync_to_dev_audio.pyand the documented behavior. The fallback is now a sharedfind_dotenv()that both scripts use, so they can't drift. Previously plaindev-syncfailed with "KOLOOK_COMFYUI_DEV_PATH not set" from any worktree.
[0.4.4] - 2026-06-23
Added
- Project funding link. Added GitHub funding metadata and a README Ko-fi link for supporting Kforge Labs development.
- GitHub discovery footer. The Kforge Labs sidebar now shows a discreet "Help creators find us on GitHub" line beneath the update-available footer, gated on the same live-panel footer flag so compare columns stay clean.
[0.4.3] - 2026-06-20
Added
- Published setup loop architecture notes. Added a maintainer-facing architecture overview for the publish → external run → ComfyUI execution loop, and linked it from the maintainer docs index.
Changed
- Koolook node compatibility policy. Koolook-created root nodes now default to clean in-place changes unless backward compatibility is explicitly requested, while forked nodes keep full saved-workflow compatibility by default. Agent and maintainer docs were updated to reflect that split.
- Easy Image Batch compatibility note. The released
keyframe_batchdeprecated alias remains available and still routes tokeyframes_insert, so workflows saved on 0.4.1/0.4.2 keep loading while new graphs usekeyframes_insert.
[0.4.2] - 2026-06-19
Fixed
- Comfy Registry node-list parsing. The package entrypoint now behaves as a
package even when registry scanners load
__init__.pydirectly from a file path, soNODE_CLASS_MAPPINGSis no longer left empty in scanner-style imports. This should let the public Registry/Manager details panel discover Koolook's nodes instead of showing "No nodes found." The entrypoint also carries the same lightweight@title/@nickname/@descriptionmetadata used by other custom-node packs.
Changed
- Automated releases. Pushing a
vX.Y.Ztag now drives the whole release viarelease.yml: it publishes to the Comfy Registry and creates the matching GitHub Release from this changelog — only on a successful publish — and refuses any tag that is not onmain. This removes the manual post-merge tag/release steps that previously left v0.4.1 on the Comfy Registry without a GitHub Release. Seedocs/maintainers/releasing.md.
[0.4.1] - 2026-06-18
Added
-
Sidebar update notice. The Kforge Labs sidebar now checks the installed pack version against GitHub's latest release and shows a discreet footer-slot "Update Available" notice only when a newer release exists. The notice links to the release notes and stays hidden when the check fails or the install is already current, so offline ComfyUI sessions remain quiet.
-
Easy Image Batch — review hardening.
keyframe_batchis kept as a deprecated alias ofkeyframes_insert(pre-rename workflows keep loading; an explicitkeyframes_insertwins if both are wired; removal deferred to a release boundary). A non-emptysource_frameswith no image source connected now logs a warning instead of silently returning a clean batch. Frame-list ranges are capped — a singleN-Mtoken can't expand past 8192 frames — guarding against typos like1-99999999. -
Easy Image Batch — passthrough inpaint mask + frame-range syntax. Two refinements: (1) when
source_batchis shorter than the output (empty list), the covered source frames are kept as real content (alpha0.0) while the uncovered tail stays placeholder and becomes theselected_image_batch/selected_framesoutput — an extend-a-clip inpaint mask (keep the first N, generate the rest). A full-coverage passthrough has no gap, soalphais all-kept and the selection is empty. This makesselected_*the gap in passthrough — the inverse of select mode, where it is the placed picks/slots. (2) Thesource_framesfield now accepts inclusive ranges:1-5, 7, 9, 14-17expands to1,2,3,4,5,7,9,14,15,16,17, in both select and insert modes (shared torch-freeparse_frame_tokens, unit-tested in CI). -
Easy Image Batch —
image1-4super-overwrite + source passthrough. The four manual slots are now the top compositing layer in every mode: a connectedimageNoverwrites output frameimageN_frameon top of the sequence/inserts and the background, in select and insert modes (they were previously ignored wheneverkeyframes_insertwas connected). On a collision the higher-numbered slot wins (image4>image1), and a slot beats a sequence pick / insert at the same position. Two model fixes ride along: an unconnected slot now contributes nothing —imageN_frameis only a placement target for a wired image, never asource_batchpick (the old "pullsource_batch[imageN_frame-1]" fallback that surfaced seemingly-random frames is removed); andsource_batch+ an empty frame list (no insert) now passes the source cut window straight through instead of emitting a placeholder batch, matching the empty-list insert passthrough so "source in → source out" is consistent. Slot placement is a pure, torch-free planner (plan_slot_overwrites) unit-tested in CI alongside the existing insert planners. -
Easy Image Batch — insert modes (
keyframes_insert). A new optionalkeyframes_insertinput (renamed from the unreleasedkeyframe_batch) adds the inverse of the node's select behaviour. When connected, the node switches from selecting sparse keyframes to scattering an already-packed short sequence onto thesource_framespositions — placing the i-th incoming frame at the i-th listed number (ascending). The background depends onsource_batch:- not connected → offset / reconstruct mode — gaps filled with the
placeholder colour, at the original sequence length. Closes the
select → process → reconstruct loop: feed a processed copy of a previous
selected_image_batchplus the sameselected_frameslist back in to rebuild the original spacing. - connected → insert-over-source mode — the
source_batchcut window is laid down as the background and the listed positions are overwritten with the insert frames (composite N>1 processed frames back into the source video at chosen indexes). Cut frames beyond the source fall back to the placeholder, reported in the summary.
Connected
image1…image4still composite on top of the inserts in this mode (see the super-overwrite note above);alpha_batchmarks the inserted positions and any slot overwrites. An emptysource_frameslist inserts nothing — a clean placeholder batch (nosource_batch) or a cleansource_batchpassthrough. Placement and the source-base mapping are pure, torch-free planners (plan_offset_placements,plan_source_base_fill) so both are unit-tested in CI. - not connected → offset / reconstruct mode — gaps filled with the
placeholder colour, at the original sequence length. Closes the
select → process → reconstruct loop: feed a processed copy of a previous
-
Easy Image Batch — clean batch instead of erroring +
width/heightfallback. With no image source connected at all, the node now emits a clean placeholder batch (sized by the newwidth/heightwidgets, default 512×512) rather than raising.width/heightare used only as the fallback when no connected image provides dimensions. -
Bundled Kforge Labs demo workflows. Fresh sidebar installs now seed a Kforge Labs Workflows folder with
Easy Image Batch - Select and RebuildandLOOP Demo, and the source workflow JSONs are tracked underdocs/workflows/kforge-labs-workflows/for release review and future demo additions. The loop demo ships with auto-queue disabled until the user sets a writable output folder. -
User help coverage for shipped publish and CameraCtrl helper nodes. The README and user-guide index now list the
Koolook/Publishcontract nodes (Koolook_PublishInput,Koolook_PublishOutput,Koolook_PublishRouter,Koolook_PublishResult) and theKoolookLoadCameraPosesAbsolutecamera helper. New user-guide pages explain the published-setup authoring group layout, router/result roles, and the RealEstate10k-style pose file format. Hover tooltips were filled in for the loop-status and camera-pose widgets, and the publish-contract tooltips now describe their external-app role more directly. -
In-app guide snapshot transfer section. The bundled sidebar guide now ends with a Snapshot transfer walkthrough showing A/B compare, source expansion, copy-folder transfer, and the return to the normal saved-kit view. The workflow right-click screenshot was refreshed with the current compact menu.
-
Publish setup success feedback (#227). Publishing a saved sidebar workflow no longer closes the dialog silently — a confirmation card now shows where the setup was saved (the registry
setups.jsonpath) and offers Open folder, Copy path, and Close, reusing the snapshot Save dialog's "Saved to … Open folder ↗" language. The publish API response gained astoragePathfield, and a newPOST /koolook/api/setups/revealroute opens the published-setups directory in the host file manager (grounded at the registry folder, separate from the snapshot-library reveal). A draft publish without a storage path still gets an explicit confirmation. Part 2 of #227 — a separate published-setups destination/library setting — remains open. -
Discover published setups in the sidebar (#227). Publishing a saved workflow now tags it
published, so it carries a small "published" badge in the Workflows tree and appears in the Tags section'spublishedpool. A new "P" toggle in the sidebar Tools row prunes the Workflows tree to just the published setups — folders force-expanded — so every published setup is visible in its original folder structure at a glance; toggling off restores the full tree. Reuses the existing tag system and Workflows tree rather than adding a separate published-setups view. -
Published setups stored beside the snapshot library (#227). The published-setup registry (
koolook-published-setups/setups.json) now lives as a sibling of the configured snapshot library (following thelibraryPathsetting /KFORGELABS_PRESETSenv) instead of a fixed user-dir folder, so setups and snapshots stay together and the success card's "Open folder" lands predictably. Any existing registry at the old user-dir path is copied to the new location on first use, non-destructively (the original is left in place). The migration copy is atomic (temp file + replace), and if the relocated file exists but is unreadable while the legacy file is intact, the registry keeps serving the legacy file instead of masking it behind an empty catalog. -
Published setup registry + catalog API. Added the first server-side Published Workflow Setup registry boundary (
listSetups()/getSetup(id)), schema validation with invalid-record diagnostics, file-backed storage with file-level diagnostics at<ComfyUI user directory>/koolook-published-setups/setups.json, a bundled sample fixture, and read-only external catalog endpoints:GET /koolook/api/setupsfor summaries andGET /koolook/api/setups/{id}for detail contracts. -
Published setup execution API. Added the first external run path for callable published setups:
POST /koolook/api/setups/{id}/runclones the stored API prompt, accepts only declared setup/app inputs, injects approved values, submits the prompt to ComfyUI/prompt, and returns a stable Koolook run id plus ComfyUI prompt id.GET /koolook/api/runs/{runId}translates ComfyUI history/queue data into queued/running/succeeded/failed state and exposes published output-contract items for external frontends. -
Sidebar publish flow for callable setups. Saved sidebar workflows now expose an explicit Publish setup... context-menu action that captures setup metadata, source workflow reference, input/output contract JSON, and an optional preview/card reference. Publishing validates the selected workflow and structurally plausible input targets, persists through the published setup registry, and surfaces through the existing catalog API.
-
Comfy API prompt preservation for published setups. Publish requests can now supply ComfyUI's exported API workflow as
apiPrompt; the registry stores it as the executable source of truth instead of re-converting the visual graph. The fallback converter now omits visual-only nodes, resolves reroutes, expands subgraphs, and repairs older prompts that accidentally stored editor-only artifacts. -
External setup runner simulator. Added
web/setup_runner_simulator.html, a small external-app harness for maintainers to load published setups, render thesetupSurface.appmode switch/source path/output controls, queue the storedapiPromptthrough the public Koolook run API, watch queued/running/final status, see the ComfyUI prompt id, and inspect the returned result path or error payloads without adding debug-only controls to the sidebar. The simulator can also load exported/offline canonical setup JSON, either as one setup record or a{ "setups": [...] }registry wrapper, through the same app-surface renderer. -
Koolook Loop Status queue controller. Added
Koolook_LoopStatus, a pass-through node that reports per-frame progress (1/4,2/4, ...) and can queue the next prompt by advancing a connectedeasy intframe index. The included loop demo workflow uses it to run one full ComfyUI execution per frame so deep subgraphs receive single-frame inputs instead of a sequence batch. The controller also caps auto-queue depth, checks ComfyUI/promptresponses, guards duplicate queue submissions, and writes an abort marker beside the target sequence when a child prompt cannot be queued. -
Workflow Archive cleanup. The synthetic Archive folder now offers
Clean up archivealongside the existing all-or-nothing delete action. Cleanup confirms keep/delete counts, groups archived entries by original workflow name, keeps representative recent backups from the last five minutes / hour / day, and leaves active workflows untouched. New archive entries storearchivedAt, while legacy timestamped names andsavedAtremain compatible. -
Snapshot Compare — actionable A/B with bidirectional copy (Phase 2, #197). The side-by-side Compare view (live kit A ↔ loaded snapshot B) is now actionable in both directions:
- Path-preserving copy. Right-click a node or workflow on the read-only
source side → "Copy to ". Workflows land at the same folder
path they came from — missing folders are auto-created, existing ones
merged into (no folder-picker step). Same-name collisions skip when
identical and keep-both (
name (from <source>)) when they differ. Tags and module flag ride along; graphs are deep-cloned so neither side aliases the other. - Both directions. B→A copies into your live kit (auto-persists, like any sidebar edit). A→B edits the snapshot as an in-memory working copy — a Save button in its footer writes it to a named snapshot file (never a rotating autosave), pre-filled with the snapshot's name; exiting with unsaved edits prompts first. Two bottom stripes keep it clear: a SOURCE/TARGET footer under each column, and a separate save stripe showing the snapshot's saved / unsaved state, instructions, and the Save button. Autosave-loaded targets show a clean name (the named parent + "autosave"), not the long autosave path.
- Copy a whole folder across. Right-click a workflow folder → "Copy folder … (with contents)" bulk-copies every workflow under it (recursively, path-preserving, same collision policy) — the merge-two-snapshots case. The Workflows root copies the entire tree; a summary toast reports added/kept-both/already-there. Archived versions are skipped.
- Swap (A↔B) toggle flips which side is the editable TARGET; the read-only capture guard follows to the SOURCE side. Each column shows a SOURCE / TARGET footer stripe so the direction is unambiguous.
- Diff legend + filter. A green/red legend (green = only in the snapshot; red = in both, graph differs) doubles as clickable filters: click new or modified to collapse the snapshot panel down to just those items (folders auto-expanded), click again to restore the full tree.
- Non-destructive throughout: no
applySnapshoton the Compare path; the read-only side only ever writes through the one explicit copy affordance.
- Path-preserving copy. Right-click a node or workflow on the read-only
source side → "Copy to ". Workflows land at the same folder
path they came from — missing folders are auto-created, existing ones
merged into (no folder-picker step). Same-name collisions skip when
identical and keep-both (
-
Easy_VideoCombine: loader-friendly video path outputs. The node now keeps its originalFilenamesoutput and appends clean string outputs for the final video's full path, directory, filename, and JSON sidecar path, so a rendered clip can wire directly intoEasy Load Video (Koolook)for a second workflow stage without parsing VHS's mixed JSON/video list. -
Node user-guide coverage and tooltip pass. Added user-guide pages for
EasyAIPipeline,EasyResize_Koolook,EasyWan22Prompt,Easy_Utility,Easy_Pattern, andLTXDirector__koolook; refreshed the guide inventory and README node list; and added hover tooltips to the public Koolook nodes that still had sparse widget metadata.
Removed
- Standalone audio-transcript tooling. Removed the experimental
KoolookAudioTranscriptTimeline/KoolookTimelineEditornodes, thescripts/transcribe_audio_timeline.pyCLI, and the[audio](faster-whisper) optional dependency — superseded by the Koolook Director's built-inaudio_transcript_jsoninput (#198). Anyone usingpip install -e '.[audio]'or that CLI out-of-tree should pin a pre-removal commit.
Fixed
- Dependency CVE audit is now enforced in CI (#245). The committed
constraints-test.txtlock is audited withpip-auditon pull requests and weekly scheduled runs, pytest installs from that same lock, and the bootstrap scripts upgradesetuptoolsalongsidepipbefore audit. The test lock was regenerated to moveaiohttpfrom 3.14.0 to the fixed 3.14.1 line, and the relock path now avoids editable Git metadata inspection so Windows cross-drive worktrees can regenerate the lock cleanly. The[test]extra'saiohttpfloor was also raised from>=3.9to>=3.14.1so the declared dependency range can no longer resolve a vulnerable version, clearing the eight range-based Dependabot alerts the lock bump alone left open. Koolook_LoopStatusauto-queue failed on non-default ports. The node'sserver_urldefaulted to ComfyUI's standard port (http://127.0.0.1:8188), so installs launched with--port(or--listen) aborted the loop withComfyUI server is not reachablewhile probing the dead default. The widget now defaults toauto; whenserver_urlisautoor blank (or the legacy:8188default) the node auto-detects the address the running server actually bound to (viacomfy.cli_args/ the runningPromptServer), so loops queue correctly against any port. A customhttp://host:portis still honored verbatim.Koolook_LoopStatusaborted into a marker file whenindex_node_idwas stale. A shifted widget value (e.g.index_node_id/numeric label resolving to0) is not a real node, so the background queue thread failed withindex node id '0' is not in promptand only left an abort-marker file. The node now falls back to the node feeding the connectedindexinput when the supplied id isn't in the prompt (self-healing the common widget-shift case), and when it still can't resolve a real node it raises synchronously with an actionable message so the error surfaces in the ComfyUI UI immediately.Koolook_LoopStatusignored savedauto_queue_nextstrings and was quiet about which frame-index node it drove. A workflow that persistedauto_queue_nextas the string"false"still auto-queued (a non-empty string is truthy), and the node never logged whicheasy intnode advanced the loop.auto_queue_nextnow coerces saved string booleans ("true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"),index_node_idis treated as an advanced override (leave blank for normal use), and the node logs the detected frame-index node class/id each time it queues the next frame.Koolook_LoopStatusnow follows the connectedindexwire deterministically. A numeric node id left in thelabelfield — or any staleindex_node_id— no longer overrides the actual wiring: the node feeding the connectedindexinput is authoritative, and a recovered numeric label is used only as a last-resort fallback when nothing is connected. Loops no longer need thelabelcleared by hand to pick the right frame-index node.- "Failed to save workflow draft" toasts returned on ComfyUI frontend
1.44+. The browser draft-quota guard is now its own global extension,
web/koolook_draft_guard.js, and matches Comfy draft keys by prefix instead of exact names — covering the legacyComfy.Workflow.Draftsblob, the interim per-workspace…:<ws>keys, the 1.44+DraftIndex.v2:/Draft.v2:index+payload scheme, and unknown future keys as last-resort evictions. On a quota error it evicts the oldest draft across all generations insidelocalStorage.setItemand retries, rescuing the save before the 1.44 frontend's session-wide storage-unavailable latch can trip (that latch otherwise re-toasts on every edit until reload). The boot prune now also deletes the suffixed V1 families 1.44-era V1→V2 migrations leave behind, scopes corrupt-key cleanup to the offending key instead of the whole store, drops oversized entries, and enforces a ~2M-char total draft budget across all generations (the old guard capped only the unsuffixed V1 blob, at 1.5M).dev-sync-audioships the guard file so a scoped sync can't strand a dev install guard-less. Behavior is covered bytests/js/test_draft_guard.mjs. - Published setup router execution maps. New
Koolook_PublishRouternodes let setup authors wire the main payload through switch-aligned writer outputs. Publishing stores an explicitexecutionMap, and setup runs prefer that map to execute only the selected writer branch;Koolook_PublishResultis now optional reporting rather than the routing mechanism. - Published setup writer results report selected file paths. Router-based
setup runs now synthesize the selected writer
filepathfrom the executed prompt when saver nodes, such asSaveImageAndPromptExact, return empty UI history items after writing the file. The status endpoint resolves those paths without creating output directories or checking overwrite state. - Published setup switch validation. External setup runs now reject invalid switch values such as booleans or branch numbers not declared by the published setup, preventing malformed requests from falling back to unpruned prompt execution.
- Published setup execution-map validation. Router-authored setup runs now reject selected branches whose stored writer nodes no longer exist in the prompt, preventing stale maps from falling back to legacy/full-prompt execution.
- Published setup runs resolve switch-selected result branches. App-style
setups where
Koolook_PublishInput.switchdrives both source and output/result switches now prune queued prompts to the selected result path, keep selected downstream saver nodes that materialize the result, and return the selected branch artifact when ComfyUI history omits theKoolook_PublishResultnode item. - Workflow validator wildcard sockets.
scripts/validate_workflow.pynow treats ComfyUI wildcard (*) inputs as compatible with concrete link types, matching real workflow links such as anIMAGEoutput returning into an Easy-Use loop body's wildcardinitial_valuesocket. - Node registry survives one broken or missing module. The root
__init__.pynow imports each node group independently and installs the Kforge Labs snapshot/preset routes (/koolook/presets/*) regardless of node-import results. Previously a single missing or failing node module raised at import time and zeroed every Koolook node and the snapshot routes at once (sidebar disk browse/save returning 404). A new guard test keeps every rootk_*.pylisted in dev-sync'sRUNTIME_PATHS. loop-audio: Director card reads execution-active values. The audio-lipsync card now reads Director widgets from the saved Director widget order before falling back to generic input-name lookup, fixing falsemodel-gen/ unknown-fps cards when saved workflows include non-input widgets such asepsilon.relay_overridesis now reported only when the active Koolook Director'srelay_overridessocket is actually wired; an unwiredRELAY_OVERRIDESnote no longer appears as an active knob on the card, log, metadata, or notes.Easy_LoadVideo: directvideo_pathhandoff. The loader now accepts a complete video file path ininput_pathwhenvideois empty, soEasy_VideoCombine.video_pathcan wire straight intoEasy_LoadVideo.input_pathwithout needing a separate filename field. Existing local video files bypass VHS's stricter path-wrapper validation and are passed directly to VHS's shared decode path. Wrapped multiline full paths from text nodes are joined back together before loading.Easy_LoadVideo: two-line path/name input. Wheninput_pathis empty, the loader now accepts a multilinevideostring whose first line is the directory and second line is the filename, matching the path/name text-block pattern used in automation workflows.dev-sync-audio: stale Director web extension cleanup. The scoped audio sync now removes the oldweb/whatdreamscost_koolook_v1_3_2/folder after the stable v1.3.9 web-extension rename, preventing legacy workflows from loading two identical timeline editors in dev installs.- LTX Director timeline images persist after canvas/setup switches. The
timeline editor keeps saved workflow payloads light by omitting preview-only
image blobs, but now rebuilds image previews from the persisted
imageFilepath when the node is restored. It also strips legacy inline preview blobs from restored timeline data when a saved file path exists, preventing old image-heavy setups from re-triggering Comfy's draft-quota error. - Comfy workflow draft quota guard for large LTX timeline workflows. The
LTX Director frontend now strips preview-only media blobs from timeline
serialization and catches browser quota failures on Comfy's own
Comfy.Workflow.Drafts/Comfy.Workflow.DraftOrderwrites, evicting only the oldest saved Comfy draft before retrying and showing a visible warning if the browser store is still full. This documents and extends the v0.3.6 stable-draft-ID lesson to large workflows imported directly from disk, which bypass the Koolook sidebar load path. - LTX Director transcript timing preserves image timelines. The
audio_transcript_jsonhook now rejects empty phrase lists and keeps existing timeline image segments intact, using the transcript only to update Prompt Relaylocal_prompts/segment_lengths. When no timeline image segments exist, it still builds speech/pause segments for older stripped-down flows. - Audio-lipsync run evidence no longer carries local machine paths. Checked-in run workflow snapshots now replace user-home, working-folder, and project-drive paths with placeholders so the reproducibility evidence stays useful without publishing workstation-specific paths.
loop-audio: run numbering now respects the log. The audio-lipsync loop now chooses the next run number from both existingrun-NNN_*folders andruns/log.md, preventing a newly saved snapshot from reusing an already logged number when a folder is absent or a run was log-only.loop-audio: fork-touching captures are always retained. Everyloop-audioinvocation now writes the log row and matchingrun-NNN_<label>/evidence folder; scratch renders should be skipped explicitly with "no log" / "don't log this" before capture.- Snapshot drift on duplicate installs no longer silently corrupts the
workflow store. Closes
#161 and
#162 as a paired
fix:
- Duplicate-install guard (#162). When the Comfy Registry/Manager
install (
custom_nodes/koolook/) and agit clonecheckout (custom_nodes/ComfyUI-Koolook/) co-exist, every ComfyUI boot would race two parallel plugins for the same/koolook/presets/*routes, the same Kforge Labs sidebar tab, and the same/userdata/koolook_workflows.jsonfile — silently reverting workflows to a stale state on every restart. Newkoolook_install_guard.pyscanscustom_nodes/siblings forkoolook_routes.pymarkers at import time, prints a critical log naming both paths + their pyproject versions, and registers nothing (no nodes, no routes, noWEB_DIRECTORY) when this install is the alphabetical loser. The scan is fail-safe by construction — an unreadable sibling directory or a non-UTF-8pyproject.tomlis skipped rather than allowed to abort the plugin import, so the guard can never itself take Koolook offline. A client-side fallback inweb/sidebar/extension_guard.jscatches the residual case where an older sibling without the backend guard still ships itskoolook_sidebar.jsto the browser, sets awindowsentinel on first load, and skips the secondapp.registerExtensioncall with a critical toast..env.example, the dev-iteration loop doc, andscripts/sync_to_dev.pynow point atcustom_nodes/koolook/(the Registry-derived path) so dev-sync overwrites the Manager install in place instead of spawning a parallelComfyUI-Koolook/folder. - Boot-time tracked-snapshot drift guard (#161). New
detectBootDrift()inweb/sidebar/snapshot.jsreads the tracked named snapshot file at session start, fingerprints it the same way the live state is fingerprinted, and flags drift on mismatch. While drifted,_autosaveSubdir()routes periodic + pre-load autosaves to_unsaved_autosave/instead of<preset>_autosave/— so a corrupt live state can never masquerade as a "newer recovery" for the named snapshot.getSnapshotStatus()gains a"drifted"precedence (above"saved") so the sidebar pill flips to a warning state (· drifted (reload?)) with a recovery- instructions tooltip. The flag clears automatically on the nextmarkStateSaved()(Save / Quick Save / Load), so a deliberate realignment retires the warning without the user having to dismiss it.
- Duplicate-install guard (#162). When the Comfy Registry/Manager
install (
EasyAIPipeline: preview resolves connected text-name builders. The path preview buttons now evaluate common connected text nodes such asText MultilineandText Concatenateinstead of reading the concatenate node's delimiter widget (_) as the whole shot name. This fixes previews like.../_/v003/__v003.%04d.exrwhenshot_nameis wired, and the preview version formatter now matches Python:003becomesv003, while an existingv003staysv003.EasyAIPipeline: path preview follows subgraph-routed version inputs. The "Get output directory/file path" buttons no longer reuse a stale converted-widgetversionvalue when the live value is wired through KJ Set/Get nodes and a ComfyUI subgraph. The browser preview resolver now crosses subgraph outputs, maps subgraph inputs back to their host node widgets/links, evaluatesEasy_Utility'sint_to_padded_stringoutput, and reports "Cannot preview" instead of silently falling back to stale local widget state when a connected value is too complex.EasyAIPipeline/Easy_VideoCombine: Run no-op whenversionwidget is wired or carries a stale value. PR #180 introduced a new STRINGversionwidget in the same widget-slot the old INT lived in. Workflows saved before #180 replay an INT into the new slot; widgets converted to inputs leave the widget object withvalue: undefined; dict-serialized Easy_VideoCombine workflows simply omit the new key. Any of those states combined with ComfyUI-Custom-Scripts (pysssss) installed crashedgraphToPrompt— pysssss'spresetText.jspatches every STRING widget'sserializeValueto runvalue.replace(...)for{variable}substitution, and.replaceonundefinedaborts the whole queue ("Cannot read properties of undefined (reading 'replace')"; Run does nothing). Fixed by trappingwidget.valuereads/writes on theversionwidget via a property descriptor so the value is always coerced to a string (web/ai_pipeline.js,web/easy_video_combine.js). Survives widget-to-input conversion, stale workflows, and downstream widget patches.
Changed
- Locked + audited test environment; bootstrap pre-authorized.
scripts/bootstrap_test_env.{sh,ps1}now install the[test]extras against a committed lock (constraints-test.txt— a pinned resolve of the extras + their full transitive closure) and gate the result withpip-audit; a known CVE fails the bootstrap. New flags:--relock/-Relock(re-resolve + rewrite the lock) and--no-audit/-NoAudit(skip the audit, e.g. offline). Because the bootstrap can now only install an already-reviewed, pinned, audited set, it is pre-authorized to run automatically —CLAUDE.mddocuments that this supersedes the global/warmupMISSING_VENVhard-stop, while dependency changes still land as a reviewedpyproject.toml/constraints-test.txtdiff. The lock + audit govern local and agent bootstraps; CI still installs the minimalpytest+aiohttpsubset it needs directly (not yet on the lock/audit path). New guard testtests/scripts/test_bootstrap_constraints.pykeeps the lock in sync with the declared extras. - Dev-sync scripts now only copy files.
sync_to_dev.pyand the scopeddev-sync-audiowrapper no longer call the ComfyUI-Manager reboot endpoint and no longer expose--no-restart/--restart-url; restart ComfyUI manually after Python changes need to be re-imported. loop-audiocard redesigned around a strict two-source rule (issue #177). The audio-lipsync card now only reads (a) the five trackedText Multilinenodes and (b) theLTXDirector__koolooknode's own widget values + input wiring. The previous SAMPLER block (BasicScheduler/KSamplerSelect/RandomNoise/CFGGuiderscrapes), FORK STATE block (_dev_build.json+git status), and INERT warning are gone — they were either widget scrapes from outside the two source families or duplicates of the Koolook Director's own state. New sections:BASE · LOCKED(epsilon · Audio src · Working folder, path-wrapped on separator boundaries) andBASE · SCENE(flat-left labels with one indented child —Segments (N)parent, spelled-out1) 0 to 5 secondsper segment, aggregatedPrompt / Audio / Keyframecoverage rows). Audio src is derived structurally from the five-state machine (director presence × audio_vae link × use_custom_audio × audioSegments count) — never from substring matches on prompt text.notes.mdandruns/log.mdfollow the same source rule; the log's Phase 1 / Phase 2 / Custom audio columns are replaced byAudio src+Segments.loop_audio.py's.envdiscovery now falls back to the main repo when running from a worktree (same asscripts/make_card.py). Fixed the upstream-LTXDirector widget index bug — the saved workflow preserves the legacy widget order even after the Koolook fork reordered the schema, soDIRECTOR_WIDXmatches the serialised order, not the schema declaration.- Automations restructured around modules instead of models.
docs/automations/now pivots on one folder per task (each its own workflow + iteration loop + findings) rather than one folder per model. The existingLTX-2.3/{README,findings,handoff-checklist}.mdcontent moved intoLTX-2.3/base-1step/(single-stage render task) and the new siblingLTX-2.3/audio-lipsync/(audio-file lip-sync task) was created.docs/automations/{README,CHEATSHEET}.mdupdated to reflect the new layout;docs/automations/CONVENTIONS.mdunchanged. docs/investigations/removed. Its two children migrated to their natural homes: narrative content into the new automation modules underdocs/automations/LTX-2.3/{base-1step,audio-lipsync}/; the modified upstream Python code into the newforks/whatdreamscost_koolook/fork (see Added). The retiredscripts/sync_investigation_patches.pyand itsKOLOOK_WHATDREAMSCOST_PATHenv-var section in.env.examplego with it — iteration now uses the standarddev-syncflow against the in-repo fork.- Glossary updated. The Investigation folder, Investigation patches,
JSON file folder, and
sync_investigation_patches.pyentries indocs/reference/glossary.mdare replaced by Automation module, Backstory, Working folder, and Card; the Loop and Run entries are rewritten to point at the new structure.
Added
- Koolook Director accepts an audio transcript for speech timing. The
Koolook LTX Director gained an
audio_transcript_jsoninput that converts phrase-timed transcript JSON into Directortimeline_data,local_prompts, andsegment_lengthsinternally before Prompt Relay conditioning runs. forks/whatdreamscost_koolook/v1_3_9/— updated Koolook LTX Director fork. Upgrades the Koolook Director to upstream WhatDreamsCost-ComfyUI3b65410(pyproject/README version1.3.9; nov1.3.9git tag at fork time). It carries forward the Koolookrelay_overridesmultiline JSON input and per-segment Prompt Relay sigma patch, while keeping upstream's v1.3.9 audio latent fixes. New workflows use stable node IDLTXDirector__koolook; existing workflows saved withLTXDirector__koolook_v1_3_2still load through a compatibility alias backed by v1.3.9. The original v1.3.2 source remains on disk for attribution, review, and rollback.- Easy Load Video (
Easy_LoadVideo). Path-aware sibling of VHS'sLoad Video Pathnode. It exposes a splitinput_path+videofield layout so a workflow can keep the source folder fixed while changing only the clip name. Absoluteinput_pathvalues load directly from disk; relative values resolve under ComfyUI'sinput/directory. Leavinginput_pathempty passesvideothrough to VHS unchanged, preserving full-path/URL workflows. Implemented as a thin subclass ink_video_load.py, with pure helper coverage intests/nodes/test_easy_video_load.pyand usage docs indocs/user_guide/nodes/koolook_video/easy_load_video.md. - Audio-lipsync reading-graph docs. Three new files alongside
docs/automations/LTX-2.3/audio-lipsync/:reading-graph.htmlis a visual flow diagram (dark theme, color-coded source bands) showing every value's path from canvas node to card section;reading-graph.schema.yamlis the machine-readable version of the same graph (source families, widget indices, audio state machine, card-section field map, invariants);CHEATSHEET.mdrefreshed against the new card design (chat triggers, required canvas shape, card source rule, the five Audio src states, snapshot folder layout, settings map). forks/whatdreamscost_koolook/v1_3_2/— Koolook fork of WhatDreamsCost-ComfyUI'sLTXDirector(upstreame81223a, GPL-3.0). Two upstream files modified (ltx_director.pyadds arelay_overridesmultiline-JSON widget;prompt_relay.pyuses the Prompt-Relay paper's per-segment σ formula instead of upstream's length-independent constant) plus the unmodifiedpatches.pyvendored verbatim because the modified file imports from it. Registered asLTXDirector__koolook_v1_3_2(display name "LTX Director (Koolook v1.3.2)") so it coexists with an installed copy of upstream WhatDreamsCost-ComfyUI in the node picker. Drives thedocs/automations/LTX-2.3/audio-lipsync/iteration loop. License attribution + change log inforks/THIRD_PARTY.md; pin metadata inforks/whatdreamscost_koolook/versions/v1_3_2/UPSTREAM_PIN.yamlandforks/forks_manifest.yaml.
[0.3.7] - 2026-05-23
- Workflow validator (
scripts/validate_workflow.py). stdlib-only CLI that checks a ComfyUI workflow JSON for internal consistency: declared- vs-referenced links, slot index bounds and type matches, endpoint cross- references,last_node_id/last_link_idbounds, and duplicate IDs. Catches the class of bugs that come from hand-editing workflow JSONs (e.g. agent-driven node re-wiring when building a slim variant from a Director workflow). Defensive against malformed-but-valid JSON — every shape assumption is guarded so bad input produces problem lines, never an uncaught exception. Exit codes:0clean,1problems on stderr,2file missing or bad JSON. Covered bytests/scripts/test_validate_workflow.py(24 tests). Seedocs/automations/CONVENTIONS.md§11 for usage and triggers.
Docs
- User support and bug-reporting path. The GitHub landing page now has troubleshooting and support sections, the in-app Help guide links to a support checklist, and GitHub issues include a bug-report template that asks for ComfyUI/Koolook versions, install method, logs, browser console errors, screenshots, and reproduction steps.
- LTX 2.3 findings — spatial upscaler architecture & stage-2-only path.
Two new sections in
docs/automations/LTX-2.3/findings.md: a locked-in Spatial upscaler architecture block (×2 hardcoded factor, brief enters only viaDirector → guide_data → DirectorGuide, attention scales(W × H × T)²) and an open Stage-2-only refine path block with the recipe, initial knob settings, and three hypotheses still pending validation across runs.
Fixed
- Workflow archive stays collapsed during search. The sidebar search state now preserves the archive folder's collapsed/expanded intent instead of forcing archived workflows open while filtering. This keeps normal workflow search focused on active entries unless the archive was explicitly opened.
- Snapshot Load list now reflects disk renames. The snapshot library
still validates each JSON file and reads its counts/metadata, but the
Load dialog displays and sorts root snapshot rows by the current
filename stem instead of the embedded
namefield. RenamingShot_A.jsontoShot_B.jsonin Finder/Explorer now showsShot_Bin the sidebar without editing the JSON contents.
[0.3.6] - 2026-05-18
Fixed
- Kforge Labs workflow loads no longer churn Comfy draft ids. Loading the
same saved workflow from the sidebar now uses a stable temporary workflow id
derived from its sidebar path/name, so Comfy's browser-side
Comfy.Workflow.Draftscache replaces the same draft entry instead of accumulating a new large draft on every load. This reduces the chance of hitting repeatedFailed to save workflow drafttoasts from browser storage quota exhaustion after restoring or loading archived workflows.
[0.3.5] - 2026-05-18
Added
-
Easy Video Combine (
Easy_VideoCombine). A path-aware sibling of Kosinkadink/ComfyUI-VideoHelperSuite'sVideo Combinenode. When thefilename_prefixfield carries an absolute path (e.g.E:/renders/shot01/v003), the node bypasses ComfyUI's output-directory sandbox and writes the video, metadata PNG, and any audio mux directly to that location. Relative prefixes behave identically to upstream (sandboxed under ComfyUI'soutput/). Adds an optionalcreate_path_if_missingBOOLEAN (defaultFalse) — when on, the parent directory is auto-created; when off, a missing parent surfaces as a clear error so typos don't silently spawn directories.Implemented as a thin subclass of VHS's
VideoCombine(~60 lines of Python, no upstream code copied — seek_video_combine.py) that scoped-patchesfolder_paths.get_save_image_pathfor the duration of onecombine_video()call. All encoding, format widgets, audio mux, batch-manager, and progress-bar behavior comes from VHS unchanged. If VHS isn't installed the node simply doesn't register, with a one-line stderr notice at module load.The
os.path.isabs(filename_prefix)discrimination pattern is borrowed from spacepxl/ComfyUI-HQ-Image-Save'sSaveEXR(MIT) — overload the existing field for both modes rather than adding a new input pin. Seeforks/THIRD_PARTY.mdfor attribution anddocs/user_guide/nodes/koolook_video/easy_video_combine.mdfor the user-facing guide.Also adds an optional
output_directorySTRING input (default empty). When set,filename_prefixis treated as just the filename root andoutput_directorycarries the path — so you can change the name without retyping the directory across many renders. Absoluteoutput_directorywrites there directly; relativeoutput_directoryjoins under ComfyUI'soutput/. The overloaded-absolute-filename_prefixmode is preserved unchanged for users who prefer a single field. -
Easy AI Pipeline (
EasyAIPipeline):no_subfolderstoggle. When on, the node writes directly intobase_directory_pathinstead of appendingshot_name/ai_method[/vNNN]subfolders underneath. The base folder is still created on the fly if missing, so you can point it at a not-yet- existing directory. Default is off, so saved workflows render the same path as before. -
Easy AI Pipeline: tooltips on every input. Hovering any field in the node now explains what it controls and how it interacts with the other fields/toggles (e.g. that
ai_methodbecomes both a filename segment and a subfolder, thatdisable_versioningdrops thevNNNsegment everywhere, thatenable_overwriteonly blocks an existing file — not an existing directory). -
AI-assisted automation workflow framework. Adds
docs/automations/as the canonical home for repeatable image/video iteration loops, with shared conventions, a quick-reference cheatsheet, an LTX 2.3 model-specific workflow, handoff checklist, promoted findings, and the companiondocs/investigations/ltx-director-4k-transitions.mdinvestigation. The newscripts/make_card.pyandscripts/watch_cards.pyhelpers generate and monitor compact review cards from ComfyUI run metadata so experiment results can be compared across runs without re-reading full workflow JSON. -
Project-local test bootstrap helpers. Adds macOS/Linux and Windows scripts for creating the local test environment, plus
pip-auditin the optionaltestdependency set and a repository line-ending policy. The POSIX helper acceptsPYTHON=...and falls back topython3whenpythonis not available onPATH.
Changed
- Workflow right-click menu shortened for large libraries. The menu no longer lists every existing folder as a move destination, which could push useful actions off-screen when many projects/modules existed. Moving a workflow into an existing folder is now handled by drag-and-drop; the right-click menu still supports creating a new directory/subdirectory and moving the workflow there in one step.
- Easy AI Pipeline: no more dangling underscores in filenames. The
filename builder now joins only the non-empty pieces of
shot_name/ai_method/vNNNwith_, so leavingai_methodblank yieldsRTX-upscale.%04d.exrinstead of the previousRTX-upscale_.%04d.exr. Same rule applies to the subfolder build, so an emptyai_methodno longer produces a phantom directory level. The preview shown by theGet output file pathbutton now matches.
Fixed
- Kforge Labs snapshot/workflow restore no longer hydrates from stale browser cache.
The sidebar's startup read of
/userdata/koolook_workflows.jsonnow uses a cache-bustedno-storerequest, and snapshot-library reads do the same for mutable preset/list/settings endpoints. This fixes the severe case where the browser returned an older 5-workflow/userdataresponse even though the on-disk file and a cache-busted fetch contained the correct 59-workflow snapshot state; the status pill could appear to track the right snapshot while the workflow tree rendered stale data and periodic autosave captured that bad state. - Easy AI Pipeline: trailing-slash on base path no longer creates a
phantom
undefined/folder. Typingn:\foo\bar\(trailing backslash) intobase_directory_pathused to leak a trailing/onto theoutput_directoryoutput. Downstream save nodes split on/, stringified the resulting empty tail as"undefined", and wrote into…/bar/undefined/<shot_name>/…on disk. The node now strips any trailing separator fromoutput_directory(with a guard so drive roots liken:/survive intact), son:\foo\bar\andn:\foo\barresolve identically. TheGet output directory pathpreview already showed the clean form — Python now matches. The same cleanup also treats literal frontend sentinels like"undefined"/"null"/"None"as empty text in path fields and strips those sentinel words when they arrive as whole path components from connected upstream values, matchingEasy_VideoCombine's defensive normalization. - Easy AI Pipeline: broader paste-input hardening on
base_directory_path. A new_normalize_base_pathhelper strips surrounding whitespace, one matched pair of surrounding quotes ("..."or'...'—Shift+Right-click → Copy as pathin Windows Explorer wraps paths in double quotes), and any number of trailing separators (mixed/and\accepted). Drive roots likeC:\andn:/are preserved. A new test suite attests/nodes/test_easy_ai_pipeline.pypins both the helper and the end-to-endgenerate_pipelinebehavior with parametrised paste-variant cases (49 tests covering 11 realistic paste shapes the maintainer has actually seen), so theundefined/phantom-folder bug can't regress. - Easy AI Pipeline: absolute
shot_name/ai_methodcan no longer escapebase_directory_path.os.path.join's "last absolute path wins" rule meant a stray leading/(typo) or a pasted full path (C:/Windows/junk) in either field would replace the user's intended base entirely —n:/safe+shot_name="/oops"was writing toC:/oopsinstead ofn:/safe/oops. A new_sanitize_segmenthelper (lstrip → splitdrive → lstrip) strips drive prefix and leading separators so segments can only be joined onto the base, never replace it. Applied symmetrically to both directory build and filename build. Rawshot_nameis preserved in the return tuple for downstream nodes that use it as a label. 16 new tests cover the helper and the escape vectors (/oops,\oops,///oops,C:/Windows/junk,/etc/passwdin bothshot_nameandai_method). The JS preview's filename builder picked up the same sanitization plus an empty-base /no_subfolders=truecorner-case fix (/name.exr→name.exr). - Easy AI Pipeline: JS preview and Python runtime now agree on
drive-prefixed
shot_name/ai_methodsegments on every host. Segment sanitization now strips Windows-style drive prefixes with Windows path semantics even when ComfyUI is running on POSIX, soC:/Windowspreviews and executes asWindowsconsistently. Base directory drive roots such asC:/andn:/remain preserved. - Easy AI Pipeline:
no_subfolders=truenow truly produces flat output, even whenshot_namecontains embedded separators. An upstream node feedingshot_name="job/shot"used to silently re-create subfolders via the filename concat — output wasbase/job/shot.extinstead of the flatbase/job_shot.extthe toggle promises. The filename build now flattens any/or\inshot_nameandai_methodto_(filesystems can't have separators in filenames anyway). Directory build withno_subfolders=falsestill uses the separators so users organizing into nested project hierarchies aren't affected. - Easy AI Pipeline:
no_subfolders=truekeeps the version folder. Previously the toggle stripped everything underbase—shot_name,ai_method, AND thev###version subfolder — which meant versioned outputs all collided in the same flat directory. New behavior:no_subfoldersonly dropsshot_nameandai_methodfrom the directory (they still appear in the filename). The version folder is added whendisable_versioningis off, so versioned outputs stay organised underbase/v###/. Truly flat output (no version folder either) requiresdisable_versioning=trueANDno_subfolders=true, matching VFX convention. Tooltip onno_subfoldersandbase_directory_pathrewritten to describe this. AllvNNNreferences in tooltips updated tov###per Nuke-style convention. - Easy AI Pipeline: newlines / tabs from upstream Text Multiline
widgets no longer leak into the path. A new
_strip_control_charshelper runs first in both_normalize_base_pathand_sanitize_segment, plus explicitly onextensioningenerate_pipeline. Removes\n,\r,\tdefensively — these can't legally appear in any filesystem path, but an upstream WASText Multilinenode (or anything with a stray paragraph break) used to ship them through, silently breaking the save downstream while rendering the preview as visually broken multi-line output. Mirrored in the JS preview'snormalizeBasePath/sanitizeSegment/ extension handling. - Easy AI Pipeline: extension widget now strips ALL whitespace.
A single trailing space on
.%04d.exr(easy to acquire via paste) made downstream save nodes that validate the suffix viaos.path.splitext(filepath)[1].lower() != ".exr"(e.g. spacepxl's ComfyUI-HQ-Image-Save) fail with "Filepath needs to end in .exr" — because splitext returns.exrwith the trailing space, which doesn't equal.exr. The previous control-char strip only handled\r\n\t. Nowextensionruns through"".join(extension.split())which removes all whitespace (internal + surrounding, including unicode whitespace and non-breaking spaces). Mirrored in the JS preview'scleanExtension. - Easy AI Pipeline:
shot_name/ai_methodstrip surrounding whitespace too._sanitize_segmentnow calls.strip()after the control-char strip, so an upstream feed with stray leading/trailing spaces (e.g." shot_v1 ") resolves to the same path as the clean form. - Easy AI Pipeline: preview buttons resolve EasyUse GET/SET tunnels.
Get output directory pathandGet output file pathnow followeasy getNodeback to its matchingeasy setNodeinput before reading the source widget value, so global path/name settings preview the same value that render-time execution receives instead of the GET key name. - Workflow sidebar persistence recovery. Workflow library saves now preserve the full workflow payload shape and recover existing user data more defensively, with tests covering payload migration and store persistence.
- Loaded workflows stay untitled in ComfyUI. Loading from the Kforge Labs workflow sidebar no longer forces a saved workflow name into Comfy's tab/title state, keeping inserted or loaded workflow sessions aligned with ComfyUI's native untitled-workflow behavior.
[0.3.2] - 2026-05-16
Added
-
Snapshot dialogs redesign (issue #137). The Save and Load dialogs now carry an inline "Saved to" / "Loaded from" library row at the top of the body (folder name + full path + "Open folder ↗" link) and a bottom command bar with
Save to…/Load from…that opens a new navigate-into folder picker (matching the mockup indocs/designs/snapshot-dialogs.html§6). The picker reuses the existing/koolook/presets/browseendpoint plus an opt-in?files=1parameter that returns child JSON files alongside directories, rendered greyed for a "yes, this is the folder I expected" affordance.The Save dialog's primary button now cycles through the four-state pattern (Default / Dirty / In progress / Done) per mockup §5 — picking a different library via
Save to…relabels Save to "Save to new folder" before the click commits.The Load dialog's auto-save discovery is now handled in one window: when a preset has a newer recovery file (server row-augment now considers
max(periodic.json, pre_load_*.json)instead of justperiodic.json), selecting that preset changes the title to "Auto-save is newer than the saved version", opens the scoped Recovery auto-saves section, and offers explicit "NO - load saved" / "YES - load latest" buttons. Presets without a newer recovery file simply select the row and change the footer action to "Load". Preset deletion is now inline too — clicking × outlines the row red and the bottom Close button transforms to "Yes"; Escape cancels. The delete buttons stay visually neutral until hover.The Settings cog is removed from the Snapshot toolbar (row is now status indicator · Load · Quick Save · Save); library-path management lives inside
Save to…/Load from…. The first pytest run (tests/server/) ships with this change — 12 cases covering the server row-augment and the picker's filesystem helpers — and CI runs them via a new Pytest job. -
Easy Image Batch (
easy_ImageBatch): "White" placeholder +source_batchinput. Theplaceholder_colordropdown now offersBlack / Gray / White(fill values0.0 / 0.5 / 1.0); previously only Black and Gray were exposed. A new optionalsource_batch(IMAGE) input accepts a pre-batched image stack (e.g. straight from Load Video). Whensource_batchis connected and a slot's individualimageNis not connected, the node pulls VFX frameimageN_framefromsource_batch[imageN_frame - 1]— soimageN_framecontrols both which source frame to pick and where to place it on the output timeline. This collapses the typical "Load Video → Get Images From Batch In Range × N → Easy Image Batch" helper chain into the node itself. ExplicitimageNinputs still override the source-batch pick for that slot, so existing workflows keep working unchanged.image1is now optional too (you can drive the node entirely fromsource_batch); a clear error is raised if neitherimage1norsource_batchis connected. -
Easy Image Batch unlimited keyframes via
source_framesfield. A new optional multiline STRING input (positioned right aboveimage1_frame) accepts a list of VFX-numbered frames separated by commas, newlines, spaces, or any mix (e.g."1, 27, 41, 63, 85, 120"or one number per line) — for sequences needing more than 4 keyframes from asource_batch. Each number both pickssource_batch[N - 1]and places it at output indexN - cut_start_frame(same convention as the per-slotimageN_framefields). Bad tokens warn and are skipped; out-of-range values warn and are skipped.List-vs-slot interaction rule: when
source_framesis non-empty, the 4 manual slots contribute ONLY whereimageNis explicitly connected — theimageN_framedefaults (5, 9, 13, 17) do NOT pick fromsource_batchin this mode. So the list fully controls the selection, with explicitimageNconnections used solely to override individual positions. Whensource_framesis empty, the original 4-keyframe behaviour is preserved (unconnected slots fall back tosource_batch[imageN_frame - 1]).Priority order (later wins):
source_frameslist → 4 manual slots. Output dedup is automatic (set-based tracking). -
Easy Image Batch: two new outputs —
selected_image_batch(IMAGE) andselected_frames(STRING).selected_image_batchcontains only the keyframes that actually landed, packed back-to-back in ascending timeline order with no placeholders (length 0 if nothing was placed).selected_framesis a comma-separated VFX-frame-number string of the placed keyframes (e.g."1, 27, 41, 63") — same format as thesource_framesinput, so the output round-trips cleanly into anothereasy_ImageBatchinstance for chaining or re-using selections. Outputs are appended at slot positions 3 and 4, so existing wires onimage_batchandalpha_batchare unaffected. -
Easy_Patternnode under categoryKoolook/Testing. Generates a batch of solid-color frames[B, H, W, 3]with optional per-frame index overlay; configurablestart_from/step/zero_pad/prefix/suffixfor the stamped text, and five overlay positions (center/top-left/top-right/bottom-left/bottom-right). Background and text colors each have a*_color_modedropdown (White/Black/Gray/Custom) backed by an optional hex string consulted only when the mode isCustom— same idiomEasyResize_Koolookalready uses forpad_color_modeandpanel_color_mode, so "Gray" means the same#80808050%-gray across nodes. Defaults out-of-box to an 81-frame magenta pattern with white centered text — useful for verifying sequence merging, frame insertion, batch indexing, and video-pipeline frame order at a glance. Drafted in an external AI chat, ingested in-tree, relicensed from the AI's MIT boilerplate to the project's GPL-3.0-or-later.
Changed
-
Easy Image Batch second output renamed
mask_batch→alpha_batch, with the same value convention as before. The default behaviour is unchanged — selected (occupied) frames =0.0(black), missing frames =1.0(white) — so existing workflows wire up identically. A newinvert_alphaboolean toggle (defaultfalse, labels "inpaint" / "compositing") flips the alpha to compositing-style (selected = 1.0, empty = 0.0) when needed. The output slot keeps its position (second output) so wires re-attach automatically when the node reloads. -
Easy Image Batch:
start_framerenamed tocut_start_frameand reinterpreted as a built-in cut window. The output now represents VFX frames[cut_start_frame .. cut_start_frame + total_frames - 1]. Frames placed outside this window are silently dropped from the output (a single end-of-run summary lists them, replacing the previous per-frame "out of range" warnings). Math:source pick index = vfx_frame - 1 (hardcoded VFX 1-based) output (cut) index = vfx_frame - cut_start_frame (cut window)The two were previously conflated under
start_frame, which produced the wrong source pick wheneverstart_framediffered from1. With the new model each slot pickssource_batch[vfx-1]and places it at output indexvfx - cut_start_frameindependently.Builds in the workflow that previously used an external
ImageFromBatch (batch_index, length)to slice the Easy Image Batch output. Defaults:cut_start_frame=1,total_frames=81(was16),imageN_framedefaults bumped to5, 9, 13, 17(was0, 4, 8, 12) so they're valid VFX-1-based frame numbers.Migration note: saved workflows that had
start_frameset will load with the field's stored value intocut_start_frame(same field position, same units). Behaviour is preserved whencut_start_frame=1is the default and the source_batch starts at VFX frame 1; if you had setstart_frameto a non-1 value, re-check that your source pick result is still what you want — under the old code,start_frame=Nalso offset the source-batch index, which the new model intentionally does not. -
scripts/sync_to_dev.pyauto-restarts the live ComfyUI server by hitting ComfyUI-Manager's/manager/rebootendpoint after a successful sync. Default-on because dev-sync is invoked precisely when the maintainer wants new code visible, and Python custom-node modules load once at server start — without a restart,.pychanges stay invisible. Use--no-restartto stage files without disturbing the live session;--restart-urloverrides the endpoint for non-default ComfyUI hosts/ports. Restart failures (Manager missing, server not running, etc.) print a diagnostic but do not fail the sync. TheRUNTIME_PATHSmanifest also gainsk_easy_pattern.pyso new top-level node files reach the dev install instead of being silently dropped — without this entry the modified__init__.pywould import a module the live install doesn't have, aborting the whole pack's registration. -
Release preflight no longer blocks on ComfyUI-Manager metadata drift.
tools/preflight_release.pystill verifies local node registration, VAE dispatch, and workflow fixtures by default, while the upstreamextension-node-map.jsoncomparison remains available as--check manager-metafor advisory checks. Releases should not depend on third-party metadata updates landing on ComfyUI-Manager's schedule.
Fixed
- Snapshot Settings folder save no longer writes a snapshot. Saving the library-path field in the Settings dialog no longer writes the in-memory snapshot. Folder-save is now path-only: the button reads "Save folder", the hint points to Save / Quick Save, and snapshot writes remain explicit actions. Resolves #129.
[0.3.1] - 2026-05-10
Changed
- Comfy Registry documentation metadata and README onboarding now point new users to the KForge Labs ComfyUI-Koolook spotlight as the public first-read overview, while keeping the bundled visual guide framed as the in-app operational guide.
- The bundled visual guide's six recipe examples now use the KForge spotlight style: wide divided showcase rows with explanatory copy paired to the relevant screenshot, instead of separate compact recipe cards.
- The bundled visual guide now includes a Tags showcase between Modules and Search, showing how saved workflows can appear under multiple labels and remain searchable without duplication.
[0.3.0] - 2026-05-08
Added
- Visual sidebar guide now hosted on GitHub Pages. The bundled guide at
web/guide/index.htmlis now also live at https://malkuthro.github.io/ComfyUI-Koolook/web/guide/ so users browsing the repo on github.com — and visitors landing on the package page in the Comfy Registry — can preview the onboarding without cloning. The in-app Tools → Help (H) button continues to open the bundled local copy viaimport.meta.url, so the guide stays available offline. README andpyproject.toml'sDocumentationURL now point at the Pages site (replacing the unmaintained wiki link). - Load dialog asks YES/NO when an auto-save is newer than the saved
version. Click any preset row in Snapshot → Load and, if the
matching
<preset>_autosave/periodic.jsonis more recent than the named save's on-disk mtime, the confirm dialog upgrades from a single "Load preset" button to a YES/NO question: "Auto-save is NEWER than the saved version. Do you want to load the auto-saved version?". YES loads the auto-save, NO loads the older deliberate save (the row the user originally clicked), Esc cancels. Both paths run the standard pre-load auto-save protection so either choice is reversible. Replaces the earlier inline "↻ Newer auto-save" subline on the row itself — putting the question on the row asked the user to compare two timestamps before they had committed to loading anything; moving it to the post-click modal puts the choice exactly where the user has already decided to load and just needs to pick which version. The list rows render as before (one timestamp, one × button) so the library list stays scannable. Server-side:/koolook/presets/listaugments root rows withlatestAutosaveMtimevia one extrastat()per row — no file read, cheap on large libraries. Aligns the system with the mental model: named saves stay deliberate (only updated on Save / Quick Save), and the auto-save is treated as a recovery copy the system surfaces at decision time rather than a parallel save destination the user has to remember to check. - Bundled visual onboarding guide for the Kforge Labs sidebar. The
existing Tools-row
Hhelp shortcut now opensweb/guide/index.html, an offline visual guide that frames Snapshots as a reusable project kit for favorite nodes, cross-sidebar search, saved workflows, modules, tags, autosaves, and starter presets. Its static styling borrows the dark, token-driven card language from the ForgeFlow sibling project's design docs. The first version ships annotated mock images underweb/guide/img/so they can be replaced by real ComfyUI screenshots later without changing the guide structure. Linked from the README and user guide index for users browsing docs outside ComfyUI. - Subgraph-aware module saves. Selection saves now carry the
transitively referenced
definitions.subgraphsentries for selected ComfyUI subgraph wrapper nodes, so the saved workflow has the definitions available for native Load / later insert attempts. - Dev-sync build-tag footer at the bottom of the Kforge Labs sidebar.
scripts/sync_to_dev.py --scope "<short scope>"now writes a tinyweb/_dev_build.jsonnext to the sidebar JS. The sidebar fetches it on render and emits a discreet two-line footer:dev <sha> · <time>(line 1, SHA at 13px monospace) + the scope (line 2, italic proportional). Visible-in-app counterpart to the chat-report convention from #95 — when the maintainer is juggling multiple parallel worktree sessions, a glance at the footer confirms which one's code is actually live in the browser tab. Cache-busted fetch so a re-sync's new mtime reaches the browser without a hard refresh. Absent on registry installs (the JSON is dev-only and gitignored); the footer renders empty there. The--scopevalue is the same ≤10-word change-summary the agent quotes on chat-report line 2 — passing it once now feeds both surfaces. ×removability across both group modes for any visible favorite, including auto-pulled REPOS{select: "all"} nodes. Adds akoolook.autoPullHidden.v1localStorage set: clicking×on a row that came from auto-pull adds the type to the hidden set so the next render skips it (without the set, the next gather pass would re-pull the type from the registry and the click would feel like a no-op). User picks still go throughremoveFromMyPicks. Re-adding via the toolbar+button or right-click → "Add to Kforge Labs" clears the hide as a side effect ofaddToMyPicks, so users don't have to think about which list a type lives in. Same uniform "remove this from my favorites / bring it back" UX whether the node is a Koolook auto-pull or a KJNodes user-pick.- Snapshot library path browser. Snapshot Settings now has a Browse button
backed by a new
/koolook/presets/browseroute, so users can navigate to the preset folder from inside Kforge Labs instead of copy-pasting a path from an external file manager. The browser hides internal auto-save folders, supports creating one child folder under the current location, and makes the selected folder name visually prominent before the user chooses it.
Changed
- Snapshot timestamps now use 24-hour format with relative-recent tier.
The Load dialog's "saved …" line and the snapshot status-dot tooltip
previously formatted local time as 12-hour
en-US(May 8, 12:56 AM), which read visually like noon for timestamps just past midnight — the classic US "12 AM = midnight" trap. Times now render as:just now(<1 min),12 min ago(<60 min),today 14:32/yesterday 23:58(same / previous calendar day), orMay 6 14:32(older; year appended when not current). Centralized in a newformat_time.jshelper used by both the Load dialog rows and the status tooltip so future surfaces stay aligned. - Sidebar context menus now enforce a single open menu. Opening a workflow, directory, archive, or Workflows-header right-click menu now closes any previous sidebar context menu first, so repeated right-clicks cannot stack multiple floating menus with stale actions.
- "Create directory" moved off the Workflows toolbar onto right-click of
the section header. The 📂 (
pi-folder-open) button used to live in the Workflows action bar between the section label and the Save buttons. Tree- structure operations (create / rename / delete a folder) already live on right-click of the row representing the structure being mutated — the per- folder menu offers "Create subdirectory…" / "Rename directory…" / "Delete directory" — but top-level dir creation was the lone outlier sitting in the toolbar. Moving it to right-click on the "Workflows" header row makes the action bar homogeneous (only save actions: Save canvas, Save selection) and unifies the mental model: every tree-structure operation lives on right-click of the structure node it acts on. Implemented via a new optionalrootContextMenufield on section descriptors plumbed throughbuildFolder; only Workflows opts in for now (Nodes/Tags keep today's no-context-menu section header). Fresh-install path is unchanged — the Save modal's "+ New directory…" entry still creates the first directory when the workflow store is empty. Closes #104. - Kforge Labs sidebar action icons now use a first-pass ForgeFlow-style
icon set. Snapshot Load / Save, Tools export / install / help, the
missing-pack canvas handoff, repo-mode grouping, and whole-canvas workflow
save now use the approved compact letter / simple-glyph treatment from
docs/designs/sidebar-icon-proposals.html, reducing repeated generic cloud / grid / floppy symbols while leaving all actions and tooltips unchanged. The existing Tools-rowHhelp shortcut is now wired to the bundled visual onboarding guide. - Node help text tightened for scannability. The Easy Resize description and HDR VAE encode/decode tooltips now use shorter, action-oriented text so the ComfyUI node help stays readable while detailed behavior remains in the user guide.
- README refocused as a short product front door. The repo landing page now leads with the Kforge Labs sidebar, Snapshots, search, workflows, and recovery, then links out to the deeper user/maintainer/fork docs instead of carrying long maintainer workflow and implementation sections inline.
- Node-list hover previews now stay at the intended 300px mock-node width.
The preview card previously had only a
min-widthplus viewportmax-width, so long node descriptions could set the card's intrinsic width and stretch the hover overlay across the canvas. The card now uses a fixed 300px preferred width clamped to the viewport, keeping labels and descriptions contained. Its surface colors also now track ComfyUI's native node charcoal grades more closely, so mock previews feel like lightweight nodes instead of black sidebar tooltips. - Theme mode now mirrors repo-mode population — auto-pulled
REPOS{select: "all"} nodes appear alongside user picks (deduped),
filtered by the new auto-pull hidden set. Supersedes the earlier
[Unreleased]"Picks-only (no REPOS-driven auto-pulled candidates)" intent below: the picks-only model dropped the entire Koolook pack from theme mode on a stock install, which silently violated the "my favorites" mental model (the user expected their visible favorites in repo mode to also show in theme mode, just regrouped). With the new × removability, the user can still curate exactly what they see — without the discoverability cost of dropping auto-pull entirely. - Pack-name badges restored on theme-mode leaves. Small dim
Koolook/KJNodesetc. on the right of each row, same opacity as the search-flatten breadcrumb. Memory aid for "where did this node come from?" — useful when the user is learning their favorites pack-by-pack. Reverts (for theme mode only) the earlier[Unreleased]removal further down; search-flatten still skips the badge since its breadcrumb prefix already conveys origin. - Snapshot status hover and path display tightened. The status tooltip now shows only a central-time timestamp and the snapshot library location, and snapshot library path lines wrap to show the full path instead of truncating.
- Snapshot Settings save state clarified. After browsing to a new folder, the Save button now becomes an explicit dirty "Save update" action; once the path is persisted it turns back into a disabled gray "Saved" state. Load and Settings dialogs now show the active library folder name first, with the full path underneath; after changing folders, Settings warns that the current snapshot still needs Save / Quick Save to be written into the new location.
- Sidebar second mode — "Theme" instead of "Category". The sitemap-icon
toggle in the Nodes action row now groups picks by semantic theme
rather than by raw CATEGORY first-segment. The new algorithm strips the
source pack's name from the front of each pick's CATEGORY (via the
existing
findPackPathForTypeprecedence) before grouping, soKJNodes/image/Get,Koolook/Image/EasyResize, andimage/fooall collapse to one top-levelimagefolder. Each theme bucket renders as a flat sorted list — no further sub-folders. Picks-only (no REPOS-driven auto-pulled candidates) so the second mode genuinely reflects "my image-related favorites" rather than "every image-shaped node ComfyUI knows about." Synthetic buckets(unresolved)/(uncategorized)survive as before. - Pack-name badges removed from leaf rows. The small dim labels
(
Koolook,KJNodes,EasyUse) that used to render at the right edge of every leaf in theme mode and in search-flatten are gone. The breadcrumb prefix on search-flatten rows already conveys origin in a cleaner form, and theme mode by definition doesn't care about source pack — the badge was redundant.makeNodeLeafRowno longer accepts apackBadgeparameter; CSS rule.koolook-pack-badgeremoved fromconstants.js.
Fixed
- Cancel / Escape / click-outside on the "Replace current workflow?"
placeholder-drop modal no longer leaks a pending Promise (closes
issue #84 part 2).
dropPlaceholdersForPacksinweb/sidebar/canvas_io.jswrappedshowConfirmModalinnew Promisebut only resolved on the OK path — every cancel left an unresolved Promise behind. The Promise now resolves with{ ok: false, reason: "cancelled" }(matching the function's other failure shapes) on every non-OK dismissal. Fix is two-layered:dropPlaceholdersForPackspasses anonCancelresolver, ANDshowConfirmModalinweb/sidebar/modals.jsnow firesonCancelfor the Cancel button, Escape, AND overlay-click via an idempotent settle flag (previously only the Cancel button fired it). Closes a latent leak on the recovery toast's "Discard offline copy" path atweb/koolook_sidebar.js:130for free — that caller had wiredonCancelfor the Cancel button only, so pressing Esc / clicking outside used to leave the toast button stuck in "Discarding…". - Snapshot status reads "auto-saved" — not "saved" — after restoring
from an auto-save. Previously, restoring
<preset>_autosave/ periodic.json(via the new YES path on the Load dialog OR the long-existing Recovery section) calledmarkStateSaved(), which baselined the named-save fingerprint to the auto-save's content. The dot turned green even though<preset>.jsonon disk still held the OLDER deliberate save — blurring the deliberate-save model the Path C UX deliberately reinforces. NewmarkStateAutosaved()export insnapshot.jsbaselines the periodic fingerprint instead, so the dot reads "auto-saved" (blue) until the user explicitly Quick Saves or Saves the restored state into the named file. Toast message also updated to nudge:Restored auto-save of "Foo" — Quick Save to commit to the named file.Fixes the regression flagged on PR #111 review.
Changed
- Load dialog header + Recovery group headers now share a layout. The
library section and each
<preset>_autosavegroup header now use the same two-line format (title + full path) and the same📂 Openbutton styling — visual consistency across both surfaces. Concrete changes:- Saved-time on every row. Regular snapshot rows now show
saved May 7, 11:53 PM(from the file's mtime, threaded throughloadPreviewfrom the listing endpoint) instead of justexported 07/05/2026. Recovery rows already had this format; both row types now read consistently. Falls back toexported <date>if mtime isn't available (defensive — covers any caller that doesn't carry listing metadata). - Recovery group headers now show the full subdir path under the
subdir name, mirroring the library header's
Library folder: X/<full path>pair. ReusesrenderLibraryLocationwith a newtitleoverride so the same CSS styling applies without the "Library folder:" prefix. - Library
📂 Openbutton now useskoolook-snapshot-row-btn(the smaller styling) to match the per-group Open button. Waskoolook-modal-button(larger) before — visually mismatched. formatAutosaveMetacollapsed back intoformatPreviewMetasince the format is now identical across row types.
- Saved-time on every row. Regular snapshot rows now show
Added
- Offline-fallback banner now has Restore + Discard actions — no more
permanent guilt trip. The red
criticalToastthat fires when localStorage holds a stale outage-fallback blob used to be a one-way street: Copy details, Dismiss, repeat next page load forever. Two buttons close the loop:- Restore as snapshot wraps the offline workflows into a full
snapshot envelope (live picks bundled in so a later Load wouldn't
wipe them) and writes it as
recovered-<iso>.jsoninto the snapshot library, then clears the localStorage key. The user lands on a durable, in-UI-visible artifact they can inspect, Load, or selectively cherry-pick from. Banner self-extinguishes on success. - Discard offline copy asks for confirmation, then clears the localStorage key and dismisses the banner. For the case where the user has already verified nothing is missing.
- The toast is now wired from
koolook_sidebar.js(the entry point) rather than fromloadWorkflowsStore()itself — the action handlers needwritePreset/loadUserPicks/showConfirmModal, and pulling those into the workflows store would create a circular import. criticalToastnow accepts anactions: [{label, onClick, primary?, busyLabel?}]array; existing callers ({ copyText }and string- only) keep working unchanged.
- Restore as snapshot wraps the offline workflows into a full
snapshot envelope (live picks bundled in so a later Load wouldn't
wipe them) and writes it as
Added
- Recovery section deep-links to the file system + per-row save time.
The Load dialog's Recovery auto-saves section now exposes everything
a power user needs to navigate the autosave subfolders directly:
📂 Openbutton at the top of the Load dialog opens the snapshot library folder in the OS file manager.📂 Openbutton on each<preset>_autosavegroup header opens that specific subfolder, so the user lands on the exact recovery files for that preset (with mtimes visible to Finder / Explorer sort).- Per-row save time replaces "exported <date>". Recovery
rows now show
saved May 7, 2:34 PMderived from the file's mtime — strictly more useful than the snapshot's self-reportedexportedAtfor telling "which pre-load is freshest." Mtime also survives out-of-band file copies in Finder. - Bare filename instead of redundant displayName. Each row now
renders the actual filename (
pre_load_2026-05-07T14-32-…,periodic) instead of the noisyPeriodic auto-save · <subdir> · <iso>string that pushed the timestamp off-screen. - New server route
POST /koolook/presets/reveal[?dir=<subdir>]powers both buttons via the platform-appropriate file-manager command (openon macOS,explorer.exeon Windows,xdg-openon Linux). Path-traversal grounded at the library base via the same_resolve_targetcheck the file-write routes use.
Fixed
- Insert pre-flight toast now surfaces both pack misses AND subgraph misses when a saved workflow has both, in plain English. Previously, the "all-misses-are-subgraph-UUIDs" early-return only fired when every missing type was a UUID; mixed cases fell through to a single "install the required pack(s)" message that listed UUIDs alongside pack types but mentioned nothing about the registration step the subgraph also needs. Users installed the pack, retried Insert, and hit a second unexplained "still missing" failure. Now the toast branches three ways — pure-pack / pure-subgraph / mixed — and the mixed case names both fixes upfront as a numbered two-step ("install pack(s) + right-click → Load it once"). Wording also de-jargoned — "subgraph definition not registered" was technically accurate but confused users who just wanted to know what to do. Engineer-speak stays in code comments and console.warn for devs; user-facing copy now talks in gestures and outcomes ("Right-click → Load it, then left-click Insert will work").
- Atomic preset writes — server crash mid-save can no longer corrupt
your snapshot library, and a hostile
.tmpsymlink can no longer redirect writes outside the library.POST /koolook/presets/filenow usestempfile.mkstemp(withO_CREAT | O_EXCLsemantics) to create a uniquely-named temp file inside the already-validated target directory, thenos.replaces onto the final name. A dropped connection or process kill mid-write can no longer truncate the existing good file. An attacker who plants<file>.json.tmpas a symlink pointing outside the library can no longer trick the server into following it:mkstempaborts withO_EXCLon any existing dirent at the chosen name, AND the name is randomized so the attacker can't pre-plant anything there. Covers every preset write path (named, periodic autosave, pre-load autosave, recovery snapshot) through the one route. The transient temp file is still invisible in the Load and Recovery dialogs (both list endpoints filter to.json). showConfirmModalnow exposes anonCancelcallback. Callers that wrap the modal in a Promise can now settle when the user cancels instead of leaking a pending Promise. Closes the second half of #84 (dropPlaceholdersForPackswas the original report); also removes the "Discarding…" stuck-button footgun on the offline-fallback recovery toast — Cancel now re-enables the toast button so the user can choose Restore / Copy / Dismiss instead of being trapped in a half-disabled state.- Snapshot status timestamps now follow the user's wall clock.
formatCentralTimewas hardcoded totimeZone: "America/Chicago"and surfaced aCDT/CSTstamp in the auto-saved hover tooltip regardless of where the user actually was. Renamed toformatLocalTimewith the timezone option dropped — the browser formats with the user's system timezone. Also droppedtimeZoneName: "short": it just rendered noisy abbreviations (GMT+2,CDT, …) that didn't help anyone identify "their own time." Tooltip now reads as a clean wall-clock stamp likeMay 7, 2026, 11:43 PM.
Added
- Modules — splice a saved cluster into your live canvas instead of
replacing it. Tag any saved workflow with the literal tag
moduleand the Kforge Labs sidebar starts treating it as a reusable building block: a greenpi pi-plus-circleicon replaces the file glyph, left-click inserts the cluster at the viewport center (with internal links re-created and the new nodes left selected), and a distinct hover tooltip surfaces the changed semantics. The selection-save modal now ships aSave as modulecheckbox (pre-checked for selection saves; unchecked for whole-canvas saves), which adds themoduletag inside the samepersistMutationas the entry write so a commit failure rolls back both. The right-click menu on every workflow row gains an explicit Insert into canvas entry alongside the existing Load — Load still works on every row, module-tagged or not. New primitiveinsertWorkflowOntoCanvasinweb/sidebar/canvas_io.jspre-flights every referenced node type againstLiteGraph.registered_node_typesand aborts cleanly when any are missing (a partial insert with stub nodes is worse than no insert), then deep-clones the saved graph, letsapp.graph.add()allocate fresh node ids so nothing collides with the live canvas, and recreates internal connections viaoriginNode.connect(...)so link ids are auto-allocated too — no manual link remap needed. Designed for setups like an EXR-output stack, a depth-only render cluster, or a Wan 2.2 prompt module the user wants to drop into many workflows. Full reference indocs/maintainers/workflows-sidebar.md. - Spotlight effect on add. Clicking the toolbar
+(or the canvas right-click "Add to Kforge Labs") now collapses every Nodes-section pack folder, then auto-expands just the pack + subcategory the just-saved node lives in — a pedagogical aid that helps new users internalize which pack each node belongs to. Multi-select adds light up every hit pack simultaneously. Duplicate adds (already in picks) trigger the same spotlight, since the educational reminder still applies. HelperfindPackPathForTypemirrors the gather code's REPO-precedence-then- user-pick-fallback to locate any node ID's sidebar path; new exportedspotlightAddedPicks(typeNames)fromweb/sidebar/tree.jsdoes the collapse + pin sequence and is called from both add paths. - Group-by-category mode for the Nodes section. Segmented toggle in
the Nodes action row (📦 Repository / 🌐 Category —
pi-database/pi-sitemap); choice persists in localStorage (koolook.groupMode.v1) and survives reloads, including cross-tab via the existingstorageevent listener. In repo mode (default) every pick lives under its pack; in category mode picks are regrouped by their node-classCATEGORYpath, ignoring REPOS affiliation. Categories that look the same after canonical-key normalization (lower().replace(/[\s_\-]+/g, "")) collapse into one folder —Loaders/loaders,Image/Upscaling/image/upscaling, andstyle_model/StyleModel/style modelall merge. Display label is the most-common original casing seen for that key (ties → first-seen, stable across renders because Map iteration follows insertion order). Ports of the same path under different repos all collapse together, so a Loader from Pack A and a Loader from Pack B share theLoaders/folder. Two synthetic top-level buckets surface the edge cases:(unresolved)for picks whose pack isn't currently loaded (repo mode silently dropped these — category mode keeps them visible as italic-dim rows),(uncategorized)for picks whose node class has noCATEGORY. Each leaf in category mode carries a small pack-name badge so users still see "where did this come from" — load-bearing for the existing↓ Install missingflow. Folder paths inpathStatesuse the canonical key (not the resolved display label), so an upstream casing change to the most-common label doesn't reset user expansions. Closes the "sort by topic, by what they do" piece of #46. (#73) - Flatten-on-search for the Nodes section. When the search field is
non-empty, the Nodes section drops its tree structure and renders a
flat list of matching leaves, sorted by display name, each with a
small dim-grey breadcrumb prefix (
Loaders › LoRA › LoraLoaderin category mode,Pack › Subcategory › Displayin repo mode) so the spatial-origin signal survives. Workflows and Tags sections retain their tree-under-filter behavior — the change is scoped to Nodes only. The breadcrumb collapses redundant synthetic labels: the(root)subcategory (which adds no info beyond the pack name) and any subcategory whose label duplicates the pack label (the "(uncategorized)" double-up). Always-flatten on any non-empty query is the deliberate choice over a count-threshold gate; the spatial cue from a tree is mostly already lost once a search has narrowed the set, and a flat sorted list scans faster. (#73) - Hover preview card for sidebar leaf rows. Mirrors ComfyUI's
official Node Library preview (NodePreview.vue): plain
HTML/CSS card with a colored title bar (HSL hue hashed from the
CATEGORYstring), category breadcrumb, two columns of type-colored slot dots (inputs left, outputs right), widgets section with truncated defaults, optional description. Hover a leaf for ~250ms to show; mouseleave dismisses; only one card visible at a time (module-level singleton). Card positions to the right of the row by default (12px offset), flips to the left when it'd overflow the viewport, clamps inside an 8px viewport-padding gutter. Tall cards (a node with many inputs) cap atcalc(100vh - 16px)and scroll internally rather than clip.pointer-events: noneon the card so it never intercepts events from rows it floats over. Slot dot colors read ComfyUI's runtime palette (app.canvas.default_connection_color_byType, populated at canvas init) first, then fall back to LiteGraph's staticLGraphCanvas.link_type_colors(mostly empty in stock LiteGraph), then a neutral grey. Widget classification mirrors ComfyUI's frontend rule: scalars (INT/FLOAT/STRING/BOOLEAN) and arrays-of-choices (COMBO) become widgets, everything else is a connection slot.INPUT_TYPES()is read defensively (some custom-node packs throw under unusual conditions); when one does throw, the failure is logged viaconsole.warnso a broken pack is visible during debugging. Picks whose pack isn't loaded show a "Pack not loaded" stub card pointing at the Tools-row install button. The hover card additionally tears down onvisibilitychange(page hidden) andwindow.blurso it doesn't leak across tab switches or focus loss. New moduleweb/sidebar/node_preview.jsholds the preview engine;attachHoverPreview(row, type)is wired insidemakeNodeLeafRowso every leaf-emit site (repo tree, category tree, search-flat) gets the preview for free. The renderer callsteardownPreview()before eachtreeEl.innerHTML = ""— without it, an active card whose anchor row gets detached during a re-render would leak (thepointer-events: nonecard can't be dismissed by clicking it). (#73)
Changed
- Hover preview rebuilt to read like a real node mock. The original
layout (HSL-hashed title strip + two-column slot table + flat widget
text list) read as a generic info card. Reworked to mirror upstream
NodePreview.vuemore faithfully: a flat header row (small colored dot + node display name) over a red PREVIEW badge, slot rows laid out as a 5-column grid ([dot] [input-name] [spacer] [output-name] [dot]) so inputs and outputs line up horizontally as parallel sockets, and widgets rendered as individual rounded pills with◀ name [spacer] value ▶chrome to mimic LiteGraph widget UI. Bigger fix underneath:readSlotsand the title / category / description reads were looking at staticnodeClass.INPUT_TYPES/RETURN_TYPESproperties, which don't exist on ComfyUI-registered nodes — ComfyUI parks the original V1 def atnodeClass.nodeData(set bylitegraphService.ts'sregisterNodeDefright beforeLiteGraph.registerNodeType). Every card was rendering as just-the-header-and-badge because the slot arrays came back empty. Reading fromnodeDatafirst (with the old property names as a legacy fallback for non-ComfyUI registered nodes) fixes the empty-card class. (#78)
Fixed
- Module save/insert now preserves internal links more defensively.
Selection save and module insert both accept serialized link arrays and
object-shaped LiteGraph
LLinkrecords, compare node/link ids by stable string keys, and strip stale saved link ids before recreating links vianode.connect(...). Saved module state is now first-class (module: true) while still honoring the legacymoduletag, so left-click module insert and right-click Tags-based toggling stay in sync. pinExpandedpaths now expand on the immediate render instead of being delayed by one.buildFolder's expansion-resolution chain gained apinnedPathscheck betweenforceExpandedandpathStates. Phase 3 ofrenderTreewrites pins intopathStatesAFTER Phase 2 has built the DOM, so without the new check the pin had no effect on the current render — only on the next one. Side-effect-free correctness improvement for the workflow-save pinning that already existed; load-bearing for the new spotlight feature above which depends on pinned paths expanding immediately.- Modal drag-out-of-input no longer dismisses the dialog. The shared
modal shell (
makeModalShell) used by every Koolook dialog gained a mousedown / click-intent check: a click on the overlay now only fires the dismiss when the gesture started on the overlay too. Drag- selecting text inside an input field that releases in the overlay's dark area used to triggerclickwithtarget=overlayand dismiss the modal mid-edit; now it stays open. Affects all dialogs including Snapshot Save / Load / Settings, Install Missing, Save Workflow, Tags, Confirm, Input — load-bearing especially for the Snapshot Settings path field where the long absolute path encourages drag-selection. - Snapshot Settings path field shows full path on hover. Added a
titleattribute synced to the saved library path on every refresh and save, so the user can read the full path without scrolling / selecting inside the narrow input.
Added (continued)
- "Install missing for picks" toolbar button in the Nodes section
(
pi-cloud-downloadicon, next to Add and Export). Walks the user's picks against ComfyUI-Manager's/customnode/getmappingsmapping, buckets into already-installed / will-install / unresolved, queues unique git URLs through/customnode/install/git_url, polls/manager/queue/statusto drive a progress bar, and prompts to reboot. Works on any install with ComfyUI-Manager loaded; falls back to a clipboard URL list (with acomfy node installhint) if Manager isn't reachable. New moduleweb/sidebar/installer.jsholds the Manager-API client + resolver; the modal inweb/sidebar/modals.jsdrives the four-phase UI (discovery → confirm → progress → result). 403s from Manager's security gate are surfaced as actionable language ("your security level forbids git-URL installs") rather than raw HTTP. - Snapshot library — save your full Kforge Labs state (curated picks
- the entire workflows store including tags + archive) as a named preset to a configurable filesystem path. New top-level Snapshot action row above the search field with three icon buttons, each opening its own focused dialog:
- Save (cloud-up icon) — one click. If a preset is currently
loaded, prompts "Save over ''?" with three options
(Save overwrite / Save as new… rename / Cancel). If
no preset is loaded yet, prompts for a name (default
preset YYYY-MM-DD, fully editable). The "current preset" is tracked in localStorage and persists across reloads, so Save keeps doing the obvious thing across sessions. - Load (cloud-down icon) — single dialog. Lists every preset in the library with metadata (workflow count · pick count · export date). Click a row → confirm "Replace current state?" → restores picks + workflows. Each row has an × button for delete (with confirm). Header line shows the current library path so you can see where you're loading from.
- Settings (cog icon) — single field for the library's filesystem path. Save writes to a per-install settings file the server reads. Reset to default clears the saved path so the server falls back to env-var or built-in default. A read-only line shows the currently-resolved path + source (settings panel / env var / built-in default), so what's in effect right now is always visible.
- Configurable storage location. Resolution chain (highest first):
- Path saved via the Settings dialog
(
<comfyui-userdata>/koolook-settings.json'slibraryPath). KFORGELABS_PRESETSenv var (deployment / facility config).- Built-in default
<comfyui-userdata>/koolook-presets/. Use cases:
- Personal cross-machine sync: point at a Dropbox/iCloud/Drive folder; the library follows your machines.
- Facility shared library: point at an NFS/SMB mount writable by every workstation; all workstations save to + load from the same library natively, no symlinks.
- Read-only distribution: point at a path the workstation can
read but not write; save fails cleanly with the server reason in
a toast, load works.
Snapshot files carry a
kind: "koolook-snapshot"discriminator +versionfield for future schema migrations. Closes the "save to a custom location, take it elsewhere" piece of #46.
- Path saved via the Settings dialog
(
Internal
- New server-side module
koolook_routes.pyregisters the/koolook/presets/*aiohttp routes on ComfyUI's PromptServer. Endpoints:info,list,file(GET/POST/DELETE on a single query-string-keyed endpoint, with the GET handler short-circuiting HEAD requests via aiohttp's auto-HEAD-from-GET so existence checks don't read the full file body), andsettings(GET/POST). The HEAD short-circuit lives inside the GET handler rather than as a dedicated@routes.headregistration because ComfyUI's mirror-to-/apicode inserver.pyblindly forwardsRouteDef.kwargsinto aRouteTableDef.route(...)closure that rejectsallow_head=False, so any kwarg-based opt-out crashes startup. Path-traversal protection at the route boundary via a strict filename whitelist regex; symlink protection via post-resolveis_relative_tocheck on every file op. Settings file is written atomically (tmp + os.replace) so an interrupted process can't truncate the user's saved library path. The library directory is auto-created on first save. replaceAllWorkflowsnow uses the samesnapshotCacherollback primitive aspersistMutationso a snapshot apply that fails to persist rolls the in-memory cache back to its pre-call state — the load is fully atomic. Theworkflows_store.jsmutator-invariants doc block listsreplaceAllWorkflowsas the fourth legitimate rebind site.- The Load dialog now gates the
currentPresetNametracker onpicksOk && workflowsOk. Partial-failure paths clear the tracker so the next Save forces a fresh name prompt rather than offering to overwrite the on-disk preset with corrupted half-state. - The Load dialog clears the tracker if the user deletes the currently-loaded preset.
- Client-side
sanitizeNamenow mirrors the server's filename whitelist regex — invalid characters collapse to_rather than hitting an opaque HTTP 400 from the server. ThepresetExistsprobe is now tri-state (true/false/null); the Save flow refuses to write when it can't reach the library to verify the name. - Server error reasons are surfaced via
await resp.text()rather thanresp.statusText, since HTTP/2 (RFC 7540) strips reason phrases — behind any HTTP/2-terminating proxystatusTextis empty and the server's helpful "read-only mount" / "invalid filename" / "parent missing" messages would otherwise be lost.
Changed
- Save selection toast distinguishes "no selection" from "selection
points at deleted nodes." Previously both produced the generic
"Select one or more nodes on the canvas first." Now the deleted-node
case (selection set non-empty but every id refers to a removed node,
common after undo/redo) shows "Selected node(s) no longer exist.
Click a node on the canvas to re-select."
serializeSelectionreturns a discriminated{ kind: "empty" | "stale" | "ok", graph? }result so the caller can route messaging precisely. curated_defaults.jsonretired in favor ofstarter_preset.json. Fresh installs no longer get picks seeded directly into localStorage on first load. Instead the bundledweb/starter_preset.json(full snapshot format — picks + workflows + tags + archive) is copied into the user's snapshot library directory asstarter.json, and the user opens Snapshot → Load to apply it in one click. The change unifies fresh-install distribution with the per-user snapshot library (#68): the maintainer flow becomes "build state in ComfyUI → click the ↓ Tools-row button → paste overweb/starter_preset.json→ commit." Existing users with non-empty picks are explicitly skipped by the new seeder, so their state is untouched. Removed:seedDefaultsIfNeeded,exportPicks,web/curated_defaults.json,SEEDED_KEY,DEFAULTS_URL. Added:seedStarterPresetIfNeeded,exportStarterPreset(both inweb/sidebar/snapshot.js),STARTER_SEEDED_KEY,STARTER_URL,STARTER_PRESET_FILENAME,web/starter_preset.json.- Sidebar toolbar: Tools row split out from Nodes row. The Export
button (now "Export starter preset") and the "Install missing for
picks" button moved up out of the Nodes row into a new dedicated
Tools row above the search field, alongside a new "Drop
placeholders onto canvas" button. The Nodes row keeps only the
everyday
+Add button. Reasoning: Export and Install-missing are admin/advanced operations; segregating them keeps the daily flow uncluttered and makes it harder to fire them by accident. The new Drop-placeholders button is thesecurity_level=normalescape hatch for "install missing" — it instantiates one placeholder per missing pack on a fresh canvas tab so ComfyUI/Manager's standard "Install Missing Custom Nodes" detection picks them up and routes the install through Manager's UI flow (which doesn't go through/customnode/install/git_url's security gate the way our programmatic call does).
Internal (sidebar tidiness pass — no behavior change)
- Lifted save-modal action sentinels and the cascade picker
sentinels into named constants at the top of
showSaveWorkflowModalinweb/sidebar/modals.js(ACTION_NEW,ACTION_USE_EXISTING,ACTION_MODIFY_EXISTING, alongside the existingNEW_TOP/SAVE_HERE). Magic strings ("new","use_existing","modify_existing") are gone from the function body. - Section-id constants (
SECTION_ID_NODES,SECTION_ID_WORKFLOWS,SECTION_ID_TAGS) lifted to module scope inweb/sidebar/tree.js. ThepinExpandedsave-flow callsite now references the constant instead of the literal"workflows"string, so a future section-id rename only needs the constant updated. modalLabel(text)factory inweb/sidebar/modals.jsreplaces the seven repetitions of the four-linecreateElement("label")+className+textContent+appendChildpattern.USERDATA_OVERWRITE_QUERY = "?overwrite=true"named constant inweb/sidebar/workflows_store.js. ComfyUI's userdata API requires this flag to allow POST over an existing file; pinning it as a constant makes the contract visible at a glance.buildFolderno longer round-trips folder-expansion state throughwrapper.dataset.expanded. The DOM dataset forces every value to a string (and led to the slightly awkward!== "false"read); expansion state now lives in a closure variable.pathStatesis the canonical store for cross-render persistence — the dataset was just mirroring it for reads.subcategoryForfallback path now logs aconsole.warnwith the offending category andcategoryRoot. Reachable only via a logic error (typically a staleREPOSentry whosecategoryRootno longer matches the upstream node category prefix); previously it silently rewrote the category, masking the misconfig.- Save-modal
onSavecallback contract renameddir→dirPathto match the codebase-wide convention (dirPathfor arrays of segments,dirNamefor single segments,dirfor resolved DirNode objects). Caller updates intree.jspaired. makeToolbarButton({iconClass, title, onClick})factory inweb/sidebar/tree.jsreplaces the four near-identical button-construction blocks inrenderPanel(export, new-dir, save-canvas, save-selection). Each block was 5–7 lines ofcreateElement/className/innerHTML/title/addEventListener; calls collapse to 4-line factory invocations. No behavior change. Closes the "extractmakeToolbarButton" item on #47.makeToolbarButton({iconClass, title, onClick})factory inweb/sidebar/tree.jsreplaces the four near-identical button-construction blocks inrenderPanel(export, new-dir, save-canvas, save-selection). Each block was 5–7 lines ofcreateElement/className/innerHTML/title/addEventListener; calls collapse to 4-line factory invocations. No behavior change. Closes the "extractmakeToolbarButton" item on #47.- Dropped duplicate
.koolook-export-btnCSS class. It was identical to.koolook-icon-btnminus the:disabledstate. The export button now uses only.koolook-icon-btnviamakeToolbarButton. Closes the "drop duplicate CSS class" item on #47. buildFolder({path})default removed.pathis now a required parameter; the previouspath = nulldefault was unreachable since every caller routes throughmakeSectionCtx, which always supplies a non-empty section-prefixed string. The runtimeif (path && …)guard insidebuildFolderis also dropped. Closes the "remove unreachable default" item on #47.workflowsCachemutator invariants documented inline above the public mutator block inweb/sidebar/workflows_store.js. Four rules a future contributor needs before adding a new mutator: pair every mutate with a commit (or usepersistMutation), returnfalsefor no-op vs. truthy for success, mutate in place, and never replaceworkflowsCacheoutside the seed/load paths. Closes the "documentworkflowsCacheinvariants" item on #47.
Added
- Right-click "Duplicate…" on any workflow row in the sidebar tree.
Opens a name modal pre-filled with
<name> (copy); saving deep-clones the source graph into a new entry in the same directory. The duplicate inherits the source's tags so the user's categorization carries over. Same-name duplicates fall through to the existing archive-on-collision behavior insaveWorkflowEntry. (Closes #58.) - Right-click "Tags…" on any workflow row. Opens a chip-style modal
to view, add, and remove the workflow's tags one at a time. Each
edit fires its own
persistMutationso changes survive a mid-edit close. (Part of #56.) - Tags sidebar section. A new section between Workflows lists every tag in use across the active workflow tree. Each tag becomes a folder whose entries are the tagged workflows (sorted A→Z); click loads the workflow from its real directory. Archived workflows are filtered out of the section so the active view stays clean — their tags are still preserved on the entry, so a restore from the Archive folder brings them back. Search matches tag name OR workflow name. (Part of #56.)
- Right-click "+ New directory…" / "+ New subdirectory under …" in the workflow row's Move-to flow. Both create a fresh directory and move the workflow into it as one atomic mutation; if the move can't land, the new directory is rolled back so the cache never leaks an empty orphan. (Closes #57.)
- Recursive subdirectories under workflow directories. Right-click any
directory in the Workflows tree → "Create subdirectory…" to nest folders
to arbitrary depth (e.g.
UP-scale / Type-A / Sharp). Each nested directory behaves like a top-level one: it can hold workflows + an Archive subfolder + further subdirectories. The save modal directory picker is cascading (multi-step): pick a parent, then a child appears, then a grandchild, etc. — each child level has a(save in "<path>")option to stop drilling at that depth. The right-click workflow "Move to…" submenu lists every other path in the tree. - Delete archive (N) on the synthetic Archive folder. Right-click the Archive folder under any directory → bulk-removes every archived workflow at that level in one confirm. Active workflows in the same directory are untouched.
- Drag-and-drop in the workflows tree (Tier 1 — moves only, no reordering). Drag a workflow onto a directory to move it. Drag a workflow onto an Archive folder to archive it (cross-directory drops move + archive in one go). Drag a directory onto another directory to nest it as a child. Cycle prevention rejects dropping a dir into itself or any of its descendants. Sort within a level stays alphabetical. (Custom ordering would be Tier 2.)
- Schema is now recursive — every directory node has a
workflowsobject AND adirectoriesobject. Existing v0.2 stores load fine:normalizeWorkflowsStoretreats a missingdirectoriesas{}and the rest of the code assumes it always exists post-normalization.
Changed
- Sidebar tab renamed: "Curated Nodes" → "Kforge Labs". Tooltip
also updated. The tab id (
koolook.curatedNodes) and theapp.registerExtensionname are unchanged so existing per-user tab state (pinning, ordering) is preserved. - Save modal —
Base on existingcandidates now walk leaf-up to root and dedupe (deepest wins). Saving into an empty subdirectory likeUP-scale / seedvr2no longer disables the existing-name actions — the modal pulls active workflows from every ancestor. Ancestor entries are labeled<name> · in <path>so the candidate's source directory is unambiguous. The selected destination path is whatever the cascade resolves to — the ancestor source only seeds the workflow name; archive semantics still apply at the destination. - Save modal —
Actiondropdown is now hidden (not just disabled) when no base candidate exists anywhere in the destination's ancestry. Disabled<option>elements render too subtly across browsers; the only useful path in that case is "type a fresh name and save," which the Workflow Name field below already provides. The underlying value is pinned tonewso submit takes the by-name path. - The right-click canvas-node menu item is now "Add to Kforge Labs" (was "Add to Curated Sidebar").
- Directory header counts in the workflows tree now show the total workflows in the whole subtree (active + archived + descendants), not just direct children. A parent with empty direct workflows but populated subdirectories no longer shows "0".
Internal
- Workflow operations are now path-addressed: every mutator and lookup
takes a
string[]path (e.g.["UP-scale", "Type-A"]). The store's internal API:addDirectory(parentPath, name),renameDirectory(parentPath, old, new),deleteDirectory(parentPath, name),saveWorkflowEntry(path, wfName, graph),archive/unarchive/rename/deleteWorkflow(path, wfName),moveWorkflow(srcPath, wfName, dstPath), plus new helperslistAllDirectoryPaths()andpathsEqual(a, b). - Reserved-name check: subdirectory names cannot be
Archive(case-insensitive) at any non-root level — collides with the synthetic Archive folder rendered for archived workflows. - Workflow entries gain optional
tags: string[].normalizeDirNodetrims, drops empties, and dedupes case-sensitively, so old entries without a tags field load astags: []and the rest of the code can assume the field always exists. New mutators inworkflows_store.js:getWorkflowTags(path, wfName),addTag(path, wfName, tag),removeTag(path, wfName, tag).getWorkflowGraph(path, wfName)is now also exported so the duplicate flow can deep-clone without reaching into the cache directly. showTagsModalinmodals.js— chip-row UI with add/remove callbacks, mirrors the existingshowInputModal/showConfirmModalsurface so the modal shell, escape-key teardown, and overlay-click dismissal stay centralized.
Documentation
- Closed out issue #28 (de-vendor upstream code under
upscaler_FIX/andnuke_CAM_exporter/) by adding the audit-trail layer that the v0.1.4 / v0.1.5 registry-hygiene cleanup deferred:forks/THIRD_PARTY.mdgained a "De-vendored upstream code" section listing all six untracked trees with former path, upstream (where pinned) and per-tree provenance notes. Preamble now also documents the publish-history finding that none of these files were ever in a successful Comfy Registry publish.forks/forks_manifest.yamlgained six entries with the_devendoredsuffix andstatus: "removed", populatingsource_repo/source_ref/local_paths/removed_in_release/licenseper issue #28's acceptance criterion (2). Best-effort where upstream URLs were not pinned at vendor time. Closes #28.
[0.2.0] - 2026-05-04
Removed (BREAKING for any saved workflow using Easy_Version)
- Dropped
Easy_Version(and thek_easy_version.pysource) — the whole node was just a one-liner that turned an integer N into the stringvNNN(zero-padded). Maintainer concluded it was the first trivial node they ever wrote and saw no real value. Anyone who needs this exact behavior can either:- inline the format string in their workflow downstream of an INT
primitive (
f"v{N:03d}"), or - use any of the more general string-format nodes in the ecosystem (KJ Nodes, ComfyUI-Custom-Scripts, etc.).
- inline the format string in their workflow downstream of an INT
primitive (
- Saved workflows that reference the
Easy_VersionID will fail to load — same migration as v0.1.5's__koolook_v1_0_1cleanup.
Fixed (carried over from PR #39, [Unreleased] in 0.1.8)
-
Easy_hdr_VAE_encode(Koolook v2.3.3) now wraps the encoded tensor in the standard ComfyUILATENTdict ({"samples": t}) instead of returning the raw tensor. Wiring this node into KSampler previously crashed withIndexError: too many indices for tensor of dimension 5on Wan 2.2 video workflows, because KSampler doeslatent["samples"]and the raw 5-D tensor doesn't support string indexing. The decoder side was already correct. -
Easy_hdr_VAE_encode/Easy_hdr_VAE_decode(Koolook v2.3.3) now produce proper N-frame sequences end-to-end on Wan 2.2 / Hunyuan / CogVideoX / LTX video workflows. Two combined fixes:- Rank-aware dispatch. 5-D
(B, F, H, W, C)video tensors paired with a 2-D image VAE are iterated frame-by-frame and stacked along a temporal axis on encode (and per-frame decoded then concatenated to a(B*F, H, W, C)ComfyUI IMAGE batch on decode). 5-D tensors with a 3-D-native VAE — identified viavae.latent_dim == 3, which is the actual attribute ComfyUI sets on its VAE class for video VAEs — pass through unchanged. - 5-D output normalization on decode. 3-D-aware VAEs return a
5-D
(B, F, H, W, C)tensor fromvae.decode(). Without an explicit reshape, downstream IMAGE-typed nodes (Get Image Size, SaveImage) misread that as a single 4-D image withcount=1andheight=F— exactly the symptom we were debugging (1056×41 count=1after a 41-frame Wan 2.2 sequence). The fix mirrors the stock ComfyUI VAEDecode node (nodes.py:303-304):if img.ndim == 5: img = img.reshape(-1, H, W, C)— the leading(B, F, …)dims collapse into the standard ComfyUI batch axis, so SaveImage now writes_00001.png … _0004N.pnginstead of one squashed frame.
The new
path=image | video-iter | video-3dfield indebug_infosurfaces which dispatch branch fired per run, for live verification. - Rank-aware dispatch. 5-D
Added (Easy_hdr_VAE_encode / decode feature parity with upstream)
- Restored the cinema-grade color-management surface from upstream
Radiance v2.3.3's
RadianceVAE4KEncode/Decode, while keeping the slim no-tile-engine code path that lets these nodes work with Wan 2.2 video VAEs (the original motivation for the Koolook fork — upstream's 4K cosine-blend tile engine errors with"size of tensor a (192) must match the size of tensor b (132) at non-singleton dimension 4"on video VAEs because the tiler's spatial alignment fights the video VAE's internal temporal-aware encoding). - 12 source / target color spaces (was 4): Linear, ACEScg,
ACES 2065-1, Rec.2020 Linear, sRGB, Raw, plus six cinema log
curves — ARRI LogC3 (EI-aware, default 800), ARRI LogC4,
Sony S-Log3, Panasonic V-Log, DaVinci Intermediate,
RED Log3G10. Round-trip math (linearize → encode) and matrix
constants ported verbatim from upstream's
color_utils.pywith the bug-fix history preserved in the docstrings. Conversions are rank-agnostic and 4-channel-aware (alpha passes through untouched). Compress (Log)HDR mode — the upstream HDR-clamp-free pipeline. On encode, the linear-space tensor is re-encoded through a cinema log curve (matchessource_spaceif it's a log space, else ARRI LogC4) and goes into the VAE without a hard clamp. On decode, the VAE output is run through_soft_log_shoulder(tanh rolloff at the per-profile knee instead of a hard clamp at 1.0) and_denoise_log_highlights(3×3 box blur in log space, ramped quadratically toward the highlight region) before log→linear conversion. Per-profile parameters (LOG_PROFILE_HDR_PARAMStable) tune knee / ceiling / denoise threshold / strength to each curve's slope at code 1.0 — RED Log3G10 gets the most conservative settings, Sony S-Log3 / ARRI LogC4 the loosest. Pairhdr_mode="Compress (Log)"on both encode and decode for HDR-clean roundtrips; mismatched modes produce garbage by design.latent_samplingparameter (sample/mean/mode).sampleis ComfyUI's default random posterior sample.meanandmodeuse the posterior mean for deterministic, lowest-noise encoding — best for img2img where minimum reconstruction noise matters. The mean/mode path replicates ComfyUI's preprocessing (BHWC → BCHW → [-1, 1] scaling) before reachingfirst_stage_model, with a graceful fallback chain that never crashes.- Real alpha output.
Easy_hdr_VAE_encodenow returns(LATENT samples, STRING debug_info, IMAGE alpha). Withalpha_handling="Preserve"and a 4-channel input, the alpha channel is surfaced as a separate IMAGE for downstream re-compositing post-decode (VAEs don't encode alpha, so it's routed around them). Withalpha_handling="Ignore"or a 3-channel input, alpha is a zeros tensor of compatible shape — downstream wiring never fails. The previousalpha_handlingflag was a documented no-op. - Exposure now applied in linear space for all source spaces. The
previous behavior multiplied raw input bytes by
2^exposureeven when the input was sRGB-gamma or log-coded, which produced visually wrong results for non-linear sources. Linear sources are unaffected (linear × 2^stops is the correct semantic). Raw source still does a raw-domain multiplication so users who know what they're feeding the VAE keep their bytes intact. - New helper module
forks/radiance_koolook/versions/v2_3_3/color_helpers.pycontains all log curves, soft-shoulder / log-denoise helpers,encode_with_sampling_mode, color-space matrices, and dispatch tables.nodes_vae.pykeeps the node wiring + sequence dispatch.
Intentionally NOT ported (upstream-only features)
- 4K cosine-blend tile engine and its
tile_size/overlap/processing_modeknobs — this is the broken plumbing that motivated the slim fork in the first place. inverse_tonemap/target_stops(SDR→HDR expansion)..rhdrsidecar export andrhdr_precision(Radiance Viewer-specific).crop_padding(only relevant when tiling).
These can be added back in future patches if needed; the slim wrapper
stays at ~600 lines of node wiring + ~450 lines of color helpers,
~40% the size of upstream's vae.py (2,638 lines).
Added (carried over from PR #40, also unreleased on main)
scripts/sync_to_dev.py— pure-stdlib helper that copies the curated runtime files (__init__.py,config.json, top-levelk_*.py,forks/,web/) into a live ComfyUIcustom_nodes/<pack>/folder for fast local iteration without bumping a version. Reads the target path from the newKOLOOK_COMFYUI_DEV_PATHenv var (auto-loads.envfrom repo root); errors out cleanly when unset..env.example—KOLOOK_COMFYUI_DEV_PATH=entry with comment.CLAUDE.md— new "dev-sync" trigger-phrase section so the agent knows to runscripts/sync_to_dev.pywhen the maintainer says "dev-sync" / "sync dev" / "copy those files" mid-session.
Net effect
- Node count drops from 8 to 7.
Koolook/Pipelinesubfolder now has only 1 entry (Easy AI Pipeline) instead of 2.- Faster local dev loop: a one-line tweak no longer requires cutting a full release just to test it inside ComfyUI.
[0.1.8] - 2026-05-03
Changed (categories — affects ComfyUI node-add menu)
Koolook/VFXis gone. Three more granular subfolders replace it:Koolook/Pipeline—Easy_Version,EasyAIPipeline(workflow setup nodes)Koolook/Image—EasyResize_Koolook,easy_ImageBatch(joinsEasyResize_Koolook;easy_ImageBatchwas previously in VFX)Koolook/VAE—Easy_hdr_VAE_encode,Easy_hdr_VAE_decode(now have their own subfolder; previously also in VFX)
- Workflows continue to load and run unchanged —
CATEGORYis purely a UI-organization hint and only affects the node-add menu hierarchy.
Fixed (search discoverability)
Easy_hdr_VAE_encode/Easy_hdr_VAE_decodedisplay names are now "Easy HDR VAE Encode (Koolook)" / "Easy HDR VAE Decode (Koolook)". Previously the display name equalled the node ID (Easy_hdr_VAE_encodewith no "Koolook" string), so typingkoolookin ComfyUI's node-add search filter excluded them — they appeared registered but invisible. Now they group with the rest of the pack under that filter.- Class IDs (
Easy_hdr_VAE_encode,Easy_hdr_VAE_decode) are unchanged — saved workflows that reference these IDs continue to load.
[0.1.7] - 2026-05-03
Changed
- All node display names now suffix
(Koolook)so they surface together when users search "koolook" in the ComfyUI node-add menu. Previously onlyKoolookLoadCameraPosesAbsolute(and the v2.3.3 fork nodes via their version suffix) matched.Easy_Version→Easy Version (Koolook)EasyAIPipeline→Easy AI Pipeline (Koolook)easy_ImageBatch→Easy Image Batch (Koolook)EasyWan22Prompt→Wan 2.2 Easy Prompt (Koolook)Easy_Versioncategory moved fromVFX/UtilstoKoolook/VFXto match the rest of the pack.
- Class keys (
NODE_CLASS_MAPPINGS) are unchanged — saved workflows keep loading and running unchanged. - Carried in via PR #36, then bundled into this release alongside the documentation reorg below.
Docs reorg (root cleanup)
- Moved out of repo root (preserving git history via
git mv):Glossary.md→docs/reference/glossary.mdRELEASING.md→docs/maintainers/releasing.md
- Root now keeps only the four files that must live there:
README.md(GitHub repo home + Comfy Registry description),LICENSE(pyproject.tomlreference + GitHub license badge),CHANGELOG.md(tooling convention),CLAUDE.md(Claude Code agent instructions). - New
docs/structure with three audience buckets:docs/user_guide/— end-user, per-node guides + screenshots (img/)docs/reference/— lookup material (glossary, node inventory)docs/maintainers/— project-internal procedures
- Each bucket gets a short index
README.mdso the structure is navigable fromdocs/README.mddownwards without grep.
Added (new maintainer docs)
docs/maintainers/registry-api.md— documents the undocumented Comfy Registry version-management API that we reverse-engineered while cleaning up v0.1.0–0.1.5 (deprecate / undeprecate / yank endpoints, status enum values, auto-deprecation behavior, ready-to-paste curl recipes, and an optionalregistry-mgmt.ymlGitHub Actions workflow for codified version management without ever exposing the token to a shell).docs/maintainers/node-versioning.md— codifies the rules from the v0.1.5 PR-review discussion for safely changing a node'sINPUT_TYPES/RETURN_TYPES/ class names without breaking saved user workflows. Five rules + the suffix-version pattern- the alias-then-deprecate migration path + concrete worked examples.
- Cross-references in
README.md,CLAUDE.md,.github/ISSUE_TEMPLATE/release_checklist.md, and theadd-external-forkskill all updated to point at the new paths. - Imported the existing untracked
docs/images that the maintainer had already started adding locally (node inventory screenshot underreference/, Easy Image Batch helper image underuser_guide/img/).
[0.1.6] - 2026-05-03
Renamed (with back-compat alias)
EasyResizeis now exposed canonically asEasyResize_Koolook(display:Easy Resize (Koolook)) to resolve a node-ID collision withComfyUI-EasyFilePaths, which also registers the bare nameEasyResize. The oldEasyResizeID is kept as a deprecated alias pointing at the same class, so saved workflows still load and run unchanged. The alias will be removed in a future major release once the deprecation has had time to propagate.
Attribution
- Added a proper SPDX header + GPL-3.0 attribution block to
k_easy_resize.py, creditingkijai/ComfyUI-KJNodes(Resize Image V2) as the original interface inspiration. The Koolook implementation is a fresh write that materially extended the surface (aspect-ratio parser, keep_proportion modes, mask + composed outputs, device selection, target/original W/H reporting, color-panel passthrough). KJ Nodes is GPL-3.0, same as our pack — no relicense required. forks/THIRD_PARTY.mdandforks/forks_manifest.yamlupgraded the KJ Nodes entry fromlicense: "unknown"/sync_state: "needs-upstream-reference"to verified GPL-3.0 with full per-feature change notes.
Notes for users
- If a saved workflow uses the bare
EasyResizeID, it still works but the node's display name now readsEasy Resize (deprecated, use 'Easy Resize (Koolook)')as a hint to swap. New workflows should pickEasyResize_Koolookfrom the node-add menu.
[0.1.5] - 2026-05-03
Removed (BREAKING for anyone using the v1_0_1 namespaced IDs)
- Dropped the entire
forks/radiance_koolook/versions/v1_0_1/folder (~5,200 lines, 26 namespaced nodes). The wrappers were vestigial — Koolook authors never used them, no internal workflow referenced any__koolook_v1_0_1suffixed ID, and the VAE pair was already superseded byEasy_hdr_VAE_encode/Easy_hdr_VAE_decodein v2_3_3. - IDs that no longer exist after this release:
ImageToFloat32__koolook_v1_0_1,Float32ColorCorrect__koolook_v1_0_1,HDRExpandDynamicRange__koolook_v1_0_1,HDRToneMap__koolook_v1_0_1,ColorSpaceConvert__koolook_v1_0_1,SaveImageEXR__koolook_v1_0_1,LoadImageEXR__koolook_v1_0_1,LoadImageEXRSequence__koolook_v1_0_1,SaveImage16bit__koolook_v1_0_1,HDRHistogram__koolook_v1_0_1,LogCurveEncode__koolook_v1_0_1,LogCurveDecode__koolook_v1_0_1,HDRExposureBlend__koolook_v1_0_1,HDRShadowHighlightRecovery__koolook_v1_0_1,OCIOColorTransform__koolook_v1_0_1,OCIOListColorspaces__koolook_v1_0_1,GPUTensorOps__koolook_v1_0_1,HDR360Generate__koolook_v1_0_1,SaveHDRI__koolook_v1_0_1,ACES2OutputTransform__koolook_v1_0_1,DaVinciWideGamut__koolook_v1_0_1,ARRIWideGamut4__koolook_v1_0_1,RadianceVAEEncode__koolook_v1_0_1,RadianceVAEDecode__koolook_v1_0_1,k_easy_OCIO_v101(the short ID forRadianceOCIOColorTransformV2),RadianceLogCurveDecode__koolook_v1_0_1. - Migration path: install upstream Radiance directly
(https://github.com/fxtdstudios/radiance) for the HDR/EXR/OCIO
functionality. Use
Easy_hdr_VAE_encode/decode(already in v0.1.3+) for video VAE workflows. - Source recoverable via
git checkout HEAD~1 -- forks/radiance_koolook/versions/v1_0_1/if you ever realize you need any of those wrappers back.
Added (registry hygiene from the original v0.1.4 plan)
.gitignorenow excludesupscaler_FIX/andnuke_CAM_exporter/— the maintainer's local dev workspaces accidentally committed in Dec 2025. These were never imported by the package's root__init__.py, so they had no runtime effect, but the Comfy Registry's static scanner picked upNODE_CLASS_MAPPINGSfrom vendored 3rd-party clones inside them and counted ~12 spurious nodes against this pack in ComfyUI-Manager (yielding the misleading "44 nodes / 13 conflicts" badge).
Removed (registry hygiene)
git rm -r --cached upscaler_FIX nuke_CAM_exporter— 70 files untracked from git, files stay on the maintainer's local disk for reference. ~3.7 MB of unrelated content out of the published archive.
Net effect on Manager / registry
- Node count drops from 44 to 8 (6 root Koolook + 2 v2_3_3 VAE).
- Spurious "Conflict with
ComfyUI-SuperUltimateVaceTools" warnings disappear. - Published archive shrinks by ~9 MB total (3.7 MB dev workspaces + ~5.2 MB v1_0_1 fork code).
Notes for the maintainer
- After merging this PR and
git pull-ing main, your local working tree will lose theupscaler_FIX/andnuke_CAM_exporter/folders (git applies the deletion). Back up first or restore viagit checkout HEAD~1 -- .... Theupscaler_FIX/folder has already been physically moved to../ComfyUI-Forks-BK/; the_Utils-CAM-track/subfolder ofnuke_CAM_exporter/has been moved to../ComfyUI-Tools-BK/nuke_CAM_exporter/. The remainder ofnuke_CAM_exporter/(your actual Nuke pipeline work) is still on disk in MAIN but no longer tracked by git.
[0.1.4] - 2026-05-03 (test-published only, superseded by 0.1.5)
Removed (registry hygiene)
- Untracked the maintainer's local dev workspaces from git:
upscaler_FIX/andnuke_CAM_exporter/. These were never imported by the package's root__init__.py, so they had no effect on what ComfyUI loaded at runtime, but the Comfy Registry's static scanner picked up theNODE_CLASS_MAPPINGSdicts inside vendored 3rd-party clones and counted them as part of this pack — yielding a misleading "44 nodes / 13 conflicts" badge in ComfyUI-Manager (againstComfyUI-SuperUltimateVaceTools,ComfyUI-multigpu, etc.). Files removed from index viagit rm -r --cached(still on the maintainer's local disk) and the two paths are now.gitignored so they cannot leak again. - Net effect: published archive shrinks by ~3.7 MB (70 files), Manager's node count drops from 44 to ~32 (the actual runtime registrations: 6 root Koolook + ~26 namespaced fork variants), and the spurious conflict warnings disappear. Same class of issue as the v0.1.2 GPL-3.0 relicense — vendored 3rd-party code in MAIN, this time without the license-compatibility risk because none of it was ever imported.
Notes for the maintainer
- After merging this PR and
git pull-ing main, your local working tree will lose theupscaler_FIX/andnuke_CAM_exporter/folders (git applies the deletion). If you want to keep working on those locally, back them up first (cp -r upscaler_FIX nuke_CAM_exporter ~/backup/) or restore from history afterwards (git checkout HEAD~1 -- upscaler_FIX nuke_CAM_exporter); they will then live as untracked files in your working tree, ignored by the new.gitignorerules.
[0.1.3] - 2026-05-03
Renamed
- The new v2_3_3 VAE nodes are now exposed as
Easy_hdr_VAE_encode/Easy_hdr_VAE_decode(clean IDs and display names — no__koolook_v2_3_3suffix), to avoid visual collision with upstream Radiance v2.3.3'sRadianceVAEEncode/RadianceVAEDecodealiases in the ComfyUI node-add search. The version is still tracked structurally (file lives inversions/v2_3_3/) and textually (file header + UPSTREAM_PIN.yaml + forks/THIRD_PARTY.md). Other Koolook nodes in the v2_3_3 set (none today, but possible in future) would still use the__koolook_v2_3_3suffix by default — opt out via the newSKIP_VERSION_SUFFIXset inversions/v2_3_3/__init__.py. - 0.1.2 was test-published to the registry with the previous
RadianceVAEEncode__koolook_v2_3_3IDs but never tagged or formally released; bumping to 0.1.3 publishes the renamed version cleanly.
[0.1.2] - 2026-05-03 (test-published only, superseded by 0.1.3)
License (BREAKING)
- Relicensed entire package to GPL-3.0. v0.1.0 and v0.1.1 shipped under
a claimed MIT license while already incorporating GPL-3.0-derived code from
fxtdstudios/radiance under
forks/radiance_koolook/. GPL-3.0 §5(c) requires the entire combined work to be GPL-3.0; relicensing aligns the package with what we actually ship and matches the dominant license posture of the ComfyUI custom-node ecosystem. Downstream users incorporating, linking to, or deriving from ComfyUI-Koolook must now distribute under GPL-3.0 (or compatible). - Added
LICENSEfile at repo root with the full GPL-3.0 text (pyproject.tomlpreviously referenced aLICENSEfile that did not exist on disk). - README "License" section rewritten to declare GPL-3.0 and explain the §5(c) implication.
Added
forks/radiance_koolook/versions/v2_3_3/— slim, video-friendly re-implementation ofRadianceVAEEncode/RadianceVAEDecodeexposed under the namespace suffix__koolook_v2_3_3. Mirrors the interface surface of upstreamRadianceVAE4KEncode/RadianceVAE4KDecodebut skips the 4K cosine-blend tile engine, which conflicts with modern video VAEs (Wan 2.2, Hunyuan, CogVideoX, LTX) that already handle their own temporal/spatial stitching internally. Fixes the"size of tensor a (192) must match the size of tensor b (132) at non-singleton dimension 4"runtime error users hit when chaining upstream's VAE encoder into Wan 2.2 video workflows.forks/THIRD_PARTY.md— full attribution entries for the v1.0.1 baseline (already in the repo) and the new v2.3.3 VAE subset, with per-class change notes..claude/skills/license-pre-check/— blocking license-compatibility audit skill for Claude Code. Run before copying or porting any third-party code; refuses to proceed on incompatible combinations (e.g. GPL upstream into MIT downstream)..claude/skills/add-external-fork/— Claude Code port of the existing Cursor skill, with a mandatory Phase 0 ("run license-pre-check first") and a Phase 3c requirement to add GPL §5(a) modification headers on derived files.RELEASING.md— canonical, step-by-step release procedure (was previously ad-hoc; the gaps it closes are exactly what caused thev0.1.0orphan-tag andCristianPPublisherIdincidents).- README "Release & Stability" section now links
RELEASING.mdand the per-release checklist template. CLAUDE.mdnow referencesRELEASING.mdfor agents.
Changed
forks/forks_manifest.yaml— explicitlicense: "GPL-3.0"andlicense_verified_atfields on the existing radiance entry (wasto_verify_at_source_ref); pre-registered the new v2.3.3 VAE entry with verified license metadata..github/ISSUE_TEMPLATE/release_checklist.mdrewritten to mirrorRELEASING.md, including the registry-publisher validation step.
Notes for users
- If your workflow currently references the bare-name
RadianceVAEEncodeorRadianceVAEDecode(which routes to upstream Radiance v2.3.3'sRadianceVAE4KEncode/Decodevia that package's alias), and you hit the 4D vs 5D tensor mismatch on video workflows, switch to the namespacedRadianceVAEEncode__koolook_v2_3_3/RadianceVAEDecode__koolook_v2_3_3(display name suffix (Koolook v2.3.3)). - The existing
__koolook_v1_0_1namespaced nodes are unchanged and remain available for backward compatibility with saved workflows.
[0.1.1] - 2026-05-03
Fixed
- Re-tag release at current
mainHEAD sogit describe --tagsresolves locally (the originalv0.1.0tag points at an orphaned merge commit that is no longer inmain's ancestry, which caused ComfyUI-Manager to display the installed version as "unknown"). - Correct
[tool.comfy] PublisherIdfrom the placeholderCristianPto the real publisherkforgelabs, which was the actual cause of everyPublish to Comfy registryworkflow failure (registry returned a misleading400 "Failed to validate token"because the declared publisher did not exist).
Changed
- Bumped
pyproject.tomlversionto0.1.1to match the new tag.
Chore
.gitignore: fix syntax and exclude__pycache__/,*.pyo,.DS_Store(carried forward from commit8fc28d1).
[0.1.0] - 2026-04-24
Added
- Fork tracking moved to
forks/with centralized workflow documentation. - Versioned Radiance fork package layout under
forks/radiance_koolook/versions/v1_0_1. CLAUDE.mdandGlossary.mdto keep workflow and naming conventions explicit.
Changed
- Root node loader now imports the versioned Radiance fork entrypoint.
- Release/checklist/template references updated to current fork-based paths.
- Introduced compact node ID override for OCIO transform:
k_easy_OCIO_v101.
Removed
- Legacy
ACES_workflow/radiancetree and duplicated tracking paths underthird_party/. - Deprecated docs/assets/workflow artifacts removed during repository cleanup.