Commit Graph
34 Commits
Author SHA1 Message Date
961a95b9bd feat(ltx-director): reference-image identity — Ghost Mask + schedulable guide attention (#264)
* feat(ltx-director): Ghost Mask reference image (REF / character sheet)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Addresses PR #264 review findings:

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: gitignore root-level generated workflow JSONs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: mark vendored WhatDreamsCost fork files linguist-vendored

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:15:41 +02:00
malkuthro 3e2b541a61 fix(registry): expose nodes to scanners (#251) 2026-06-19 08:39:44 +02:00
malkuthro e165c96fcc fix(ci): enforce dependency audit gate (#247) 2026-06-18 19:40:31 +02:00
malkuthroandClaude Fable 5 c6a155a033 fix(web): generation-agnostic Comfy draft-quota guard (#237)
* fix(web): generation-agnostic Comfy draft-quota guard

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:29:25 +02:00
malkuthro 16c1b42760 Add published setup execution API (#230)
* feat(setups): add published setup execution API

* fix(setups): expose app surface run results

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

* feat(setups): add publish contract nodes

* feat(setups): split publish result contract

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

* fix(setups): validate app surface on read

* fix(setups): harden app contract validation
2026-06-07 22:02:16 +02:00
malkuthro 1cf2fd31a2 feat(setups): add published setup catalog API (#214)
* feat(setups): add published setup catalog API

* fix(setups): report registry storage diagnostics
2026-06-06 16:12:21 +02:00
malkuthro 8c8546c17c Add queue-driven loop status controller (#205)
* Add queue-driven loop status node

* Document ComfyUI server API testing

* Harden loop queue controller

* Guard loop controller HTTP requests
2026-06-03 12:34:15 +02:00
05f94cbd19 feat(scripts): add make_silent_wav.py for no-audio LTX graphs (#196)
Generates a silent WAV (stereo, 44.1 kHz, 16-bit by default) matching the
AUDIO format ComfyUI/LTX-2.3 expect, so a required AUDIO input can be
satisfied via the stock LoadAudio node when no real audio is available —
the same waveform the LTX Director produces internally.

stdlib only (wave module); CLI flags for duration/rate/channels.

Co-authored-by: Cristian Predut <cristian.predut@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:36:18 +02:00
malkuthroandClaude Opus 4.8 447465ecd8 refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198) (#199)
* refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198)

Remove the two experimental nodes layered on the WhatDreamsCost fork — KoolookAudioTranscriptTimeline (k_audio_timeline.py) and KoolookTimelineEditor (k_timeline_editor.py) — plus the transcribe_audio_timeline.py CLI and its test. The audio-transcript to Director-timing job now lives inside the fork via the Director's audio_transcript_json input, so the standalone nodes are redundant. Also drops the orphaned [audio]/faster-whisper extra and updates packaging, dev-sync, web, and docs references.

Harden the node registry against the #183 failure: __init__.py now imports each node group independently and installs the /koolook/presets/* routes regardless of node-import results, so one missing or broken module degrades to 'that node absent' instead of zeroing every node and 404'ing the sidebar disk routes. A new test keeps every root k_*.py listed in dev-sync's RUNTIME_PATHS.

Closes #198.

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

* fix(nodes): ship loader gates via dev-sync-audio; generalize RUNTIME_PATHS guard (#198)

Address the K-Flame review on #199:

MEDIUM — the #198 refactor made koolook_versioning.py a single registration gate, but dev-sync-audio shipped the new __init__.py without it (and the k_*.py-glob guard test couldn't see it). Add koolook_versioning.py AND koolook_install_guard.py (whose absolute-import fallback raises uncaught) to AUDIO_PATHS so a scoped sync can never leave the loader pointing at a module the target lacks.

Generalize the guard test: parse __init__.py's actual load-time import targets (relative imports + _merge_node_group calls) and assert each is in RUNTIME_PATHS, instead of globbing k_*.py which missed the non-k_ gates. Add an AUDIO_PATHS gate-coverage test.

LOW — CHANGELOG now records the [audio]/faster-whisper + transcribe_audio_timeline.py removal as an external-contract break.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:47:25 +02:00
8058e17456 fix(snapshot): paired drift-corruption fix — duplicate-install guard + boot-time mismatch (#161, #162) (#186)
* fix(snapshot): prevent drift corruption from duplicate installs + boot-time mismatch (#161, #162)

Closes #162 and #161 as the paired fix the issues describe — both touch
the same data-loss surface and verify against the same dev-sync rig.

installs at import time, log critical, register nothing in the
alphabetical loser; client-side `window` sentinel as belt-and-suspenders;
align .env.example / dev-iteration-loop / sync_to_dev / README at the
Registry-derived `koolook/` path.

compare against live /userdata + picks, set a drift flag on mismatch;
gate periodic + pre-load autosave routing to `_unsaved_autosave/` while
drifted; promote status pill to "drifted" precedence over "saved" with a
recovery tooltip; auto-clear on the next markStateSaved (Save / Load).

26 new tests (14 install guard, 4 sidebar sentinel, 8 boot drift) —
316 total passing.

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

* fix(snapshot): apply review fixes — canonical fingerprint + early loser exit + drift timestamp

Review-driven follow-ups on PR #186:

1. **Symmetrize file-side fingerprint (MEDIUM).** `detectBootDrift`
   now uses a new `_canonicalStateFingerprint()` that normalizes the
   workflows tree through `normalizeWorkflowsStore` AND deep-sorts keys
   via `sortJsonValue`. Without this, hand-edited / cross-machine /
   pre-v0.3 files would false-positive as drifted on first boot
   (e.g. `archived: "false"` string vs `false` bool, missing
   `directories: {}`, key insertion-order asymmetry from the
   `{...wf, archived, module, tags}` spread). `_computeFingerprint`
   stays unchanged so the localStorage save baseline contract holds
   for existing users. New regression test verifies a shape-only diff
   does NOT trigger drift.

2. **Gate fork imports on `_is_winning_install` (LOW).** Non-winning
   duplicate installs now short-circuit before the heavy torch / PIL /
   diffusers / aiohttp imports, exiting in ~5 LoC instead of paying
   full import cost just to discard the mappings.

3. **Drift timestamp uses `driftDetectedAt`, not stale
   `lastNamedSaveAt` (LOW).** The localStorage save baseline could be
   from a prior session whose live state was already corrupt —
   surfacing it as "Last named save" in the drift tooltip would
   mislead. Replaced with the session-noticed-at timestamp from
   `_bootDriftDiagnostics`.

352 tests passing (316 prior + 26 from PR #186 + 1 new normalize-
symmetry case).

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

* fix(install-guard): fail safe on unreadable / non-UTF-8 siblings (#162)

Re-review follow-up. The import-time duplicate-install scan could crash
the entire plugin load (fail-silent — the alphabetical winner vanished
too, not just the loser) on two unguarded paths:

- detect_duplicate_koolook_installs: the per-entry is_dir()/is_file()
  over custom_nodes/ siblings was unguarded, so a non-traversable
  sibling (restrictive perms, broken mount, a special dir) raised
  PermissionError up into __init__.py. No duplicate even required.
  Now caught per-entry and skipped.
- read_pyproject_version: read_text(encoding="utf-8") raises
  UnicodeDecodeError (a ValueError, NOT OSError) on a non-UTF-8 /
  binary pyproject.toml, escaping the `except OSError`. Now degrades
  to "?" via `except (OSError, ValueError)`.
- __init__.py: wrapped the guard call site as a final backstop — any
  unforeseen guard failure now degrades to register-normally rather
  than aborting the import.

Both paths reproduced before the fix; the first surfaced while running
the suite from a /tmp checkout whose parent held a root-owned dir.

3 regression tests added (non-traversable sibling skipped; non-UTF-8
pyproject -> "?"; report survives a non-UTF-8 sibling). Full suite:
452 passed (449 + 3).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
2026-06-02 18:44:57 +02:00
malkuthroandClaude Opus 4.7 b706a94030 chore(testenv): lock + audit test deps; pre-authorize auto-bootstrap (#182)
Make the Python test-env bootstrap safe to auto-run so contributors and
agents no longer have to approve a fresh-worktree install every session.

* `constraints-test.txt` — pinned resolve of `.[test]` extras + their full
  transitive closure (39 entries); committed lockfile becomes the
  dependency-change review surface.
* `scripts/bootstrap_test_env.{sh,ps1}` — install via the lock when
  present, then gate with `pip-audit` (a known CVE fails the bootstrap).
  New flags: `--force`/`-Force`, `--relock`/`-Relock`,
  `--no-audit`/`-NoAudit`. `--relock` without `--force` now warns
  instead of silently no-op'ing.
* `tests/scripts/test_bootstrap_constraints.py` — guards the lock stays
  in sync with the `[test]` extras declared in pyproject.toml.
* `CLAUDE.md` — documents that the bootstrap is pre-authorized to run
  automatically and supersedes the global `/warmup` MISSING_VENV
  hard-stop; dependency *changes* still land as a reviewed diff.
* `.gitattributes` — normalize `constraints-test.txt` to LF so the
  PowerShell `--relock` path doesn't flip the file to CRLF.

CI still installs its own minimal `pytest`+`aiohttp` subset directly
(not yet on the lock/audit path) — noted in the CHANGELOG; CI hardening
left as a fast-follow.

Verified: `--force` rebuild from the lock → pip-audit clean → 235 tests
pass (231 + 4 new guards). bash `-n` syntax check OK; arg-error path
exits 2; `--relock` without `--force` warns + exits 0.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:26:59 +02:00
malkuthro 3a0f835814 fix(audio-loop): read active director card values (#195) 2026-05-31 21:25:25 +02:00
malkuthro 63f5c1db7b fix(audio-loop): capture card metadata and Director pins (#193)
* fix(audio-loop): capture card metadata and Director pins

* fix(audio-loop): address card metadata review

* fix(audio-loop): remove duplicate card import
2026-05-31 16:44:46 +02:00
malkuthro 201cdf3001 Track audio loop card setup metadata (#191)
* feat(audio): surface director flavor and prompt mode

* Track audio loop card setup metadata

* Fix audio card review issues

* Harden audio card metadata capture
2026-05-31 00:02:45 +02:00
malkuthro 1d9d40ab70 Add audio transcript timeline node (#189)
* Add audio transcript timeline node

* Hook audio transcripts into Koolook Director

* Add modular timeline audio timing workflow

* fix(frontend): guard Comfy workflow draft cache

* fix(audio): compose transcript timeline prompts

* fix(audio): address transcript review findings

* fix(ci): keep transcript tests lightweight

* fix(timeline): restore image previews from saved paths

* fix(timeline): scrub restored preview blobs
2026-05-30 18:31:41 +02:00
malkuthro a5b6494848 fix(audio): remove stale director web sync folder (#188) 2026-05-29 19:53:49 +02:00
malkuthro 031af80f43 feat(forks): upgrade Koolook Director to stable v1.3.9 (#187)
* feat(forks): upgrade Koolook Director to stable v1.3.9

* docs(forks): clarify Director upgrade compatibility
2026-05-29 18:09:03 +02:00
malkuthroandClaude Opus 4.7 26124e08d5 refactor(loop-audio): card reads only multilines + Koolook Director (#177) (#185)
* refactor(loop-audio): card reads only multilines + Koolook Director (#177)

The audio-lipsync card is now bounded to two source families: the five
tracked Text Multiline nodes and the LTX Director (Koolook v1.3.2)
node's own widgets + input wiring. SAMPLER block (BasicScheduler /
KSamplerSelect / RandomNoise / CFGGuider scrapes), FORK STATE block
(_dev_build.json + git status), and INERT warning are gone — they were
either widget scrapes outside the two source families or duplicates of
the Director's own state.

New card layout (top to bottom):

  HEADER         run NNN — {name} + date + workflow filename
  KNOB STATE     relay_overrides (the per-render knob)
  BASE · NOTES   overlay - info verbatim, blank lines preserved
  BASE · LOCKED  epsilon · Audio src · Working folder (path-wrapped)
  BASE · SCENE   Segments (N) parent, indented `1) 0 to 5 seconds`
                 child list, flat Prompt/Audio/Keyframe coverage
  POST-RENDER    overlay - feedback + parsed motion/sync/sharp

Audio src now derived structurally from a 4-state machine
(audio_vae link × use_custom_audio × audioSegments count) that mirrors
ltx_director.py:610-622 — never from substring matches on prompt text.
Per-segment Audio coverage checks audioSegments[] overlap, not the
maintainer's [Audio]: prompt formatting.

Also fixed: upstream-LTXDirector widget index bug (saved workflow keeps
legacy order even after Koolook fork reordered the schema; DIRECTOR_WIDX
matches the serialised order); loop_audio.py's .env discovery now falls
back to the main repo when run from a worktree (same pattern as
make_card.py); notes.md drops the widget-scraped Mechanical interp
section; log.md columns realigned (Audio src + Segments replace Phase 1
/ Phase 2 / Custom audio).

Three new docs alongside the module:

  reading-graph.html        visual flow (dark theme, color-coded bands)
  reading-graph.schema.yaml machine-readable mirror of the same graph
  CHEATSHEET.md             refreshed against the new design

All 231 tests pass.

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

* fix(lint): split semicolon statements + drop unused symbols in make_card_audio

Ruff (project's Lint & Smoke gate) flagged 10 issues in the refactored
card renderer:

- F401 unused imports — `typing.Optional` at the top of the file and
  `loop_audio.REPO_ROOT` in the standalone CLI's import block.
- F841 unused locals — `dur_f` and `dur_s` were left over from the
  dropped "Duration" row; segment time ranges convey the same info now.
  Kept in the state dict for notes.md; not read in the renderer.
- E702 six semicolon-joined statements in the HEADER and POST-RENDER
  sections (`draw.text(...); y += N`). Split into separate lines.

Rendering is unchanged — verified by re-running the standalone CLI
against run-002. All 249 tests still pass; ruff now clean.

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

* fix(loop-audio): resolve PR #185 multi-agent review findings

Anchored by the verified multi-agent review on PR #185 — every fix
traces to a confirmed finding (false-positive rate was 0/11).

HIGH:
- derive_audio_state now distinguishes "(no director)" from "off (no
  VAE)". Pre-fix, is_input_wired(None, ...) returned None, the
  truthiness check collapsed director-missing into VAE-unwired, and
  the card / log silently agreed on the wrong story. Added an early
  return when director_node is None. Also documented that vae_wired
  can be None (input socket absent in older schemas) and that we
  intentionally collapse it with False at the unwired branch — after
  the missing-director case has been split off.

- extract_multilines now iterates needles longest-first. Pre-fix the
  per-node `break` was order-dependent on config insertion: a future
  short substring (e.g. "relay") in the same tracked_multilines list
  would have silently shadowed "relay_overrides". Sorting by length
  descending removes that footgun without breaking the current 5-needle
  layout.

- parse_timeline now coerces per-segment numeric fields (start /
  length / trimStart) to int via int(float(...)) with try/except → 0
  fallback. The upstream Director itself does this defensively at
  ltx_director.py:232-234; without coercion a string-valued field
  silently breaks video_segment_has_audio's `<` comparisons
  (lexical) and crashes the renderer at `start / fps`. Also drops
  non-dict entries from segments / audioSegments arrays so a
  malformed JSON can't slip past json.loads + isinstance checks.

- Duplicated wrap_path + video_segment_has_audio bodies in
  make_card_audio.py are gone. Renderer now imports them at module
  level from loop_audio so the two scripts agree byte-for-byte on
  path-wrapping rules and audio-overlap geometry.

MEDIUM:
- DIRECTOR_WIDX now carries a MIRROR: comment pointing at the YAML
  schema's `widget_order:` block (the cross-link the review asked for
  as the minimum drift-prevention fix).

- duration_frames / duration_seconds removed from the state dict and
  from render_audio_card's docstring. The renderer dropped the
  Duration row earlier in the PR; notes.md reads them straight from
  director_node via director_widget, so keeping them in state was
  documenting a contract the renderer didn't honour. YAML schema now
  splits them out under a notes_only section to match.

- Log-row score formatting now uses an explicit `is not None` check
  (`_score()` helper) instead of `or '?'` — 0 was a legitimate score
  the maintainer could type that pre-fix collapsed to '?' because 0
  is falsy. Matches the card's existing `_s()` helper for parity.

- Card already surfaces the missing-Director state distinctly thanks
  to derive_audio_state returning "(no director)" — the BASE · LOCKED
  Audio src row shows that label loudly when applicable. No
  additional row needed.

LOW:
- Renderer falls back to "1) frames 0-120 (no fps)" when frame_rate
  is None instead of computing "0 to 0 seconds" silently.

Test coverage:
- New tests/scripts/test_loop_audio.py — 41 tests covering all five
  audio_src states, multiline ordering edge cases, parse_timeline
  coercion + malformed inputs, video_segment_has_audio overlap
  boundaries, parse_feedback (with explicit zero-score regression
  test), is_input_wired None vs False semantics, wrap_path
  separator-only wrapping, pick_existing_path filesystem fallback,
  autogen_label slug derivation.

Docs:
- reading-graph.html lists five audio states (was four).
- reading-graph.schema.yaml audio_state_machine adds "(no director)"
  state + director_present input; state_dict moves duration_frames /
  duration_seconds into a notes_only section; invariants block grows
  with the missing-director split, type-coercion guarantee, longest-
  needle-wins guarantee, and zero-score preservation.
- CHEATSHEET.md Audio src table grows from 4 to 5 rows.

Validation:
- 290 tests pass (was 249 — 41 new + 0 regressions).
- ruff clean on scripts/loop_audio.py, scripts/make_card_audio.py,
  tests/scripts/test_loop_audio.py.
- Standalone CLI re-renders run-002 card identically (same workflow,
  same pixels).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:56:02 +02:00
malkuthroandClaude Opus 4.7 4739dd4203 feat: Easy_Utility, Easy_LoadVideo, and preview fixes (#184)
* feat(nodes): add Easy_Utility — INT → padded STRING dispatcher

First mode `int_to_padded_string`: takes an INT widget (with up/down
arrows) and emits a zero-padded STRING (default `001`, no `v` prefix).
The downstream `koolook_versioning.resolve_version_token` prepends `v`
and re-pads, so wiring this into `EasyAIPipeline.version` /
`Easy_VideoCombine.version` yields the standard `vNNN` token at execution.

Designed for extension: the `mode` widget shipped from day one so adding
further conversions (string→int, formatting variants, etc.) doesn't
rewire saved workflows.

Note: the previously-attempted JS preview-resolver refactor that aimed
to fix the "preview shows stale version" bug was reverted in this
worktree before commit — it didn't actually fix the bug (the real cause
is the JS resolver not descending into ComfyUI subgraphs) and it broke
shot_name resolution in the process. The preview-resolver work is NOT
in this commit; only the Python node + tests.

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

* fix(nodes): guard version widget against pysssss .replace crash

PR #180 added a STRING `version` widget to EasyAIPipeline and
Easy_VideoCombine. Three workflow states leave widget.value undefined
or non-string when graphToPrompt iterates widgets:

- Pre-#180 workflows replay an INT (e.g. `1`) into the new STRING slot.
- Widget-to-input conversion voids the value in newer LiteGraph.
- Easy_VideoCombine's dict-based widgets_values omits the new key
  entirely on workflows saved before #180.

With pysssss/ComfyUI-Custom-Scripts installed, presetText.js patches
every STRING TextWidget.serializeValue to run callbacks that do
`value.replace(...)` for `{variable}` substitution. Any of the states
above crashes graphToPrompt and turns Run into a no-op for the whole
workflow ("Cannot read properties of undefined (reading 'replace')").

Fix: trap `widget.value` reads/writes on the version widget via
`Object.defineProperty` so the property is always coerced to a string
regardless of caller. `configurable: true` so downstream code can
still override the descriptor if it needs to.

Tests: 231 passing; both JS files parse OK.

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

* fix(ai-pipeline): resolve subgraph version preview

* feat(video): add path-aware load video node

* fix(ai-pipeline): preview connected text names

* fix(pr184): address review cleanup

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:59:27 +02:00
malkuthroandClaude Opus 4.8 9834c10087 feat(nodes): single STRING version input + strict v001 naming (#180)
* feat(nodes): single STRING version input + strict v001 naming

- koolook_versioning.py: shared version-token helper (bare number -> vNNN,
  else verbatim; disable_versioning is the master off-switch)
- EasyAIPipeline: version is now ONE STRING field (typed or wired from a
  global version node), replacing the INT widget
- Easy_VideoCombine: new STRING 'version' (replaces keep_silent_intermediate)
  + enable_overwrite; strict mode strips VHS's _NNNNN counter ->
  <name>_v001.<ext>; empty keeps the legacy counter
- Fix empty JSON sidecar: record prompt graph as 'workflow' fallback when
  extra_pnginfo is absent (headless/API path)
- forks: pin VHS dependency (1.7.7 / 0edce8e)
- sync_to_dev: add koolook_versioning.py to RUNTIME_PATHS

Tests: 199 passing; preflight_release all green.

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

* feat(video-combine): JSON sidecar is a drag-loadable workflow

Write the litegraph workflow graph at the top level of the metadata JSON
(CreationTime tucked into graph 'extra') so dropping the sidecar onto the
ComfyUI canvas reloads the workflow, instead of a non-loadable wrapper.
Falls back to {CreationTime, prompt} when no graph is available (headless).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:37:30 +02:00
malkuthroandClaude Opus 4.7 9f063345f7 feat(investigations): LTX23 audio-file lip-sync scaffold (#177) (#178)
* feat(investigations): scaffold #177 LTX23 audio-file lip-sync investigation

Reorganize docs/investigations/ into numbered topic folders, each
self-contained with narrative + upstream pin + patches + per-run snapshots.
Add a generic sync helper that mirrors the dev-sync pattern for any
investigation's third-party node modifications.

Structure (new):
  docs/investigations/
    00_LTX23-base-1-step/README.md             (rename of ltx-director-4k-transitions.md)
    01_LTX23-audio-file-lipsync/
      README.md            -- problem, mechanism, patches, reversion, promotion
      upstream.json        -- repo + version + pinned commit + sync target env + files
      patches/             -- source of truth for modified upstream files
        ltx_director.py    -- adds `relay_overrides` multiline JSON input
        prompt_relay.py    -- per-segment sigma per Prompt-Relay paper formula
      runs/
        LOOP.md            -- iteration protocol
        log.md             -- rolling table (empty header for now)

Sync helper:
  scripts/sync_investigation_patches.py <NN>_<topic> [--dry-run]
  - Reads upstream.json for target env var + file list
  - Backs up the target's current state once per day (<file>.bak.<YYYYMMDD>)
  - Worktree-aware: falls back to MAIN's .env via git rev-parse --git-common-dir
  - Prints a two-line report matching scripts/sync_to_dev.py shape
  - Per-investigation trigger phrase via `trigger:` key (sync-audio for #177)

Env:
  .env.example -- documents new KOLOOK_WHATDREAMSCOST_PATH
  .env         -- local path set

Glossary updates (docs/reference/glossary.md):
  + Investigation folder
  + JSON file folder
  + Loop
  + Run
  + Investigation patches
  + sync_investigation_patches.py

Closes the workflow gap from #177: MAIN is now the source of truth for
investigation patches; the live install is a sync destination only. No
manual file copying between iterations — `sync-audio` does it.

* chore(ruff): exclude docs/investigations from lint

The investigation patches under docs/investigations/*/patches/ are
vendored upstream source (WhatDreamsCost-ComfyUI v1.3.2) carried so
sync_investigation_patches.py can mirror them into the live install
and so iterations can be diff'd against the pinned upstream commit.
Linting them would force divergence from upstream and defeat that
purpose. Mirrors the existing forks/ exclude precedent.

Fixes the CI Lint & Smoke failure on PR #178.

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

* refactor: per-task automation modules + WhatDreamsCost Koolook fork

Pivot docs/automations/ from "one folder per model" to "one folder per
automation module" so the same diffusion model can host several
independent tasks side by side. Replace the parallel docs/investigations/
tree with a fork of WhatDreamsCost-ComfyUI under forks/.

Automation modules (new layout)
- docs/automations/LTX-2.3/base-1step/         the existing single-stage
                                                automation, moved from
                                                docs/automations/LTX-2.3/
                                                with its README, findings,
                                                and handoff-checklist
                                                rescoped to the task. The
                                                4k-transitions backstory
                                                that produced the module
                                                moves here as
                                                backstory/4k-transitions.md.
                                                The earlier
                                                docs/investigations/experiments/
                                                card PNGs/HTML land in
                                                example-cards/.
- docs/automations/LTX-2.3/audio-lipsync/      the audio-file lip-sync
                                                module previously living
                                                under docs/investigations/01_…/.
                                                README, handoff, findings
                                                stub, backstory/, and
                                                runs/{LOOP,log}.md, all
                                                referencing the new fork.
- docs/automations/README.md + CHEATSHEET.md   updated to describe the
                                                module-per-task pivot and
                                                the new tree shape.
- docs/automations/CONVENTIONS.md               unchanged (cross-cutting
                                                loop contract still holds).

WhatDreamsCost Koolook fork (new)
- forks/whatdreamscost_koolook/versions/v1_3_2/
    UPSTREAM_PIN.yaml                          pinned to upstream e81223a
                                                (v1.3.2), GPL-3.0
                                                verified 2026-05-28.
    ltx_director.py                            MODIFIED — adds the
                                                relay_overrides multiline
                                                JSON widget on LTXDirector.
                                                Moved from
                                                docs/investigations/01_…/patches/.
    prompt_relay.py                            MODIFIED — per-segment σ per
                                                the Prompt-Relay paper
                                                formula σ = (L − w_eff) /
                                                (2 · √ln(1/ε)). Moved from
                                                docs/investigations/01_…/patches/.
    patches.py                                 UNMODIFIED — vendored
                                                verbatim from upstream
                                                because ltx_director.py
                                                imports detect_model_type
                                                and apply_patches from it
                                                (same precedent as
                                                radiance_koolook's
                                                color_helpers.py).
- Registered as LTXDirector__koolook_v1_3_2 ("LTX Director (Koolook v1.3.2)")
  so it coexists with an installed copy of upstream WhatDreamsCost-ComfyUI.
  Wired into the root __init__.py NODE_CLASS_MAPPINGS merge.
- forks/forks_manifest.yaml + forks/THIRD_PARTY.md updated with the new
  entry (attribution, change log, license verified date, exposed node IDs).

Removed
- docs/investigations/                          entire tree retired; its
                                                contents either moved to
                                                automation modules or to
                                                forks/.
- scripts/sync_investigation_patches.py         no longer needed — fork
                                                code lives in the Koolook
                                                tree and is synced via the
                                                standard dev-sync flow.
- KOLOOK_WHATDREAMSCOST_PATH in .env.example    same reason.
- docs/investigations in pyproject.toml's ruff
  extend-exclude                                added in 96bf8ad to silence
                                                lint on vendored patches;
                                                no longer needed now that
                                                vendored code is under
                                                forks/ (already excluded).

Docs
- docs/reference/glossary.md                    Investigation folder,
                                                Investigation patches,
                                                JSON file folder, and the
                                                sync_investigation_patches.py
                                                entries replaced by
                                                Automation module, Backstory,
                                                Working folder, and Card.
                                                Loop and Run rewritten to
                                                point at the new structure.
- CHANGELOG.md [Unreleased]                     describes the restructure +
                                                the new fork.

Verification
- ruff check . → all checks passed
- tools/preflight_release.py --skip manager-meta → 42 files parsed; 11
  unique node IDs collected including LTXDirector; VAE dispatch branches
  all OK; fixture workflows still reference all registered Koolook IDs.
- pytest → 173 passed, 0 failed.

Workflow JSON heads-up
The active audio-lipsync workflow LTX-23-audio_tests_v01.json continues to
reference the upstream LTXDirector ID. To pick up the new fork's behaviour,
swap that node to "LTX Director (Koolook v1.3.2)" (registered ID
LTXDirector__koolook_v1_3_2) — sockets and wiring are preserved. The
relay_overrides widget appears at the bottom of the new node. Documented
in the audio-lipsync README's "Workflow JSON — Koolook node ID" section.

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

* feat(scripts): add dev-sync-audio — scoped sync for audio-lipsync iteration

`dev-sync-audio` is a scoped wrapper around `dev-sync` for the
[`docs/automations/LTX-2.3/audio-lipsync/`](docs/automations/LTX-2.3/audio-lipsync/)
iteration loop. It copies *only* the paths that module touches — the
`forks/whatdreamscost_koolook/` subtree and the root `__init__.py` —
instead of `rmtree`-ing the whole dest `forks/` and re-copying every
Koolook fork on every render. Same `KOLOOK_COMFYUI_DEV_PATH` target,
same ComfyUI-Manager restart, same two-line chat-report shape.

Changes
- `scripts/sync_to_dev_audio.py` (new). Thin wrapper that reuses
  `sync_to_dev`'s env loader, target validator, restart helper, and
  chat-report builder, supplying its own `AUDIO_PATHS` tuple. Defaults
  the `--scope` tag to `"audio-lipsync fork edit"` so the build
  identifier names this module even when invoked without an explicit
  scope.
- `scripts/sync_to_dev.py` — `sync()` gains an optional `paths` kwarg
  (defaults to the existing `RUNTIME_PATHS`). Backwards-compatible with
  every existing call site.
- `docs/automations/LTX-2.3/audio-lipsync/README.md` and
  `runs/LOOP.md` updated to point at `dev-sync-audio` instead of plain
  `dev-sync` for the iteration step.
- Project `CLAUDE.md` — new "Per-module scoped variants" subsection
  under the existing `dev-sync` section documents the
  `dev-sync-<module>` pattern, the trigger-name → script-name mapping,
  and reiterates that the user-initiated-only rule applies verbatim to
  every `dev-sync-*` variant.

Verification
- ruff check . → all passed
- python scripts/sync_to_dev_audio.py --dry-run -v →
    would copy: __init__.py
    would copy: forks/whatdreamscost_koolook
    e7f3ab4 - beautiful-mahavira-ae6da6
    would sync 2 entries -> <KOLOOK_COMFYUI_DEV_PATH>  (dev-sync-audio)
  (Confirms scope is correct and the existing dev-sync chat-report
  shape is preserved.)

User-initiated only — never automatic. Trigger phrase: `dev-sync-audio`
in chat (also: "copy audio fork", "sync audio fork"). Same rule as
`dev-sync` — see project CLAUDE.md.

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

* docs(audio-lipsync): only snapshot runs when explicitly kept

Every render adds a row to runs/log.md (always). The heavier
run-NNN_<label>/ snapshot folders (workflow JSON copy + relay_overrides
+ patch state + notes) are now created only when the maintainer says
"keep", "save run", "save this", or "snapshot" in chat. Without that
trigger, the render is just a log row.

Why: an active knob sweep can do 10+ renders/hour. Saving a folder
per render bloats runs/ fast and most of those folders are noise the
moment the next render lands. Cheap log row everywhere, expensive
snapshot only for runs worth pinning (knob landed, surprising failure,
something heading for findings.md).

Changes
- docs/automations/LTX-2.3/audio-lipsync/runs/LOOP.md
    Step 5 split into "always log" + "snapshot only on request".
    Trigger-detection table updated. Run-labels section reframed
    (NNN matches log.md row number). Added a one-paragraph "why this
    retention policy" rationale.
- docs/automations/LTX-2.3/audio-lipsync/README.md
    Folder-map flags run-NNN_<label>/ as conditional. Iteration loop
    step 5 names the keep / save-run / save-this triggers explicitly.

Not a code change. No script edits — the retention rule lives in the
agent protocol, not in dev-sync-audio (which stays scoped to code
sync, not run management).

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

* fix(review): harden relay_overrides parser + polish (PR #178 review)

Addresses verified findings from the /review-pr pass on PR #178.

H1 — relay_overrides values are now type-checked
- Allowlist of recognised knobs (`video_strength`, `video_window_scale`,
  `audio_strength`, `audio_window_scale`, `audio_epsilon`) with explicit
  float-coercion. Numeric strings ("10") coerce silently; non-numeric
  values (e.g. "ten") now raise a ValueError naming the offending knob
  and showing a minimal correct example — instead of a TypeError deep
  inside `build_segments` arithmetic, or a SUCCESS render at upstream
  defaults that polluted the A/B iteration log.

H2 — malformed JSON no longer silently falls back to defaults
- `json.JSONDecodeError`, non-dict JSON (arrays, strings, bare numbers,
  null), and non-numeric values for recognised knobs all now raise
  `ValueError` with operator-actionable messages. The only silent
  fallback is the explicit empty/whitespace case — "use upstream
  defaults" is the operator's intent there.
- Typo'd keys (e.g. `viedo_strength`) log a warning naming the unknown
  keys + the recognised set, then drop the typo. Previously they were
  silently retained in the cleaned dict but never queried by
  `build_segments`, indistinguishable from a default render.

Architectural side-effect — shrinks the diff vs vendored upstream
- `_parse_relay_overrides` extracted into a stdlib-only sibling module
  `_relay_overrides.py`. `ltx_director.py` shrinks closer to upstream
  baseline (one import + one alias instead of a 30-line function body)
  and is easier to diff against `e81223a`. The parser is also now
  unit-testable without ComfyUI's `comfy_api` runtime.

L5 — tests added (31 cases, all passing)
- New `tests/forks/__init__.py` + `tests/forks/test_whatdreamscost_v1_3_2_relay_overrides.py`.
- Covers every confirmed failure mode (H1 + H2), the happy path, the
  comment-key convention, the unknown-key warning, and an allowlist-
  consistency assertion that fails loudly if `build_segments` ever
  grows a knob without the parser being updated.
- Test module uses `importlib.util.spec_from_file_location` so it can
  load the parser without walking through the package hierarchy (which
  would trigger the `comfy_api` import inside `ltx_director.py`).

L3 — `_build_line` promoted to public `build_line`
- `scripts/sync_to_dev.py` renames the private helper. The underscore
  alias is kept for one release cycle as a deprecation slot.
- `scripts/sync_to_dev_audio.py` switches to `_dev.build_line()`. No
  behaviour change; chat-report shape is bit-identical to before.

L2 — `source_ref` clarified in pin metadata
- `forks/whatdreamscost_koolook/versions/v1_3_2/UPSTREAM_PIN.yaml` and
  `forks/forks_manifest.yaml` both now spell out that `e81223a` is the
  post-v1.3.2 README-only commit on upstream's `main`, not the v1.3.2
  release tag itself. Code is byte-identical to the tagged release; the
  pin differs only in trailing docs.

L1, L4 are explicitly left alone (L1 is dead upstream cruft, kept for
minimum-diff; L4 is helper duplication tracked for a separate follow-up
issue when a third fork lands).

Verification
- ruff check . → clean
- pytest → 204 passed (173 prior + 31 new parser tests)
- tools/preflight_release.py --skip manager-meta → all PASS (static AST
  extraction still picks up LTXDirector from the fork)
- scripts/sync_to_dev_audio.py --dry-run → two-line chat report still
  emits via the renamed `build_line` helper

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

* feat(audio-lipsync): config-driven loop-audio script + module cheat sheet

End-to-end automation for one render cycle of the audio-lipsync
iteration loop. The chat phrase `loop-audio` invokes a single script
that does the whole chain — no manual artifact-shuffling.

Scripts (cross-cutting infrastructure at the repo root, like make_card
and sync_to_dev; they get lifted into the module folder in the
`docs/automations/` → `automations/` move queued for the follow-up PR)

- scripts/loop_audio.py
    Reads its settings from loop_audio.config.json (alongside),
    auto-discovers the most recent workflow JSON in the ComfyUI user
    workflows dir (derived from KOLOOK_COMFYUI_DEV_PATH or overridden
    via KOLOOK_COMFYUI_WORKFLOWS_DIR), extracts the five tracked Text
    Multilines + LTX Director state + sampler chain, captures
    fork-state (HEAD SHA + last dev-sync-audio sync info + fork-dir
    git status), writes the run-NNN_<auto-label>/ snapshot, appends a
    row to runs/log.md, and chains the card renderer in. Flags
    mirror sync_to_dev: --dry-run, --label, --no-log, --no-card,
    --workflow, --config.

- scripts/loop_audio.config.json
    Single source of truth for the customisable settings (job_name,
    module_path, workflow_pattern, comfyui_workflows_subpath,
    skip_filename_substring, tracked_multilines, fork_to_track,
    render_card). Underscore-prefixed keys are inline documentation
    and ignored by the loader. When the convention shifts — new
    multiline title, new fork path, different filename pattern —
    edit the JSON, not the Python.

- scripts/make_card_audio.py
    Audio-lipsync-specific card renderer. Five sections (KNOB STATE,
    FORK STATE, SAMPLER, BASE notes, OUTCOME) plus an INERT warning
    band when the workflow's Director is upstream `LTXDirector` with
    a non-empty relay_overrides. Same palette + font fallback chain
    as scripts/make_card.py so the base-1step and audio-lipsync card
    families read as a set. PIL/Pillow added to the `test` optional
    deps so CI doesn't hit ImportError if any future test touches
    this script.

Docs

- docs/automations/LTX-2.3/audio-lipsync/CHEATSHEET.md
    Module-scoped operating instructions per the cheat-sheet pattern:
    what the module is for, the chat triggers, the required workflow
    shape (5 multilines + the Koolook Director variant), the render
    cycle steps, what lands in each run-NNN_ folder, findings-
    promotion rule, where every customisable setting lives, and a
    card-section legend.

- docs/reference/glossary.md
    `Card` rewritten to describe the per-module renderer fan-out
    (`make_card.py` for base-1step, `make_card_audio.py` for
    audio-lipsync) — cards no longer live at a single stable
    `_AI/card.png` path. New `Loop config (per-module)` entry
    documenting the `<script>.config.json` convention + the
    required-keys list.

- docs/automations/LTX-2.3/audio-lipsync/runs/log.md
    Header rewritten with column set matching the actual log-row
    output (Director / relay_overrides / Custom audio / Phase 1 /
    Phase 2 / Scores / Notes).

First real run captured

- docs/automations/LTX-2.3/audio-lipsync/runs/run-001_bear_3x-fr_w.audio_upstream_audio-off_vstr10.0/
    Baseline reference render — `Bear_3x-FR_w.Audio`, upstream
    `LTXDirector` still wired into the workflow, use_custom_audio
    off. Motion 4/5 · Sync 5/5 · Sharp 4/5 per maintainer feedback.
    The relay_overrides `{"video_strength": 10.0}` is set but inert
    (Director not yet swapped to the Koolook variant); the card
    surfaces this with the amber INERT band at the bottom. Captures:
    workflow.json (185KB), relay_overrides.txt (with INERT note),
    patch_state.txt (3ffb7b7 / 15d5688 / clean), notes.md (feedback
    + scores + mechanical interp), card.png (61KB).

Verification

- ruff check . → clean
- pytest -q → 204 passed, 0 failed
- tools/preflight_release.py --skip manager-meta → all PASS
- scripts/loop_audio.py --dry-run → identifies the latest workflow,
  emits the two-line chat-report header, doesn't touch disk
- scripts/loop_audio.py (real) → wrote the run-001 folder + card +
  log row in a single invocation

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

* fix(audio-lipsync card): match base-1step visual style 1:1

The first cut of the audio-lipsync card used solid colored header bands
and block section panels — visually heavier than base-1step's renderer.
Refactored to copy make_card.py's primitives directly:

  - small uppercase accent-colored section labels (no header bars)
  - subtle BG_SECTION panel outlines instead of full-width fills
  - left-accented note boxes (4px coloured bar + dark BG + label inside)
    for Base · notes, Feedback (video), and the conditional Director
    note warning
  - same font fallback chain + sizes as make_card.py (F_SECTION 17pt,
    F_MONO 19pt, F_NOTE italic 18pt)
  - oversized canvas + post-render crop matches make_card.py's pattern
    so cards size to actual content
  - palette imported in lockstep (BG_OUTER/CARD/SECTION, BORDER, the
    three accent triples plus the new ACCENT_FORK violet)

Two tuning fixes spotted during re-render:

  - _wrap_text gained a keep_blank_lines flag so prose blocks can
    collapse the empty separator lines maintainer canvases often have
  - max_lines bumped on the prose blocks: Base · notes 18 (was 6,
    truncated the OVERLAY - INFO body), Feedback 6 (was 5), Director
    note 8 (was 4, truncated mid-sentence)
  - make_card_audio.py's standalone CLI now loads .env via
    loop_audio.load_dotenv before rebuilding state, so re-renders show
    the same FORK STATE lines (MAIN sha + sync sha + scope) that the
    live loop_audio run captures

Visual reference: docs/automations/LTX-2.3/base-1step/example-cards/
run-001-card.png is the layout target. The audio-lipsync card now
reads as a sibling of that family — KNOB STATE in amber, FORK STATE in
violet, SAMPLER + Base · notes in sky, FEEDBACK + OUTCOME in green.

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

* feat(fork): ship LTX Director timeline-editor JS to the Koolook variant

The Koolook variant of LTXDirector was loading as a bare schema — no
image strip, no audio strip, no canvas timeline editor — because the
upstream timeline-editor JS bundle never came across with the fork.
The widget upstream paints onto the node lives entirely in
`js/ltx_director.js`; the Python class only declares the inputs.
Compounding it, the JS's binding check is hard-coded
``if (nodeData.name === "LTXDirector")``, so even if the bundle had
been loaded, it would have skipped our suffixed registry ID
(`LTXDirector__koolook_v1_3_2`).

Fix: vendor the JS into the fork and rebind it.

- web/whatdreamscost_koolook_v1_3_2/ltx_director.js
    Verbatim copy of upstream's
    custom_nodes/WhatDreamsCost-ComfyUI/js/ltx_director.js at the
    pinned commit (e81223a). Two string changes vs upstream:
      * `name: "LTXDirector"` -> `"LTXDirector_Koolook_v1_3_2"`
        so the registered ComfyUI extension doesn't collide with the
        upstream extension (both load in parallel).
      * `if (nodeData.name === "LTXDirector")` ->
        `if (nodeData.name === "LTXDirector__koolook_v1_3_2")`
        so the canvas widget mounts on the Koolook variant only;
        the upstream extension keeps owning the upstream node.
    A short comment block at the top documents both deltas.

- scripts/sync_to_dev_audio.py
    AUDIO_PATHS gains `web/whatdreamscost_koolook_v1_3_2` so
    dev-sync-audio carries the JS into the live install alongside the
    fork's Python files + root __init__.py.

After dev-sync-audio + ComfyUI restart, the Koolook variant mounts the
same timeline editor as upstream: image/audio strips, segment dragging,
the visual preview row, the [Audio] tag handling, the
img_compression/divisible_by widgets. The `relay_overrides` field
declared in the Python schema renders at the bottom of the node
alongside everything else.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 01:56:12 +02:00
malkuthroandClaude Opus 4.7 9372032b46 feat(automations): workflow validator + LTX 2.3 stage-2-only docs (#170)
* feat(automations): workflow JSON validator + LTX 2.3 stage-2-only findings

Adds scripts/validate_workflow.py — a stdlib-only CLI that checks a
ComfyUI workflow JSON for internal consistency: link-table declarations
vs node-IO references, slot index bounds and types, endpoint cross-
checks, last_node_id/last_link_id bounds, and duplicate IDs.

Catches the class of bugs that come from hand-editing workflow JSONs
(e.g. when an agent re-wires a graph to build a slim variant from a
Director workflow). Comfy's loader silently drops or mis-wires bad
parts, so the cost of an undetected drift is "graph looks fine but
behaves wrong" — exactly the failure mode this script blocks.

Exit codes: 0 clean, 1 problems on stderr, 2 file missing or bad JSON.

Docs:
- docs/automations/CONVENTIONS.md §11 — when and how to run the
  validator, full list of checks performed.
- docs/automations/LTX-2.3/findings.md — two new sections covering the
  spatial upscaler's architectural facts (ltx-2.3-spatial-upscaler-x2
  is hardcoded x2, the brief enters only via Director > guide_data >
  DirectorGuide, attention scales (W * H * T)^2) plus three open
  hypotheses for a stage-2-only refine path.
- CHANGELOG.md [Unreleased] entry.

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

* fix(scripts): make validate_workflow.py robust against malformed JSON

Addresses PR #170 review (Codex GPT-5.3) — the validator crashed with
AttributeError on malformed-but-valid JSON, defeating its goal as a safe
preflight tool. Two repros from the review:

    {"nodes":[1],"links":[],"last_node_id":0,"last_link_id":0}
    -> AttributeError: 'int' object has no attribute 'get'

    [1,2]
    -> AttributeError: 'list' object has no attribute 'get'

Both confirmed locally. Now every shape assumption is guarded:

- Top-level must be a JSON object; non-object short-circuits with one
  clear problem line.
- 'nodes' and 'links' must be lists; non-list reported and treated as
  empty so subsequent checks still run.
- Each node entry must be a dict; bad entries reported and dropped.
- Each input/output entry must be a dict (extracted into a
  _sanitize_slots helper, cached per node so duplicates aren't reported
  when the same node is touched by multiple links).
- Each link entry must be a 6-element list; malformed link entries
  reported and skipped without crashing later phases.
- node_by_id only keys on int node IDs (non-int IDs can't match a
  link's int src/dst, so we don't mask bad data by accepting them).

Adds tests/scripts/test_validate_workflow.py — 24 tests covering both
review repros, every new guard, and a sampling of the existing
consistency checks (link-to-missing-node, type mismatch, endpoint
desync, duplicate IDs, last_node_id bound, CLI exit codes). Full suite:
168 passed in 1.59s.

CHANGELOG entry updated to reflect the defensive behavior + test
coverage.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:52:35 +02:00
malkuthro 96d4b51d7b Release v0.3.5 (#160)
* Release v0.3.5 prep

* fix(sidebar): bypass stale browser cache for snapshot state

* test(sidebar): guard no-store snapshot fetches
2026-05-18 00:54:49 +02:00
malkuthroandClaude Opus 4.7 092c204060 feat(automations): LTX Director iteration-loop framework + 4K investigation docs (#159)
* docs(investigations): kickoff for LTX Director 4K transition breakdown

Captures the maintainer's problem statement (HD multi-keyframe works,
4K single-keyframe works, 4K multi-keyframe degenerates into dissolves
between keyframes) and an initial conclusion about the WhatDreamsCost
LTXDirector node's implementation.

Traces the lineage to the Prompt Relay paper (Chen, Huang, Liu —
arXiv:2604.10030) via Kijai's ComfyUI port, separates the three
mechanisms the node bundles (cross-attention penalty, guide-image
keyframes, audio/image preprocessing), and flags the candidates for
the 4K dissolve: model-side limit at LTX sampling time vs. node-side
length-independent σ in the penalty matrix.

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

* docs(investigations): round 2 — 2K vs 4K + two-stage upscale findings

Adds the maintainer's round-2 observations:
 - 2K renders behave much better than 4K (segments morph instead of
   dissolving) — model is closer to its training distribution.
 - The two-stage workflow's upscale stage (stage 2) is overwriting
   stage 1's audio/animation: lip sync degrades and segment routing
   weakens.

Captures the hypothesis space:
 - Why 2K > 4K: model prior (out of distribution at 4K) is dominant;
   token-grid scaling with a length-independent sigma is secondary.
 - Why stage 2 overwrites stage 1: KSampler denoise re-applies
   conditioning from scratch; if LTX Director isn't wired into
   stage 2 the Prompt Relay + audio_attn2 patches go missing;
   latent shape change between stages forces a penalty rebuild.

Includes a 3-option pros/cons table (4K direct vs 2K direct vs
2-stage) and 6 investigation directions ordered cheapest-first,
starting with auditing the stage-2 wiring.

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

* docs(investigations): round 3 — both stages run LTX Director; new prime suspect

Maintainer's screenshot confirms both stages route through LTX Director,
so round 2's missing-patch hypothesis is ruled out. Audited the stock
LTX nodes used in stage 2 against ComfyUI source:

 - LTXVLatentUpsampler explicitly pop()s the noise_mask and runs a
   learned (EXPERIMENTAL) neural upscaler — not interpolation.
 - LTXVConcatAVLatent fills a missing noise_mask with ones_like
   (denoise fully) for the side that's missing.
 - LTXVCropGuides removes keyframe slots and clears keyframe_idxs.

Traced the audio noise_mask end-to-end through stage 2 and found:
audio waveform stays preserved (noise_mask=0), video keyframes stay
pinned, but every non-keyframe video frame goes into stage 2's
sampler at noise_mask=1 — i.e. fully regenerated at denoise=0.80.
That's where stage 1's audio-aligned mouth shapes live, and that's
what's getting thrown away.

Prime suspect: denoise=0.80 is too high for "preserve stage 1"
semantics — most LTX two-stage reference workflows use 0.30–0.40.
Secondary suspect: the LTXVLatentUpsampler [BETA] is a learned model
that can hallucinate spatially-dense detail (faces, mouths) before
stage 2 even starts denoising.

Includes a 4-step cheapest-first test plan starting with dropping
stage 2 denoise to 0.30.

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

* docs(investigations): round 4 — scheduler-shape finding + tracking plan

Captures two outputs from the planning session so they're preserved
for the next pickup:

1. Promotes the maintainer's empirical finding to a locked-in
   baseline: `linear_quadratic` scheduler at 8 steps produces the
   right sigma curve shape (flat plateau near sigma=1.0 for ~5
   steps, then steep drop in the last 3) for both phases. Any
   other scheduler degrades fast motion and keyframe transitions.

2. Records the agreed tracking-system plan (BASELINE.md +
   log.md + per-run screenshot folder, with three 1-5 score axes
   matching the live trade-off: motion / audio sync / Phase-2
   sharpness). Not yet scaffolded — explicit "next step" callout
   for whoever picks this up.

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

* feat(investigations): /make-card skill + PNG card renderer toolkit

Per-iteration tracking card for the LTX Director 4K investigation.
Solves the documentation-drift problem we hit on v01->v02 (multiline
summary diverged from actual node values) by treating the saved
workflow JSON as the source of truth.

Adds:
 - scripts/make_card.py — reads a ComfyUI workflow JSON, paints a
   self-contained 540px PNG card via Pillow. Extracts Phase 1 run
   params (Format/Mult/Scheduler/Steps/Denoise), locked base
   (Sampler/CFG/Seed/epsilon/Audio src), scene (duration/segments
   with prompt/audio/keyframe checkboard), free-text BASE notes
   from `OVERLAY - INFO` Text Multiline, FEEDBACK from `OVERLAY -
   FEEDBACK` Text Multiline, and a POST-RENDER block that
   auto-computes render duration from JSON->newest-video mtime
   delta in the same folder. Layout mirrors the project's
   snapshot-dialogs design language (panelled sections, sky/amber/
   green accents matching docs/designs/snapshot-dialogs.html).
 - scripts/watch_cards.py — background poller. Watches a working
   folder; auto-renders <stem>_card.png whenever a workflow JSON
   appears or is updated. Skips up-to-date PNGs.
 - .claude/skills/make-card/SKILL.md — /make-card slash command;
   globs the newest JSON in the working folder, calls make_card.py,
   shows the PNG inline.
 - .claude/skills/make-card/work-folder.txt — one-line config of
   the working folder path. Edit to switch projects.
 - docs/investigations/experiments/{baseline,combined-card,run-001-card}.html
   — design prototypes that fed into the PNG layout; kept as
   history.
 - docs/investigations/experiments/run-001-card.png and
   run-v04-card.png — sample card outputs from v03 and v04 of the
   workflow JSON, included for reference.

Issue #151 tracks the eventual native ComfyUI custom node so this
flow happens inside the graph; for now the external script + watcher
is the loop.

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

* chore(launch): use absolute Windows python path for design-server

The default `python3` runtimeExecutable fails on this Windows host
(triggers the Microsoft Store install stub). Hard-coding
C:\Python313\python.exe lets the preview MCP and the design-server
target work on the maintainer's machine. Worktree-scoped change —
main keeps the cross-platform `python3` default.

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

* feat(automations): docs/loop scaffold + env-var working folder + _AI subdir

Promotes the LTX Director card-rendering loop from one-off scripts to a
documented, project-portable automation framework.

Adds `docs/automations/` as the home for AI-assisted iteration loops:
 - README.md — the pattern; how to start a loop for a new model
 - CONVENTIONS.md — cross-cutting: working folder, Text Multiline node
   contract (Working_Folder_PATH / OVERLAY - INFO / OVERLAY - FEEDBACK),
   card format, iterations log, skill, watcher, run-id rules, tool
   locations
 - LTX-2.3/README.md — entry point for the LTX 2.3 loop; per-iteration
   steps; full extraction table; active investigation status
 - LTX-2.3/handoff-checklist.md — 5-minute onboarding for a new machine
   or new person
 - LTX-2.3/findings.md — promoted findings (linear_quadratic scheduler
   locked-in; resolution behaviour; audio mode; open two-stage suspects)

Replaces the committed `.claude/skills/make-card/work-folder.txt` with
an env-var-driven path. Follows the project's existing `KOLOOK_*`
convention — `.env.example` documents the new
`KOLOOK_AUTOMATIONS_WORK_DIR`. Real path lives in `.env` (gitignored).
No more leaking client-specific absolute paths into committed files.

`scripts/make_card.py`:
 - Loads `.env` automatically; worktree-aware (walks the .git common-dir
   marker to find the main repo when run from a worktree).
 - Output folder priority: Working_Folder_PATH node in JSON → env var →
   JSON's parent dir.
 - Run with no args: auto-discovers newest *.json in the working folder.
 - Writes `_AI/card.png` + `_AI/iterations.md` inside the working folder,
   not the root. Lets the maintainer wipe AI artifacts or user content
   (JSONs/MP4s) independently.

`/make-card` skill simplified to a single command — `python
scripts/make_card.py` — the script handles env loading and discovery
itself.

baseline.html: stripped the project-specific label.

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

* docs(automations): sync all path references to _AI/ + add layout diagram

Audit pass after introducing _AI/ subfolder. Updated every doc and
script that still referenced root-level card.png / iterations.md:

 - docs/automations/CONVENTIONS.md (3 spots)
 - docs/automations/LTX-2.3/README.md (per-iteration steps + composite)
 - docs/automations/LTX-2.3/handoff-checklist.md (4 spots, including
   the off-boarding section which conflated repo commits with working-
   folder artifacts)
 - docs/automations/LTX-2.3/findings.md (promotion criterion)
 - scripts/watch_cards.py (target PNG path now matches make_card.py's
   _AI/card.png — was writing to a stale <stem>_card.png location)

Adds a clear "Where things live" section to docs/automations/README.md
showing both layouts side-by-side:
 - the repo tree (where the tooling, skill, env-example, and per-model
   loop docs sit)
 - the per-project working-folder tree (user content at root, _AI/
   subfolder for managed artifacts)

So anyone picking this up can answer "what's in the repo vs. what's in
my project folder" without reading prose.

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

* docs(automations): add CHEATSHEET.md — one-page dense reference

Compact reminder card for returning users. Structured by application
context: ComfyUI · Scripts · Folder structure · Conventions · Quick-fail
diagnostics. Skippable for first-timers; targets the maintainer who
already knows the loop and just needs the right node names, score
syntax, and command list at a glance.

Linked from docs/automations/README.md.

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

* docs(automations): add Bootstrap section to CHEATSHEET — bare-bones setup

What's needed to get a fresh machine running the loop, separated from
the daily-loop reference. Three checklists:
 - Repo side: Python + Pillow + .env config
 - ComfyUI side: was-node-suite + the three required Text Multiline
   nodes, with exact titles
 - First card: /make-card → expect _AI/card.png

Deliberately skips model-specific setup (checkpoints, LoRAs, per-model
custom nodes) — those live in each model's handoff-checklist.md.

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

* feat(automations): skip *loop* files in auto-discovery + watcher

The maintainer's workflow includes a "loop" stage in ComfyUI that
composites the card next to the video and writes the result back into
the working folder as `<base>_loop_<seq>.{mp4,json}`. Without a skip
rule, auto-discovery treats this composited-card output as the newest
input — the script then re-renders the card from yesterday's metadata.

Now: any JSON or video file with `loop` in its stem (case-insensitive)
is ignored by:
 - scripts/make_card.py — auto-discover newest JSON, newest video
 - scripts/watch_cards.py — polling glob

Explicit invocation still works: `python scripts/make_card.py
<wf_loop.json>` will render a card for that file specifically.

Documents the rule in docs/automations/CONVENTIONS.md (new §7) and adds
a one-line bullet to CHEATSHEET conventions.

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

* style(automations): satisfy Ruff lint — split if-on-one-line, rename l, drop os

CI's Lint & Smoke step flagged 17 violations across the new scripts.
Cleaned up:

 - scripts/make_card.py: 11 × E701 (multi-statement lines)
   split every `if cond: stmt` onto two lines.
 - scripts/make_card.py: 5 × E741 (ambiguous variable `l`)
   renamed to `link` in the two link-dict comprehensions and their
   downstream lookups.
 - scripts/watch_cards.py: 1 × F401 (unused `os` import).

No functional change — `make_card.py` re-renders the working folder's
card identically post-cleanup. Verified `ruff check` returns
"All checks passed!" on both files.

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

* feat(automations): render time from ComfyUI log, not disk timestamps

Disk mtimes don't reliably bracket the model run — the maintainer's
workflow saves the workflow JSON manually after rendering, assembles
QuickTime from EXRs separately, and ComfyUI's metadata bundle writes
JSON + MP4 at the same instant. None of those give us actual render
duration.

Render time now sourced from ComfyUI's own log
(`<comfyui-root>/user/comfyui.log`):

 - Find the latest `saving images: 100%` line → pair it with the next
   `Prompt executed in X` → that's the most recent EXR-producing render
   wall-time.
 - Supports both `Prompt executed in 18.21 seconds` and
   `Prompt executed in HH:MM:SS` log formats.
 - Skips trivial follow-up prompts (e.g. audio-VAE load between render
   and /make-card invocation).

Log path resolution:
 1. `KOLOOK_COMFYUI_LOG` env var (explicit override).
 2. Inferred from existing `KOLOOK_COMFYUI_DEV_PATH` →
    `<comfyui-root>/user/comfyui.log`.

Fallbacks remain in priority order: EXR sequence span (oldest to newest
in the latest version subfolder) → MP4 mtime delta → "pending".

Docs:
 - .env.example: documents the optional KOLOOK_COMFYUI_LOG.
 - CONVENTIONS.md: new §4 "Render duration" describing the resolution
   chain; section numbers renumbered.
 - CHEATSHEET.md: updated the "Render time —" diagnostic.

Lint: ruff clean.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:44:52 +02:00
malkuthroandClaude Opus 4.7 b42bd4af77 chore(python): add bootstrap scripts + pip-audit + line-ending policy (#158)
Solidify the Python test-env path so any agent (Claude / Codex) or
human can bootstrap with one command, and so the global preflight
skill's pip-audit branch actually runs against this repo.

scripts/bootstrap_test_env.{ps1,sh}: one-liner per platform, idempotent
  (no-op if .venv exists, --force / -Force to recreate), upgrades pip,
  installs project + test extras.

pyproject.toml: add pip-audit>=2.7 to [project.optional-dependencies]
  test so /preflight's Python audit branch lights up.

CLAUDE.md, Hard rules: replace the inline venv-bootstrap command with
  a reference to the platform script.

AGENTS.md: short "Test Environment Bootstrap" pointer (mirrors CLAUDE.md
  per the Claude/Codex symmetry rule).

.gitattributes: enforce LF for *.sh (otherwise Windows CRLF breaks the
  bash shebang on POSIX) and CRLF for *.ps1 (Windows convention).

Triggered by the warmup-skill Python detection added in agent-playbooks
PR #34 — projects now need a clean bootstrap path for the new check to
have a recommended remediation.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:19:20 +02:00
malkuthroandClaude Opus 4.7 ff79b0f418 feat(video): add Easy_VideoCombine (#154)
* feat(video): add Easy_VideoCombine with absolute-path support

Adds a Koolook subclass of VHS VideoCombine that bypasses ComfyUI's
output-directory sandbox when filename_prefix is an absolute path —
the VFX-pipeline missing piece for writing renders into a project
tree (e.g. E:/projects/shot01/v003) instead of ComfyUI/output/.

Implementation:
- k_video_combine.py: ~60 LoC subclass + a pure _resolve_abs_target
  helper. Scoped-patches folder_paths.get_save_image_path for the
  duration of one combine_video() call so upstream's encoder, audio
  mux, batch manager, and progress bar run unchanged. No VHS code
  copied. Self-skips registration when VHS isn't importable (clean
  one-line stderr notice).
- Discriminator: os.path.isabs(filename_prefix). No new input pin;
  pattern borrowed from spacepxl/ComfyUI-HQ-Image-Save's SaveEXR.
- Optional create_path_if_missing BOOLEAN (default False) so typos
  surface as FileNotFoundError instead of spawning stray dirs.

License audit (2026-05-16):
- VHS upstream: GPL-3.0 (verified via raw LICENSE fetch).
- HQ-Image-Save pattern source: MIT, copyright 2023 spacepxl.
- Koolook: already GPL-3.0; both upstreams are compatible.
- No source code from either upstream is incorporated.

Verification: 18 pytest cases green (12 existing + 6 new covering
relative passthrough, absolute resolution, create-on/off branches,
trailing-separator normalization, and empty-basename rejection).

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

* feat(starter-preset): pre-favourite Easy_VideoCombine

Adds the new node to the shipped starter_preset.json picks so fresh
installs surface it in the Kforge Labs sidebar's Nodes section without
the user having to discover it via Comfy's stock search first. Pairs
naturally with the existing LoadEXR / SaveEXRFrames picks from
HQ-Image-Save.

Existing installs with non-empty picks are unaffected (the seeder
skips them by design); they can either Load -> "starter" in the
Snapshot dialog or hit "+" once in the Nodes section to pull it in.

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

* fix(dev-sync): include k_video_combine.py in RUNTIME_PATHS

Without this entry the new Easy_VideoCombine module is never copied
into the dev ComfyUI install, so the node fails to register on
restart with a silent ImportError. Caught when the maintainer
restarted ComfyUI and couldn't find the node in the search palette.

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

* feat(starter-preset): include all Koolook nodes in default picks

Fresh installs should land on a populated Nodes (favorites) panel
that immediately surfaces what Koolook provides — otherwise new
users see only third-party picks and have no entry point into the
Koolook-native workflow. Adds the 8 currently-registered Koolook
node IDs to the shipped starter preset alongside the existing
third-party essentials:

  EasyAIPipeline, EasyResize_Koolook, EasyWan22Prompt,
  Easy_Pattern, Easy_VideoCombine, Easy_hdr_VAE_decode,
  Easy_hdr_VAE_encode, KoolookLoadCameraPosesAbsolute,
  easy_ImageBatch

Also adds Easy_VideoCombine to the README's Included Nodes table.

Existing installs with non-empty picks are unaffected by the seeder
(by design). To pull the Koolook defaults into an existing install,
Quick Save first, then Snapshot -> Load -> "starter".

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

* fix(video): reach VHS_VideoCombine via nodes.NODE_CLASS_MAPPINGS

The previous `from videohelpersuite.nodes import VideoCombine` fails
inside ComfyUI's loader context because VHS uses relative imports
throughout (`from .logger ...`, `from .utils ...`) that only resolve
when VHS is loaded through ComfyUI's importlib spec — not through a
plain absolute import path. The error surfaced as

    No module named 'utils.install_util'; 'utils' is not a package

at Koolook load time, which caused the node to self-skip with an
empty NODE_CLASS_MAPPINGS — invisible in ComfyUI's search palette.

Fix: look up VHS_VideoCombine through `nodes.NODE_CLASS_MAPPINGS`,
which is the documented cross-pack interface. ComfyUI's loader
(custom_nodes/.../__init__.py exec_module → merge into global) runs
to completion for each pack before moving to the next, and VHS sorts
before Koolook alphabetically in every layout we've seen, so by the
time Koolook's __init__.py runs the global registry already has
VHS_VideoCombine in it.

Verified against ComfyUI's nodes.py load_custom_node() at
load_custom_node() lines 2196 (exec_module) and 2225-2228 (merge),
which guarantee this ordering.

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

* feat(video): add output_directory input for split path/name editing

Overloading filename_prefix for absolute paths means losing the
"just type a name" affordance — once you put the path there, there's
nowhere to specify just the file root. Across many renders into the
same project tree this becomes tedious: every iteration retypes the
whole path.

Adds an optional `output_directory` STRING input. When set:
  - filename_prefix is treated as just the filename root; any path
    components are stripped (so accidental typos like "sub/clip"
    don't double-up under the directory).
  - Absolute output_directory writes directly to that location.
  - Relative output_directory joins under ComfyUI's output/.

When output_directory is empty, the original overloaded
filename_prefix behavior is preserved unchanged (back-compat for
the one-field flow).

Implementation:
- New pure helper `_compose_prefix(filename_prefix, output_directory)`
  combines the two fields into one effective prefix. The existing
  isabs discrimination then handles sandboxed vs absolute writes
  uniformly downstream.
- 6 new tests cover all combinations: empty/abs/relative directory,
  path stripping from filename_prefix, trailing-separator trim,
  empty-name fallback.

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

* fix(video): coerce frontend 'undefined' STRING values to empty

The ComfyUI frontend serializes a STRING widget with empty default
as the literal string "undefined" when left untouched (a known
long-running quirk). Without defensive normalization, an untouched
output_directory was treated as a real directory name and prepended
to the user's path:

  filename_prefix = "E:/renders/clip_v003" (typed by user)
  output_directory = "undefined" (frontend default, not typed)

  _compose_prefix(...) -> "undefined/clip_v003"      (relative)
  _resolve_abs_target -> None (not absolute)
  super().combine_video -> writes to <output>/undefined/clip_v003_*

The QuickTime ended up at <ComfyUI>/output/undefined/<name>.mov
with a spurious `undefined/` subfolder, instead of at the absolute
path the user actually specified.

Fix: a `_normalize_text_input()` helper coerces the literal strings
"undefined" / "null" / "None" (case-insensitive, post-strip) to "".
Applied to both filename_prefix and output_directory at the top of
combine_video() before any path composition. 4 new tests cover the
sentinels and a regression test for the exact path-composition
behavior that produced the bug.

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

* feat(video-fe): wire format-specific widgets for Easy_VideoCombine

VHS's frontend extension (web/js/VHS.core.js:2030) gates its
format-widget machinery on `nodeData?.name == "VHS_VideoCombine"` —
strict-equality on the registered node ID. Our subclass uses a
different ID (`Easy_VideoCombine`), so none of VHS's per-format
widget rendering ran. The user saw:

    [VideoHelperSuite] - WARNING - Missing input for p has been set to hq

on every execution (profile defaulting to "hq" for ProRes), and
had no way to pick anything other than the default — because the
profile dropdown was never rendered.

Fix: a minimal frontend extension at web/easy_video_combine.js
(~80 LoC) that replicates `addFormatWidgets` for our node name.
Hooks the format dropdown's callback to add/remove per-format
widgets the same way upstream does. Self-contained — does not
import from VHS's module (their helpers aren't exported as ES
imports).

Verifies via the actual ComfyUI install after dev-sync; static
preview servers can't exercise it (depends on LiteGraph's
registered_node_types at runtime).

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

* fix(video): strip 'undefined' from anywhere + one-shot debug log

The previous normalization only caught the EXACT literal "undefined"
/ "null" / "None" at field-level. Adds two stronger defenses for the
case where a sentinel still slips through:

1. _normalize_text_input now also strips surrounding quotes (paste-
   from-Explorer artifact) and handles embedded newlines by taking
   the first non-empty non-sentinel line (defends against multi-line
   Text Multiline inputs with trailing blank or junk lines).
2. New _strip_sentinel_components helper drops any path component
   that is exactly a sentinel string from the composed effective_
   prefix — belt-and-suspenders for any path that somehow carries
   `\undefined\` as a literal segment by the time it reaches us.

Also adds a one-shot print() of fp_raw, od_raw, normalized values,
and effective_prefix at the top of combine_video. The user is
seeing an undefined/ folder under their custom path despite the
field-level normalization, and pure code tracing doesn't reproduce
it — the log will surface what kwargs actually contains at run
time so we can fix the real cause if defensive stripping doesn't
already handle it. To be removed in a follow-up once root cause
is known.

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

* chore(video): trim node docstring + tooltips, remove debug print

- Class docstring collapsed from ~20 lines to a single sentence (the
  frontend was showing it as the node description on right-click,
  cluttering the UI).
- Widget tooltips trimmed to one short sentence each — same intent,
  no walls of text.
- Debug print() added in 943be67 removed (defensive strip handled
  the undefined/ bug; no further diagnosis needed).
- Local fp_raw / od_raw variables (only used by the debug print)
  collapsed back into direct call sites.

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

* feat(video): suppress metadata PNG + silent intermediate by default

Upstream VHS writes 3 files per render when audio is connected:
  - <name>_NNNNN.png      (first-frame sidecar with workflow metadata)
  - <name>_NNNNN.mov      (silent intermediate)
  - <name>_NNNNN-audio.mov (final audio-muxed video)

Both extras are controlled by hidden VHS_MetadataImage and
VHS_KeepIntermediate flags in extra_pnginfo['workflow']['extra'],
both default-on upstream. Result: a single 500MB render lands as
three files (one needed, two clutter), nearly 2x the disk footprint
and a confusing folder.

Adds two optional BOOLEAN inputs (both default False on Koolook):
  - save_metadata_png
  - keep_silent_intermediate

combine_video injects them into extra_pnginfo before delegating to
super(), so the existing upstream flags do the actual work — no
post-hoc file deletion, no fork divergence from upstream's encoder
path. The extra_pnginfo dict is copied (not mutated in place) so
other nodes in the workflow that reference the same dict are
unaffected.

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

* diag(video): log frame-count + audio length, restore PNG default-on

Two changes to investigate a doubled-length output report:

1. Default save_metadata_png back to True (was False since 946e170).
   The PNG sidecar carries the embedded workflow JSON — it's the
   only audit trail when a render comes out wrong. Removing it by
   default left no way to diagnose retroactively. Users who don't
   want the sidecar can still toggle it off explicitly; ones who do
   want auditability (most VFX pipelines) get it automatically.

2. One-shot diagnostic print() at the top of combine_video logging:
   - input image batch size + shape
   - audio sample count + sample rate -> seconds
   - frame_rate -> expected video seconds
   - pingpong + loop_count

This pins down whether a doubled-length output is the image stream
arriving already-doubled (workflow issue: ImageConcatMulti in batch
mode, an upstream pingpong, etc.) vs combine_video itself doing
something with the frames. To be removed in a follow-up once the
current report is diagnosed.

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

* fix(video): restore Easy_VideoCombine widgets by name

* feat(video): polish Easy_VideoCombine review workflow

* test(video): fix Easy_VideoCombine pytest coverage

* docs: require repo-local Python venv

* fix(video): handle existing silent mux intermediates

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:53:18 +02:00
malkuthroandClaude Opus 4.7 9dab85c695 feat(easy_pattern): add test-pattern node + auto-restart dev-sync (#135)
* feat(easy_pattern): add test-pattern node + auto-restart dev-sync

Adds an `Easy_Pattern` node (Koolook/Testing) for solid-color frame
batches with optional per-frame index overlays — verifies sequence
merging, frame insertion, batch indexing, and video-pipeline frame
order at a glance. Color inputs use the same
`White/Black/Gray/Custom` dropdown idiom `EasyResize_Koolook` already
exposes for pad/panel colors, so "Gray" means the same 50% sRGB across
nodes.

`scripts/sync_to_dev.py` now restarts the live ComfyUI server after a
successful sync (via ComfyUI-Manager's `/manager/reboot`). Default-on
because dev-sync is only ever invoked to see new code, and Python
custom-node files load once at server start — `--no-restart` is the
escape hatch. The `RUNTIME_PATHS` manifest also picks up
`k_easy_pattern.py` so adding a new top-level node doesn't silently
break the pack at import time on the next sync.

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

* fix(sync-dev): restrict restart URL to http(s) for Bandit B310

`urllib.request.urlopen` accepts arbitrary schemes including `file://`,
which Bandit B310 flags as a path-traversal vector. The script's typical
caller is the maintainer's own shell, but a typo in `--restart-url`
could surprise — and the static gate is right to flag it. Validate the
scheme is http/https before the urlopen call and annotate it so Bandit
accepts the now-truthful narrowing.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:00:34 +02:00
malkuthroandClaude Opus 4.7 a0a8469448 feat(sidebar): build-tag footer + theme auto-pull + × removability + pack badges (#98)
Five user-visible changes to the Kforge Labs sidebar that complete the
dev-sync verification loop and fix theme-mode discovery:

- Build-tag footer at the bottom of the sidebar: `dev <sha> · <time>`
  on line 1 (SHA at 13px monospace, the at-a-glance identifier),
  italic <scope> on line 2. Reads `web/_dev_build.json` written by
  `scripts/sync_to_dev.py --scope "<short scope>"`. Visible-in-app
  companion to PR #95's chat-report convention — closes the gap when
  the maintainer is juggling parallel sessions and can't tell whether
  the browser tab is showing the latest sync. Absent on registry
  installs; footer renders empty there.

- Theme/category mode now mirrors repo-mode population: REPOS auto-
  pull union user-picks, deduped, with the new auto-pull-hidden set
  filtered out. The earlier picks-only design dropped the entire
  Koolook pack from theme mode on a stock install, even though those
  same nodes appeared in repo mode — silently violating the "my
  favorites in two views" mental model. `gatherNodesByTheme` rewritten
  to seed from REPOS first, then user picks, with the same
  autoCategoryRoots + hidden filtering repo mode uses.

- `×` works on every favorite, including auto-pulled REPOS nodes.
  Adds `koolook.autoPullHidden.v1` localStorage Set; `×` on an auto-
  pulled row calls `hideAutoPullType(type)` so the next render skips
  it. `addToMyPicks` calls `unhideAutoPullType` as a side effect so
  re-adding via `+` automatically restores visibility. Same uniform
  "remove / bring back" UX whether the node was a user pick or REPOS
  auto-pull.

- Pack-name badges restored on theme-mode leaves only. Small dim
  `Koolook` / `KJNodes` text on the right of each row, same opacity
  as the search-flatten breadcrumb. Memory aid for "where did this
  node come from?" Repo mode and search-flatten still skip the badge
  (origin is conveyed by tree nesting / breadcrumb prefix).

- `scripts/sync_to_dev.py --scope` flag: extends PR #95's brief-
  output design with a JSON artifact (`<target>/web/_dev_build.json`)
  carrying commit + timestamp + worktree + scope, which the sidebar
  footer fetches on render. Doesn't change the existing one-line
  chat-report contract or the `_build_line()` shape.

Tested via dev-sync against the live ComfyUI install:
- Footer renders correctly (`dev afe539e · 2026-05-06 15:38` + scope)
- Image bucket in theme mode now contains Koolook image nodes
- Pack badges visible on theme leaves
- × hides auto-pulled Koolook nodes; toolbar + restores them

CHANGELOG entries added; the prior conflicting [Unreleased] notes
about picks-only theme + pack-badge removal are flagged as superseded
in the new entries (release prep can squash).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:06:02 +02:00
malkuthroandClaude Opus 4.7 afe539e8dc chore(dev-sync): brief output + build-id stamp + chat-report convention (#95)
* chore(dev-sync): brief output + build-id stamp + chat-report convention

After a dev-sync the maintainer was running 4 parallel ComfyUI installs
with no way to tell which version was just synced and a 50-word "what's
new" message that was hard to skim. Two fixes, one PR.

Script (`scripts/sync_to_dev.py`):

  • Default output is now ONE line — the per-file "copied: <path>" lines
    move behind `-v` / `--verbose`. Useful when something goes wrong; not
    useful for the routine "did it work" signal.
  • New summary line includes a build identifier:
        synced 10 entries @ <short-sha> v<version> -> <target>
    Both the SHA (via `git rev-parse --short HEAD`) and version (parsed
    from pyproject.toml) are best-effort — either can be missing and the
    line still reads cleanly.
  • Dry-run picks up the same shape:
        would sync 10 entries @ <short-sha> v<version> -> <target>

CLAUDE.md (project instructions):

  • Tightened the `dev-sync` section to its essentials.
  • Added a "Chat report format" subsection — the agent must include the
    build line from script output and a ≤10-word summary naming what to
    LOOK AT after restart, not a list of changes:
        dev-sync @ <short-sha> v<version> — <≤10 word summary>. Restart.
  • Concrete examples included so the convention is unambiguous.

No runtime behavior change — pure tooling + docs.

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

* chore(dev-sync): two-line report format — sha + worktree, then summary

Maintainer feedback: the previous format ("dev-sync @ <sha> v<version>
— <summary>. Restart.") cluttered slot 1 with version info and forced
the summary onto the same line. Both made the message harder to scan
when juggling multiple parallel ComfyUI installs.

New format — two lines, build id alone on line 1:

  <short-sha> - <worktree-name>
  <≤10 word summary of what to look at>

Worktree name disambiguates "which checkout fed this sync" when the
maintainer is running several in parallel — the version field rarely
moves between syncs so it bought less than the worktree name does.

Script changes:
  • Replaced `_build_id()` with `_build_line()` returning the
    pre-formatted ``<sha> - <worktree>`` header.
  • Replaced `_get_version()` with `_get_worktree_name()` (REPO_ROOT
    basename — works for both worktrees and the main checkout).
  • Output now prints the header line first, then the mechanical
    "synced N entries -> <target>" line, then the restart hint. Eye
    lands on the identifier before the details.

CLAUDE.md change: matching update to the chat-report convention,
including two examples.

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

* chore(dev-sync): drop "restart ComfyUI" line + user-initiated-only policy

Maintainer feedback:
  • The "restart ComfyUI to pick up changes" line at the end of the
    script output is noise — they know to restart, the reminder
    clutters the message. Removed.
  • Dev-sync was being run by sessions automatically (post-commit, on
    ship-pr, etc.), silently overwriting code that other parallel
    sessions were reviewing. Now explicitly user-initiated only:
    project CLAUDE.md spells out the policy and lists the automated
    flows that must NOT trigger it; the script docstring carries the
    same warning so anything that reads it sees the rule.

Chat report convention: line 2 is now described as "scope of what to
review" (the change in this build) rather than "thing to look at after
restart", to match how the maintainer actually thinks about it.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:07:14 +02:00
malkuthroandClaude Opus 4.7 93363cc625 feat(sidebar): snapshot library v2 — configurable filesystem path via KFORGELABS_PRESETS (#68)
* feat(sidebar): snapshot library — named presets for picks + workflows

Implements #46 item 1 (custom save location + cross-machine portability)
as a preset library: each preset is one named JSON file under
/userdata/koolook-presets/, capturing the full Kforge Labs state.

Use cases addressed
-------------------
- **Personal portability:** copy `koolook-presets/` between machines, or
  sync ComfyUI's userdata folder via Dropbox/iCloud/Drive — your kit
  follows.
- **Team / facility shared library:** multiple workstations on the same
  ComfyUI server share `/userdata/koolook-presets/` natively. One person
  publishes "facility kit", everyone else loads it.
- **One-off transfer (different servers):** Manage tab's Download button
  saves a snapshot to local disk; Upload imports it into another machine's
  library.

UI
--
New top-level **Snapshot** action row above the search field, mirroring
the Nodes/Workflows action-row pattern. Three icon buttons (Load / Save /
Manage), each opens the same modal at a different starting tab.

- Load tab: lists every preset with metadata (workflow count · pick
  count · export date). Click a row → "Replace current state?" confirm
  → restores both picks and workflows.
- Save tab: input field defaults to `preset YYYY-MM-DD`, fully editable.
  Existing-name save prompts to overwrite.
- Manage tab: per-row Download / Delete buttons + a top-of-tab Upload
  action (file picker imports a JSON snapshot from disk into the library).

Schema
------
```json
{
  "kind": "koolook-snapshot",
  "version": 1,
  "name": "Wan video kit",
  "exportedAt": "2026-05-05T08:42:13.000Z",
  "picks": [...],
  "workflows": { "directories": {...} }
}
```
`kind` + `version` for future schema migrations. Anything without
`kind === "koolook-snapshot"` is rejected on import.

Module shape
------------
- `web/sidebar/snapshot.js` (new) — server I/O on the preset directory,
  schema validation, gather/apply, download/upload helpers. UI-free.
- `web/sidebar/modals.js` — new `showSnapshotModal({initialTab, ...})`
  that takes capability functions instead of importing snapshot.js
  directly, so the UI module stays UI-only.
- `web/sidebar/workflows_store.js` — new `replaceAllWorkflows(rawStore)`
  for bulk-replace (bypasses persistMutation since the entire cache is
  being swapped) + `getAllWorkflowsForExport()` for the gather flow.
- `web/sidebar/picks_store.js` — new `setAllPicks(picks)` exported
  helper.
- `web/sidebar/tree.js` — new top action row + 3 toolbar buttons +
  one wiring helper.
- `web/sidebar/constants.js` — CSS for tab strip, list rows, manage
  buttons.

Deliberate scope cuts (per the design discussion)
-------------------------------------------------
- No continuous sync. Each save is a deliberate act.
- No conflict resolution. Same preset name written from two machines
  on the same server = last-write-wins. Documented as a known limit;
  revisit when it bites.
- No selective import (preset = full state). Merge can be a v2.
- No custom userdata path config. The `koolook-presets/` location is
  fixed; portability comes from the snapshot files themselves, not
  from rerouting storage.

Verification
------------
- `node --input-type=module --check` on all modified + new JS files —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac for browser smoke-test.

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

* feat(sidebar): snapshot library now uses configurable filesystem path

Reworks PR #67 (which served snapshots from /userdata/koolook-presets/)
to expose the library through Koolook's own server-side endpoints,
making the storage location configurable via an env var. The userdata
namespace is per-install — fine for personal use, useless for the
"facility shared library" use case the user explicitly called out.

Architectural change
--------------------
- The client no longer talks to ComfyUI's `/userdata/*` API for snapshot
  files. New JS-side endpoints: `/koolook/presets/{info,list,file}`.
- Server side: new `koolook_routes.py` registers aiohttp routes on
  ComfyUI's PromptServer at custom-node load time. Path-traversal
  protection at the route boundary via a strict filename whitelist
  regex (`^[A-Za-z0-9 _.()\-]+\.json$`).
- Storage location is read from the `KFORGELABS_PRESETS` env var on
  every request (so re-launching ComfyUI with a new value picks it
  up without any cache fuss). Default fallback:
  `<comfyui-userdata>/koolook-presets/`.
- Auto-creates the configured directory on first save (parent must
  already exist — for NFS/SMB mounts the mount must be present).

Use cases this enables vs. PR #67
---------------------------------
| Use case                | PR #67 | This PR |
|-------------------------|--------|---------|
| Personal, single-machine | ✓     | ✓       |
| Personal cross-machine  | symlink-or-sync userdata | point env var at Dropbox folder |
| NFS/SMB facility share  | ✗ (locked to userdata) | ✓ point env var at mount |
| Multiple parallel libs  | ✗     | per-instance env var |
| Read-only distribution  | ✗     | ✓ (server detects, save fails cleanly) |

UI difference vs. PR #67
------------------------
- The Manage tab gains a small `i` icon at the right edge of the
  upload row. Hover tooltip shows the current library path, source
  (default vs `KFORGELABS_PRESETS` override), and writability status.
  No header line, no extra UI weight.

Files added/changed
-------------------
- `koolook_routes.py` (new, ~180 lines) — aiohttp routes + env-var
  config + filename validation. Loaded by `__init__.py` at startup;
  failure is non-fatal (the rest of Koolook still works, just the
  snapshot feature is unavailable).
- `__init__.py` — registers `koolook_routes` import-time side-effect.
- `scripts/sync_to_dev.py` — `koolook_routes.py` added to
  `RUNTIME_PATHS` so dev-sync includes it.
- `web/sidebar/snapshot.js` — server I/O section rewritten to call
  `/koolook/presets/*` instead of `/userdata/koolook-presets/*`. Drops
  the URL-encoding workaround (we own the route now, so the
  `name=<file>` query param is clean and the server validates).
- `web/sidebar/modals.js` — `showSnapshotModal` accepts a
  `getLibraryInfo` capability function; the Manage tab calls it and
  populates the info-icon tooltip.
- `web/sidebar/tree.js` — passes `getLibraryInfo` from `snapshot.js`
  to the modal.
- `.env.example` — new `KFORGELABS_PRESETS` block with macOS / Linux /
  Windows examples for personal-sync, NFS facility share, and SMB
  facility share patterns.
- `CHANGELOG.md` — Added entry rewritten for the env-var design;
  Internal entry on the new server-side module.

Why a separate v2 PR (instead of patching #67)
----------------------------------------------
The userdata-namespace design in #67 was the wrong architecture for
what the user actually wanted. Patching it to the env-var design
would have meant a near-total rewrite of #67's server I/O paths
mid-review. Cleaner: branch fresh from main, cherry-pick #67's UI
+ stores work as a base, layer the new endpoint design on top.
PR #67 will be closed as superseded once this lands.

Verification
------------
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python -c "import ast; ast.parse(...)"` on `koolook_routes.py`
  + `__init__.py` — clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac. User to smoke-test against a real
  ComfyUI session (the previous URL-encoding 405 should be gone
  since the new route handlers own the URL space).

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

* feat(sidebar): redesign snapshot UX — single-click Save, real Settings panel

User feedback on #68's first cut: tabbed modal is too complicated, can't
see existing presets in the Save flow, the env var alone isn't a real
settings UI, the default-path text was stale (still claimed
`/userdata/koolook-presets/` after the configurable-path landed).

UX changes
----------
- **Save button → one click.** If a preset is currently loaded
  (tracked in localStorage as `koolook.snapshot.currentPresetName.v1`,
  survives reloads), prompts "Save over '<name>'?" with three options:
  Save (overwrite) / Save as new… (rename) / Cancel. If nothing is
  loaded yet, prompts for a name with `preset YYYY-MM-DD` default,
  fully editable.
- **Load → single dialog.** Preset list with per-row × Delete button.
  Header line shows the current library path so the user can see where
  they're loading from. No tabs.
- **Cog → real Settings panel.** Single field for the library path
  (text input, absolute path). Save / Reset to default / Close. Read-
  only line shows the currently-resolved path + source label
  (settings panel / env var / built-in default). The "Manage" tab and
  its Download / Upload sub-actions are gone — the user can already
  reach the file on disk after a save, no in-app file ops needed.

Server side
-----------
- New `GET /koolook/presets/settings` returns the saved library path +
  resolved path + source.
- New `POST /koolook/presets/settings` writes the saved library path
  to `<comfyui-userdata>/koolook-settings.json`. Empty value clears
  the override.
- Resolution chain in `_configured_dir()`:
    settings file > KFORGELABS_PRESETS env var > built-in default
  Reads each on every call; no module-load caching.
- `info` endpoint gains a `source` field ("settings" | "env" | "default")
  the UI uses for the readout line.

Client side
-----------
- `showSnapshotModal` (tabbed) replaced by three exports:
    showSaveSnapshotDialog, showLoadSnapshotDialog,
    showSnapshotSettingsDialog.
  Each is self-contained, no shared state.
- `snapshot.js` adds `getSettings`/`saveSettings` for the Settings
  dialog and `getCurrentPresetName`/`setCurrentPresetName` for the
  one-click Save tracking. Settings ride on the new server endpoints;
  current-preset tracking is browser-local.
- `applySnapshot` now also updates the current-preset tracker (via
  the dialog wrapper, not the function itself) on successful load.
- `tree.js` toolbar wires three direct callbacks instead of one
  `openSnapshotModal(initialTab)` helper.

Stale text fix
--------------
- The Save dialog's hint text in #68's first cut hardcoded
  "/userdata/koolook-presets/<name>.json" — stale from the userdata-
  era PR #67. The redesigned Save flow doesn't show that text at all
  (the path is no longer relevant to the user-typed name); the Load
  dialog's header line and the Settings dialog's read-only field are
  the canonical surfaces for "where my presets actually live."

Deliberately out of scope
-------------------------
- No per-save path picker. The library is one location at a time, set
  in advance via Settings or env var. Saving to ad-hoc locations would
  fragment the library and break the "facility shared library" use case.
- No in-app Download / Upload (was in #68's first cut). Files are on
  disk at the configured path; the OS file manager handles transfer.

Verification
------------
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python -c "import ast; ast.parse(...)"` on `koolook_routes.py` —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- Synced to live install on Mac. User to smoke-test against a real
  ComfyUI session.

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

* fix(sidebar): address PR #68 review — atomicity, security, error UX

Multi-agent review surfaced 2 BLOCKING + 5 HIGH + 4 MEDIUM + 3 LOW.
Validated the BLOCKINGs against the actual code (both real) and
applied the bundle of fixes that hold up. Modal-size HIGH (#7 file
size) deferred to a follow-up — that's a wider refactor.

BLOCKING fixes
--------------
- **`applySnapshot` partial-failure tracker gating** (modals.js Load
  dialog). `setCurrentPresetName(...)` was called unconditionally
  before checking `{picksOk, workflowsOk}` from the apply. Partial
  apply (one of picks/workflows persisted, the other didn't) left
  the tracker pointing at the preset, so the next Save would offer
  "Save over '<name>'?" and silently overwrite the on-disk preset
  with the corrupted half-state. Now: tracker is set only on
  `picksOk && workflowsOk`; partial failure clears it (and the toast
  tells the user "Reload to recover prior state. Tracker cleared.").

- **`replaceAllWorkflows` non-atomic rebind** (workflows_store.js).
  Rebinds `workflowsCache` before awaiting persist, with no rollback.
  Now: snapshots the cache via the existing `snapshotCache()`
  primitive that `persistMutation` uses; on persist failure, restore
  the prior cache and return false. Load is fully atomic.

HIGH fixes
----------
- **Workflows-store invariant block updated.** Lists `replaceAllWorkflows`
  as the fourth legitimate rebind site (with rollback semantics).
  "Fifth rebind" warning bumped to "sixth."
- **Explicit HEAD route on `/koolook/presets/file`** that just does
  `is_file()` + status, no body work. The auto-synthesis path was
  reading the full file from disk just to discard it — wasteful on
  multi-MB snapshots over Dropbox/iCloud/NFS.
- **Client `sanitizeName` mirrors server whitelist regex**
  (`^[A-Za-z0-9 _.()\-]+$`). Characters outside the whitelist (`+`,
  `&`, `'`, accented letters, emoji, etc.) collapse to `_` rather
  than triggering an opaque HTTP 400 at save time.
- **Error-reason surfacing via `await resp.text()`.** RFC 7540 strips
  HTTP/2 reason phrases; behind any HTTP/2-terminating proxy
  `resp.statusText` is empty. Reading the body always carries
  aiohttp's `reason=` text. Used by writePreset, deletePreset,
  saveSettings, readPreset.

MEDIUM fixes
------------
- **Symlink follow-through hardening.** `_resolve_within_base` helper
  computes `(base / name).resolve()` and asserts it's still under
  `base.resolve()`. Defends against a symlink planted in a facility-
  shared library dir redirecting reads/writes off-share.
- **Atomic settings write.** `_write_settings` writes to `<tmp>` then
  `os.replace`, so an interrupted process can't truncate the file
  and silently revert the user's saved library path on next load.
- **`__init__.py` import wrapped in try/except.** The "non-fatal"
  comment promised that an import failure wouldn't break node
  registration; previously only `install()` returning False was
  guarded. Now any module-load exception is caught and logged with
  the `[Koolook]` prefix, and the rest of the plugin loads.

LOW housekeeping
----------------
- Dropped dead exports `downloadSnapshotAsFile` + `readSnapshotFile`
  (left over from an earlier iteration that had Download/Upload UI).
- Dropped 5 dead CSS rules (`.koolook-snapshot-tabs*`,
  `.koolook-snapshot-tab-pane`) from when the modal was tabbed.
- `currentPresetName` cleared when the user deletes the currently-
  loaded preset (Load dialog's promptDelete now checks via the new
  `getCurrentPresetName` capability).
- `loadPreview` warns to console on `kind` mismatch — non-snapshot
  JSON files in the library directory are now visible in DevTools
  rather than silently filtered.
- `presetExists` is tri-state (`true` / `false` / `null`) — the
  Save flow refuses to write when the library is unreachable rather
  than silently skipping the overwrite-confirm prompt.

Deferred
--------
- modals.js size (1080 lines) — extracting the snapshot dialogs into
  their own file is a follow-up refactor. Not blocking merge.
- Doc drift on maintainer docs / README env-var convention — separate
  documentation sweep.

Verification
------------
- `python -c "import ast; ast.parse(...)"` on all .py files — clean.
- `node --input-type=module --check` on every modified JS file —
  clean.
- `python tools/preflight_release.py --skip manager-meta -v` PASS.
- `grep -rn "downloadSnapshotAsFile\\|readSnapshotFile\\|koolook-snapshot-tab" web/`
  — zero hits (dead code gone).
- Synced to live install for browser smoke-test.

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

* ci: nudge — re-trigger CI after webhook miss on d6d2bf8

The CI workflow didn't fire on the prior push (Socket Security ran but
GitHub Actions Lint/Smoke/Pre-flight/Security Gate didn't). Empty
commit to refire the pull_request webhook.

* fix(routes): drop unnecessary f-string prefixes flagged by ruff (F541)

Two adjacent string literals in the symlink-escape error message had
an `f` prefix despite containing no placeholders. CI's ruff check
fails on F541 — strip the prefixes so the lint gate passes.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:06:46 +02:00
malkuthroandClaude Opus 4.7 0d4c81295b chore(sync): --init flag for first-run dev-sync setup + Mac path docs (#61)
Background
----------
Setting up `scripts/sync_to_dev.py` on a fresh machine required two
pre-steps the script didn't help with: (1) editing `.env` to point at
a path that didn't exist yet, then (2) `mkdir -p` on that path before
the first sync would work. The script errored out cleanly with
"target does not exist" but didn't tell the user the obvious next
move was to create it. This was particularly silent on Mac where the
ComfyUI install path differs from the Windows examples in `.env.example`.

Changes
-------
- **`--init` flag** on `scripts/sync_to_dev.py`. When the target
  directory doesn't exist and `--init` is passed, the script creates
  it and proceeds with the sync. Without `--init`, it errors out with
  the same "first time on this machine?" hint pointing at the new flag.
- **Safety guards** on `--init`. Refuses (exit 3) if:
  - the parent directory doesn't exist, OR
  - the parent doesn't look like a ComfyUI `custom_nodes/` folder
    (heuristic: parent is named `custom_nodes` OR sits inside a
    directory named `ComfyUI`).
  This prevents a typo in `KOLOOK_COMFYUI_DEV_PATH` from quietly
  creating a `ComfyUI-Koolook/` somewhere unexpected on disk. The
  refusal message includes a manual escape hatch (`mkdir -p` + retry
  without `--init`) for the case where the parent really IS the
  ComfyUI install but doesn't match the heuristic.
- **Drop stale `k_easy_version.py`** from `RUNTIME_PATHS`. The file
  was deleted in v0.1.10 (PR #41); the script's `if not src.exists()`
  guard meant it was a no-op every run, but the entry was misleading.
- **`.env.example`** gains explicit macOS / Linux / Windows path
  examples for `KOLOOK_COMFYUI_DEV_PATH` and a 3-step first-run
  recipe (cp .env.example → edit → `--init`).
- **`docs/maintainers/dev-iteration-loop.md`** gains a "First-time
  setup on a new machine" section with the per-OS path table and the
  same 3-step recipe.

Verification
------------
- `python scripts/sync_to_dev.py --dry-run` against a missing target
  → exit 2, hint at `--init`.
- `python scripts/sync_to_dev.py --init` against a real ComfyUI
  `custom_nodes/` parent → creates the leaf, copies 9 entries, exits 0.
- `python scripts/sync_to_dev.py` (no flag) re-run after `--init` →
  re-syncs cleanly, idempotent.
- `python scripts/sync_to_dev.py --init` against `/tmp/not-custom-nodes`
  → exit 3, "parent does not exist".
- `python scripts/sync_to_dev.py --init` against an existing
  `/tmp/wrong-parent` (not custom_nodes) → exit 3, "doesn't look like
  a ComfyUI custom_nodes/ folder", manual escape hatch in the message.
- `python -c "import ast; ast.parse(open('scripts/sync_to_dev.py').read())"`
  → syntax OK.
- `python tools/preflight_release.py --skip manager-meta -v` → PASS.

No CHANGELOG entry — dev-tooling under `scripts/` doesn't ship to
users of the published package. Avoiding noise on the [Unreleased]
section that PRs #59 / #60 also touch.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:58:24 +02:00
malkuthroandClaude Opus 4.7 0a914cb8de chore(dev): add scripts/sync_to_dev.py + KOLOOK_COMFYUI_DEV_PATH env var (#40)
For one-line fixes that don't deserve their own version bump, copy the
runtime files into the maintainer's live ComfyUI custom_nodes folder
directly instead of going through the publish-and-update round-trip.

What this adds:
- `scripts/sync_to_dev.py` — minimal Python (no third-party deps); reads
  `KOLOOK_COMFYUI_DEV_PATH` from the environment (auto-loads `.env` at
  the repo root); copies the curated set of runtime-relevant files
  (top-level `k_*.py`, `__init__.py`, `config.json`, `forks/`, `web/`)
  while skipping CI/docs/metadata. Errors out cleanly when the env var
  is unset — never guesses a path.
- `.env.example` entry `KOLOOK_COMFYUI_DEV_PATH=` with explanatory
  comment, matching the existing `KOLOOK_*` convention.
- `CLAUDE.md` section documenting the trigger phrase `dev-sync` so a
  fresh agent session knows to invoke the script when the maintainer
  asks for it.

This is a development-loop convenience only — anything worth shipping
still goes through the normal PR + release flow.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:02:02 +02:00