Commit Graph
229 Commits
Author SHA1 Message Date
malkuthro df1d66402a fix(deps): relock test aiohttp (#215)
* fix(deps): relock test aiohttp

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

* Document ComfyUI server API testing

* Harden loop queue controller

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:26:26 +02:00
malkuthro 7f8c09ad29 feat(sidebar): clean up workflow archives (#201) 2026-06-02 22:25:09 +02:00
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 f7e0666cc0 fix(sidebar): guard name-keyed store mutators against prototype pollution (#203)
CodeQL js/prototype-polluting-assignment (alert #1, medium) flagged
`dir.workflows[wfName].archived = true` in archiveWorkflow: a "__proto__"
name resolves to Object.prototype, passes the truthy existence check, and
the assignment pollutes the prototype process-wide. The same shape exists
across every exported mutator that uses an untrusted directory/workflow
name as an object key.

Add a shared isSafeObjectKey() denylist guard (__proto__, constructor,
prototype) with an early `return false` at all 12 keyed-mutation sinks:
add/rename/delete/move Directory, save/archive/unarchive/rename/delete/move
Workflow, and add/removeTag. Read-only accessors and clearArchive (which
derives names from own entries) are left untouched — reads don't pollute.

Supersedes the Copilot autofix draft #167 (extends it from a partial set
to every sink). Validated with `node --check`; repo has no JS test runner.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:33:27 +02:00
malkuthro 67e9d55e98 docs(nodes): add user guides and tooltips (#200)
* docs(nodes): add user guides and tooltips

* chore: clean up node preflight nits
2026-06-02 20:42:56 +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
malkuthro b5b46d7fcc feat(sidebar): Snapshot Compare — read-only A/B side-by-side with diff tint (#181) (#183)
Phase 1 of #181: read-only side-by-side Compare. A/B toggle renders the existing sidebar twice — live working copy (A) + a loaded snapshot (B), B labeled with its own name + diff-tinted (green=only in comparison, red=same name/graph differs ignoring savedAt). Non-destructive: read-only capture guard on B, onChoose chooser never calls applySnapshot. Phase 2 (pull-in + A<->B swap) tracked in #197.
2026-06-02 08:22:15 +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 3294e6e7ae fix(video): robust load path handoff (#192)
* fix(video): make load path handoff robust

* fix(video): defer validation for linked load paths

* fix(video): avoid duplicate extension on counter strip

* revert(video): leave combine naming unchanged

* docs(video): clarify load path validation tradeoffs
2026-05-31 01:24:32 +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 ac01c4e573 feat(video): add load-friendly video outputs (#190) 2026-05-30 22:52:04 +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
malkuthro 7d46d1cdf2 Release v0.3.7 (#175)
* Release v0.3.7: support docs and fixes

* docs: align bug report privacy guidance
v0.3.7
2026-05-23 10:23:04 +02:00
malkuthroandkflame99 d0fb000239 fix(snapshot): show disk-renamed snapshot names (#174)
* fix(snapshot): show disk-renamed snapshot names

* fix: address kflame99 review feedback (#174)

Split the autosave embedded-name guard into its own test and make the root snapshot filename assertion whitespace-tolerant.

Co-Authored-By: kflame99 <kflame99@users.noreply.github.com>

---------

Co-authored-by: kflame99 <kflame99@users.noreply.github.com>
2026-05-22 22:43:26 +02:00
malkuthro 8447c05795 docs: add automation loop visual overview (#163) 2026-05-19 23:20:36 +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 0fea0a6a70 fix(sidebar): keep archive collapsed during search (#165) 2026-05-18 22:20:21 +02:00
malkuthro 1ae86da0de Release v0.3.6: workflow draft quota hotfix (#168) v0.3.6 2026-05-18 20:33:13 +02:00
malkuthro e3f94cca6b fix(sidebar): reuse draft id for workflow loads (#166) 2026-05-18 11:43:55 +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
v0.3.5
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
malkuthro 1c1c870439 fix(sidebar): keep loaded workflows untitled in Comfy (#157) 2026-05-17 22:04:38 +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 4409d3c9d8 feat(ai_pipeline): no_subfolders toggle + path normalisation + tooltips (#156)
* feat(ai_pipeline): no_subfolders toggle + path normalisation + tooltips

Three EasyAIPipeline improvements with a 49-case test suite pinning
the behavior so the user-reported "phantom undefined/ folder" bug
can't regress.

no_subfolders (default off): write directly into base_directory_path
with no shot_name/ai_method/vNNN subfolders appended. Base folder is
still auto-created if missing.

Paste-input hardening: _normalize_base_path strips surrounding
whitespace, one matched pair of quotes ("..." from Explorer's Copy
as path), and trailing path separators. Drive roots (C:\, n:/) are
preserved. Plus a belt-and-suspenders rstrip on the final
output_directory — fixes the symptom where a trailing backslash on
the user's pasted base path caused downstream save nodes to split
on /, stringify the empty tail as "undefined", and write into
.../undefined/... on disk.

Smart filename: empty pieces of shot_name / ai_method / vNNN are
filtered before the underscore join, so blank ai_method yields
RTX-upscale.exr (not RTX-upscale_.exr). Same rule for subfolder
build.

Tooltips on every input explain interactions (e.g. ai_method
becomes both filename segment and subfolder; enable_overwrite only
blocks an existing file, not directory).

JS preview helpers in web/ai_pipeline.js mirror the Python so the
Get output directory / file path buttons stay accurate.

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

* fix(ai_pipeline): sanitize shot_name/ai_method to prevent base-path escape

Reviewer-flagged blocker in PR #156: os.path.join's "last absolute path
wins" rule let absolute-looking shot_name or ai_method values (typos
like "/oops", or pasted full paths like "C:/Windows/junk") replace
base_directory_path entirely. The pre-#156 code had a partial defense
(.lstrip('/')) that the refactor dropped without replacing.

New _sanitize_segment helper: lstrip leading separators → splitdrive →
lstrip again. Strips leading seps BEFORE splitdrive so multi-slash
input like "///oops" doesn't get swallowed as a UNC prefix on Windows
(empirically observed — the user's intended segment name would be lost).
Applied symmetrically to directory build and filename build. Raw
shot_name kept in the return tuple for downstream label-only consumers.

Also fixes a minor JS preview mismatch at web/ai_pipeline.js: empty
base + no_subfolders=true used to render "/name.exr" while Python
returns "name.exr". Filter empties before joining the file_path.

16 new tests pin both the helper and the end-to-end escape vectors:
- TestSanitizeSegment (7 unit tests, platform-aware drive handling)
- 4 parametrised absolute shot_name escape cases
- 4 parametrised absolute ai_method escape cases
- 1 filename-leak regression

Full pytest: 102 passed.

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

* fix(ai_pipeline): flatten embedded separators in shot_name for filename

Maintainer-reported: with no_subfolders=true and an upstream node feeding
shot_name="ComfyUI-working-folder/LTX-Director-2K", the output was
base/ComfyUI-working-folder/LTX-Director-2K.%04d.exr — still creating
subfolders via the filename concat, defeating the toggle entirely.

Root cause: the prior fix sanitized only LEADING separators (path-escape
case). Internal separators in shot_name slipped through and became
subfolders when the filename string with embedded "/" was joined onto
output_directory by the filesystem.

Fix: filenames can't contain "/" or "\\" on any OS, so always flatten
them to "_" in the filename build. Directory build with
no_subfolders=false still uses the separators as subfolder delimiters
(intentional — users organizing into Project/v1/ai_method/vNNN want this).

With no_subfolders=true, shot_name="job/shot" now correctly produces
base/job_shot.ext (flat in base), not base/job/shot.ext.

3 new tests:
- Maintainer's exact case (no_subfolders=true + slash in shot_name)
- Belt-and-suspenders across 5 shot_name/ai_method/toggle combinations
- Verify no_subfolders=false still creates subfolders from slashes
  (preserves intended use case)

Full pytest: 110 passed.

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

* fix(ai_pipeline): strip control chars + single-line Output Preview

Maintainer reported the file_path preview rendering as two visual lines
("n:/.../bear/...LTX-Director-2K" + newline + "_0.%04d.exr") and the
output looking like it contained a literal newline. Two contributing
causes:

1. The multi-line Output Preview widget word-wrapped long single-line
   paths at the textarea edge. The data was always one logical string,
   but the visual break made it look broken. Fix: set wrap="off",
   whiteSpace="pre", overflowX="auto" on the input element so long
   paths render on a single horizontal-scrollable line.

2. Upstream nodes (WAS Text Multiline etc.) can legitimately emit
   strings with embedded \n / \r / \t — a stray paragraph break in the
   widget content. Those would silently break filesystem saves and ALSO
   wrap the preview at the actual newline character (not just the
   textarea edge). Fix: new _strip_control_chars helper, applied at the
   top of _normalize_base_path and _sanitize_segment (covers
   base_directory_path, shot_name, ai_method) plus explicitly on
   extension in generate_pipeline. Mirrored in JS for preview parity.

9 new tests: 6 helper unit tests, 3 end-to-end (shot_name w/ newline,
base path w/ newline, extension w/ newline).

Full pytest: 119 passed.

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

* fix(ai_pipeline): strict whitespace strip on extension + revert widget

Two issues, one commit:

1. Maintainer reported ComfyUI-HQ-Image-Save bailing with "Filepath
   needs to end in .exr" even though our preview showed a clean
   .%04d.exr suffix. Root cause: HQ-Image-Save validates via
   os.path.splitext(filepath)[1].lower() != ".exr" — a single trailing
   space on the extension widget (easy to acquire from a paste) makes
   splitext return ".exr " with the space, which fails the strict
   equality. Previous control-char strip only handled \r\n\t and
   left regular spaces alone.

   Fix: extension now runs through "".join(extension.split()) which
   removes ALL whitespace (internal + surrounding, including unicode
   whitespace and non-breaking spaces). Mirrored as cleanExtension
   in JS. Also added .strip() to _sanitize_segment so shot_name /
   ai_method with stray surrounding whitespace canonicalise too.

2. Reverted the Output Preview widget from the previous commit's
   single-line wrap=off + horizontal-scroll layout back to the
   original multi-line word-wrap (100px height, default textarea
   behavior). The earlier change traded mouse-drag-selectability for
   single-line display — wrong tradeoff. The path is one logical
   string anyway (cleanLine strips real newlines), so visual wrap is
   just cosmetic.

9 new tests:
- 6 parametrised extension whitespace cases (trailing/leading/internal
  space, tab, non-breaking space) — all verify HQ-Image-Save's
  splitext check passes after sanitization
- 3 parametrised shot_name surrounding-whitespace cases

Full pytest: 128 passed.

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

* feat(ai_pipeline): no_subfolders keeps version folder; v### in tooltips

Maintainer-clarified semantics for the no_subfolders toggle: it should
only drop shot_name / ai_method from the directory chain, NOT the
version folder. Previously, no_subfolders=true stripped everything,
which meant versioned outputs all collided in the same flat directory —
defeating versioning whenever the toggle was on.

New behavior matrix for no_subfolders=true:
- disable_versioning=true  → output_directory = base
- disable_versioning=false → output_directory = base/v###

So truly flat output (no subfolders at all) now requires BOTH toggles,
matching VFX convention. Versioned-but-no-name-subfolders is now
expressible with just no_subfolders=true, which is the common case.

Tooltip updates:
- no_subfolders: rewritten to describe new behavior
- base_directory_path: path-shape description updated to
  shot_name/ai_method/[v###]
- ai_method: clarifies subfolder application is gated by no_subfolders
- version, disable_versioning: vNNN → v### (Nuke-style convention
  consistent with the existing .%04d.exr default)

2 new tests:
- no_subfolders=True + disable_versioning=False → base/v###
- no_subfolders=True + disable_versioning=True  → base (truly flat)

Existing tests use the disable_versioning=True default with
no_subfolders=True so they continue to assert base-only — they still
pass under the new logic since version_str="" gets filtered out of the
join.

Full pytest: 130 passed.

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

* fix(ai_pipeline): strip sentinel path components

* fix(ai_pipeline): resolve EasyUse get-set preview values

* docs: document ComfyUI preview button debugging

* fix(ai_pipeline): align drive segment sanitization

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:01:43 +02:00
malkuthro bc7ec09971 Fix sidebar workflow persistence recovery (#155)
* fix(sidebar): recover workflow saves after fallback

* fix(sidebar): merge workflow fallback recovery

* fix(sidebar): clear redundant workflow fallback
2026-05-17 16:30:11 +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
malkuthro 55dab39c9b Trim workflow context menu move list (#153) 2026-05-16 23:22:23 +02:00
malkuthro f74b30603e Release v0.3.2 (#149) v0.3.2 2026-05-16 01:25:39 +02:00
malkuthro 119a1e3e6c docs: refresh release guide screenshots (#148) 2026-05-16 01:05:02 +02:00
malkuthro c7961998aa feat(snapshot): refine recovery choices and snapshot dialog styling (#146)
* Refine snapshot load dialog recovery UI

* Make snapshot load recovery choices explicit

* Align snapshot picker styling

* Respect Comfy theme variables in snapshot dialogs

* Document superseded snapshot recovery design

* Fix load recovery selection races

* Prevent duplicate snapshot dialog actions

* Harden snapshot recovery UI fallbacks
2026-05-15 10:35:13 +02:00
malkuthroandClaude Opus 4.7 8e489ad37a feat(snapshot): redesigned Save/Load dialogs + folder picker (#137) (#144)
* feat(snapshot): row-augment max(periodic, pre_load_*) + pytest infra (#137)

Server-side preparation for the snapshot dialogs redesign. The Load
dialog's "newer auto-save available" row flag now reflects the freshest
recovery file regardless of which mechanism wrote it — before this we
only stat'd ``periodic.json``, missing cases where the user's most
recent Load wrote a fresher ``pre_load_*.json`` snapshot inside the
same autosave subfolder.

Extracts ``_latest_autosave_mtime(autosave_dir, named_mtime)`` from the
inline block in ``list_presets`` so the scan is unit-testable. The
helper walks ``periodic.json`` and every ``pre_load_*.json`` once,
returns the max mtime if strictly newer than the named file, else
``None``. Same shape as before, broader source set.

Introduces pytest as a first-class test runner:

  * ``tests/server/test_listing_autosave_mtime.py`` — 5 row-augment
    test cases (periodic-only, pre_load-only, max-of-both, no-newer,
    missing-dir).
  * ``[tool.pytest.ini_options]`` in ``pyproject.toml`` scopes
    discovery to ``tests/`` and uses ``importmode=importlib``.
  * ``[project.optional-dependencies] test = [...]`` defines the
    test extra (pytest + aiohttp, which is a runtime dep of
    ``koolook_routes`` that must be importable even when only a pure
    helper is under test).
  * ``.github/workflows/ci.yml`` gains a ``pytest`` job that installs
    via ``pip install .[test]`` and runs ``pytest -q``.

Guards ``__init__.py``'s relative imports in a ``try / except
ImportError / else`` block. Pytest's package collector walks up from
test files looking for ``__init__.py`` and tries to import the one at
the repo root — but the relative imports inside it only resolve under
ComfyUI's custom-node loader, not under a standalone Python import.
Without the guard, every pytest run aborts during collection with
``attempted relative import with no known parent package``. The
fallback emits a one-line diagnostic and registers empty mappings so
the package directory can still be imported by tooling. ComfyUI's
loader still hits the ``else`` branch and behaviour is unchanged for
real users.

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

* feat(snapshot): folder picker + agent-driven visual harness (#137)

Commit 2 of the snapshot dialogs redesign. Introduces the navigate-
into folder picker from mockup section 6 — the picker that opens when
the user clicks Save to… / Load from… in the redesigned dialogs (and
that the Settings cog will be removed in favour of, once commit 6
lands). Path input is the source of truth: type/paste + Enter
navigates, clicking a folder row drills in, ↑ Up climbs one level.
Files appear greyed below the directories as a "yes, this is the
folder I expected" affordance.

End-visible path overflow: the path input uses ``direction: rtl`` +
``unicode-bidi: plaintext`` so when the path exceeds the input width
the leading ``/Users/…`` clips on the left, never the trailing folder
name. Per mockup section 6.

Visual harness — the path the issue called out as "visual verification
is a hard gate":

  * ``.claude/launch.json`` defines ``design-server`` (``python3 -m
    http.server 8765``). Agent / maintainer can spin this up via
    ``preview_start`` or directly from a terminal.
  * ``docs/designs/_harness/folder-picker.html`` mounts the real
    ``showFolderPicker`` from ``web/sidebar/modals.js`` against a
    canned filesystem, with one trigger button per mockup-illustrated
    state (default / new-folder-input / empty / error). Cache-busts
    module imports so editing modals.js while the harness is open
    picks up fresh code on reload.
  * ``docs/maintainers/visual-harness.md`` documents the workflow,
    when to use the harness vs. dev-sync, and the backtick-in-CSS-
    comments pitfall that broke this commit twice during development.
  * ``CLAUDE.md`` now lists the harness as the preferred agent-driven
    path; dev-sync stays the integration-level gate.

Server: ``/koolook/presets/browse`` gains an opt-in ``?files=1``
parameter that returns child JSON files alongside the directories.
Default response shape unchanged so the legacy Settings dialog
keeps working. ``_list_child_files`` mirrors ``_list_child_dirs``'s
swallow-OSError-per-entry policy so a broken symlink mid-listing
doesn't sink the whole response.

Tests: six new pytest cases in
``tests/server/test_browse_list_children.py`` covering directory
sorting, autosave-subfolder hiding, JSON-only filter, case-insensitive
extension match, and the empty-result paths. ``pytest`` runs all 11
tests cleanly under the existing CI job from commit 1.

What's still verified via dev-sync (not via the harness):
  * Real Comfy theme tokens (the harness fakes ``--comfy-menu-bg``
    etc. with dark-theme defaults).
  * End-to-end Save → Load round-trip across the new library path.
  * Picker interaction with ComfyUI's keyboard manager.

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

* fix(snapshot): modal headers stay title-only; route gloss to tooltip (#137)

Follow-up to the folder picker commit. Maintainer flagged that the
"Where snapshots are saved and loaded from." subtitle below the title
was extra information the user doesn't need at first glance — if the
title isn't self-explanatory, fix the title, don't pad it with a
sentence. The mockup at docs/designs/snapshot-dialogs.html section 6
shows a title-only header for exactly this reason.

Locks the rule in as a project convention so every dialog box behaves
the same way going forward:

  * docs/maintainers/conventions.md — new "Modal headers" section
    spells out the rule, what counts as a "descriptive subtitle"
    (remove) vs. a "functional info row" (keep), and how to plumb
    explanatory copy via a hover tooltip on the title instead.
  * web/sidebar/modals.js — makeModalShell now accepts ``titleTooltip``,
    applied as the ``title`` attribute on the modal title element so
    the gloss is mouseover-discoverable but never visible by default.
    showFolderPicker drops its ``subtitle`` param (it was added one
    commit ago, no other callers) in favour of ``titleTooltip``.
  * docs/designs/_harness/folder-picker.html — harness re-points its
    explanatory text to ``titleTooltip``. Verified in the harness: the
    visible header is title-only, the tooltip surfaces on hover.

Applies prospectively to the Save / Load dialogs (commits 3 and 4) —
they will also be title-only.

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

* feat(snapshot): redesigned Save dialog with inline library row + Save to... + 4-state button (#137)

Commit 3 of the snapshot dialogs redesign. The Save dialog is now a
single modal (was three: three-way overwrite / input prompt / confirm)
that matches mockup section 2:

  * Inline library-path info row at the top of the body — "Saved to"
    label on the left, "Open folder ↗" link on the right, full path
    below. Pure information; every action lives in the bottom command
    bar.
  * Bottom command bar with split layout: Save to... on the left,
    Cancel + Save as new... + Save on the right (spacer in between).
  * Save to... opens the folder picker from commit 2. On Use this
    folder, the dialog calls saveSettings() to persist the new library
    path, refreshes its in-place library row, and flips the primary
    button into Dirty state.
  * The primary Save button cycles through the four states from
    mockup section 5:
      - Default ("Save") — clean state, primary blue.
      - Dirty ("Save to new folder") — folder differs from the one the
        dialog opened with; the label spells out the consequence.
      - In progress ("Saving...") — disabled, dimmed primary.
      - Done ("Saved") — disabled, subtle green, brief pause before
        the dialog closes.
  * Save as new... still routes through the existing showInputModal
    plus presetExists guard, but the prompt no longer carries a
    library-path subtitle — the parent dialog already shows the
    target. Convention from the previous commit applied.

tree.js now passes the new capability functions (saveSettings,
browseDirectories, createBrowseDirectory, revealPresetFolder) into
showSaveSnapshotDialog so the dialog can invoke the folder picker
end-to-end inside the Save flow.

Visual harness: docs/designs/_harness/save-dialog.html exercises all
five mockup states (default tracked, untracked Save-as-new, dirty,
in-progress, done) so the maintainer (or future agent) can re-verify
without dev-syncing. Verified all states render correctly in the
preview server.

Static guard against the recurring backtick-in-CSS-comments trap:
tests/sidebar/test_css_template_literal.py asserts that the CSS
template literal in web/sidebar/constants.js contains exactly two
backticks (open + close) and nothing in between. The trap bit this
work three times (commits 2 and 3) — a literal backtick inside a
comment closes the template early and silently bricks the entire
sidebar. The test runs under the same pytest job introduced in
commit 1.

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

* feat(snapshot): redesigned Load dialog with scoped recovery + inline delete (#137)

Commit 4 of the snapshot dialogs redesign. The Load dialog is now a
single modal matching mockup section 3:

  * Inline library row at the top, same shape as the Save dialog —
    "Loaded from" label + Open folder ↗ link + leaf folder name +
    full path. No 📂 emoji button (the link supersedes it).
  * Preset rows: click to load. The "Replace current state?" confirm
    modal is gone — clicking Load is the intent, the dialog title
    already says Load snapshot, no double-confirm.
  * Scoped recovery: when a preset has a newer autosave (per the
    server-side row-augment from commit 1), the first click DOES NOT
    load. It expands exactly ONE recovery row directly beneath the
    parent, showing the freshest entry (newest of periodic.json or
    pre_load_*.json) with its kind badge + meta line. The user then
    chooses: re-click the named row to load the named version, OR
    click the recovery row to restore the auto-save. Both options
    are surfaced inline; the user picks by clicking the one they
    want.
  * Inline delete confirm: × outlines the target row red, the bottom
    Close button transforms to 'Yes — delete "<name>"' (danger
    styling). A second click commits; Escape cancels and reverts.
    No second modal.
  * Bottom command bar matches mockup section 3 split layout:
    [Load from…] on the left, [Close] on the right. Load from…
    opens the folder picker from commit 2; on Use this folder it
    persists via saveSettings() and refreshes the listing.

The pre-#137 YES/NO choice modal (``promptApplyChoice``) and the
collapsible Recovery auto-saves <details> section are both removed —
the scoped-recovery inline row + the click flow above subsume their
job. (Commit 5 in the issue plan was specifically for dropping the
YES/NO modal, which happens here as a natural consequence of the
routing change; commit 5 will be a small follow-up if needed.)

Behavior preserved from the pre-#137 implementation:
  * doNamedLoad — pre-load auto-save backup chain, partial-failure
    tracker handling, identical toast messages.
  * doAutosaveRestore — same backup chain, same markStateAutosaved()
    semantics, same restored-name derivation from subdir.

Tree.js plumbs the three new caps (saveSettings, browseDirectories,
createBrowseDirectory) into showLoadSnapshotDialog so the dialog can
invoke the folder picker end-to-end.

Visual harness: docs/designs/_harness/load-dialog.html exercises
default, empty, scoped-recovery, and pending-delete states. Verified
all four render correctly in the preview server.

Net diff: -56 lines in the sidebar source (the redesign deletes more
than it adds because the YES/NO modal + collapsible recovery
disclosure + confirm modal for plain loads are all gone).

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

* docs(designs): reconcile snapshot redesign sketch with shipped behaviour (#137)

Commit 5 of the snapshot dialogs redesign. The code-level removal of
the autosave-newer YES/NO modal happened in commit 4 as a natural
consequence of the scoped-recovery inline row taking over its job;
this commit reconciles the design docs so they describe what was
actually built.

docs/designs/snapshot-dialogs-redesign.md:
  * "What changes" item 4 reworded — the YES/NO modal is dropped;
    the affordance is now an inline scoped recovery row that opens
    directly beneath the clicked preset.
  * "What changes" gains items 2, 3, 5 covering the Save/Load dialog
    redesigns and the inline delete confirm that landed in commits 3
    and 4.
  * Mermaid flow rewritten to reflect the new routing (preset click
    → scoped recovery expand → user picks named or recovery row),
    matching commit 4's onPresetClick implementation.

docs/designs/snapshot-dialogs.html "Locked-in decisions":
  * Updated the autosave-modal item from "modal head reads X" to
    "modal — dropped" and explains the inline scoped recovery row
    replacement.
  * Added the title-only modal header convention (commit 2 follow-up)
    pointing to docs/maintainers/conventions.md.
  * Added a pointer to the visual harness workflow added in commit 2.

No code changes — the YES/NO modal was already gone after commit 4.

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

* feat(snapshot): remove Settings cog + showSnapshotSettingsDialog + CHANGELOG (#137)

Final commit of the snapshot dialogs redesign. The Settings cog disappears
from the Snapshot toolbar — the snapshot row is now status indicator
| Load | Quick Save | Save, matching mockup section 1. Library-path
management now lives inside the Save dialog ("Save to…") and Load
dialog ("Load from…"), both of which open the folder picker introduced
in commit 2 and persist via the existing `saveSettings` route.

Removals:
  * `showSnapshotSettingsDialog` (~325 lines) — the dialog body, its
    inline directory browser, and the "save folder location" state
    machine. The same browse functionality lives in `showFolderPicker`
    now; the same persistence happens via `saveSettings()` called from
    inside Save / Load.
  * `showThreeWayDialog` (~45 lines) — only consumer was the pre-#137
    Save flow (overwrite / Save as new / Cancel), which the redesigned
    Save dialog absorbed into its primary action bar.
  * `renderLibraryLocation` (~17 lines) — replaced by inline rendering
    in the Save and Load dialogs' library rows.
  * Settings cog toolbar button + `openSettingsDialog` wiring in
    tree.js (~12 lines).
  * Orphan `.koolook-browse-*`, `.koolook-path-input-row`,
    `.koolook-load-library-row*`, `.koolook-settings-save-note` CSS
    rules in constants.js (~16 lines) — only consumed by the removed
    dialog. `.koolook-settings-folder-name` and `.koolook-settings-
    folder-path` stay; the new Save / Load library rows reuse them
    verbatim (git-blame continuity for the visual style).

Adds:
  * CHANGELOG.md [Unreleased] entry summarising the whole #137
    feature — picker, redesigned Save/Load dialogs, scoped recovery,
    inline delete, removed Settings cog, pytest infrastructure.

Net diff for this commit: -377 lines.

Cumulative PR diff (commits 1–6 + harness/convention follow-ups):
all six issue ACs satisfied. The maintainer should dev-sync once
before merging to confirm real-Comfy theme tokens render correctly
(harness fakes the dark-theme defaults) and end-to-end Save → Load
round-trips against the live preset folder behave.

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

* fix(ci): install pytest + aiohttp directly, not via pip install . (#137)

The Pytest job introduced in commit 1 of this PR was failing on CI
because ``pip install .[test]`` tries to build a wheel of the entire
project. setuptools's flat-layout discovery then trips on the
non-Python ``web/`` and ``forks/`` directories at the repo root and
errors out with "Multiple top-level packages discovered in a flat-
layout: ['web', 'forks']".

The project is a ComfyUI custom node loaded directly from its source
directory, not a pip-installable package. There's no wheel to build.
Install the test deps explicitly instead — the ``[project.optional-
dependencies] test`` block in pyproject.toml is the spec, CI mirrors
it by hand.

Worked locally because the venv install path went straight to
``pip install pytest aiohttp`` and never touched the project.

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

* fix(snapshot): untracked dirty state + Load dialog Escape listener leak (#137)

Two findings from the external review on PR #144:

**Blocker — Save dialog's primary action with no tracked preset + folder
changed via Save to...**

``updatePrimaryLabel`` was entering the "dirty" state whenever the
library path diverged from the original, regardless of whether a
tracked preset existed. The primary click handler then routed "dirty"
to ``doOverwrite(current)`` — and with ``current === null``,
``sanitizeName(null)`` would throw / silently coerce to an empty
string instead of asking the user for a name.

Fix: ``isDirty()`` only matters when there is something to overwrite.
With no tracked preset, the primary stays in ``default-new``
regardless of folder divergence. The Save to... button still updates
the library row, so the user has feedback the destination changed; the
eventual primary click then routes to the Save-as-new name prompt and
writes into the new folder cleanly.

Verified in the visual harness: opening Save as untracked, switching
the library to ``/Volumes/Shared/different-library`` via the picker,
then inspecting the primary button:

  * Label stays "Save" (was "Save to new folder" pre-fix)
  * Tooltip "Choose a name and write the snapshot." (was "Write into
    /Volumes/Shared/different-library.")
  * Library row still reflects the new path (saveSettings DID run)

**Major — Load dialog leaked a document keydown listener per session**

The inline-delete Escape-to-cancel handler was registered with
``document.addEventListener`` but never removed. Each open/close cycle
of the Load dialog accumulated another stale handler; long sessions
ended up firing the cancelDelete check on every Escape press across
every dialog (no-op because ``pendingDelete`` was null in closed
dialogs, but the listener was still alive and called).

Fix: capture the handler by name, attach on open, tear down on close
by wrapping ``overlay.remove`` (mirrors the pattern the modal shell
already uses at ``modals.js:52-66`` for its own Escape handler).

Verified by instrumenting ``document.{add,remove}EventListener`` and
opening + closing the Load dialog five times in a row: every keydown
listener added during the cycle was removed before the next cycle
opened. No accumulation.

CI gates: ruff, compileall, bandit (via preflight equivalent), pytest
12/12, preflight 4/4 — all green.

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-14 21:00:34 +02:00
k_flame_99andkflame99 41ff4de460 feat(workflows): add right-click update from selection or canvas (#133)
* feat(workflows): add context-menu update from current canvas

* feat(workflows): update from selection or canvas

* refactor(workflows): keep sidebar app access inside canvas_io

* fix(workflows): repair selection-update block syntax and duplication

* nit(workflows): clarify selection-unavailable toast path

---------

Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
2026-05-14 19:39:42 +02:00
malkuthroandClaude Opus 4.7 e87d111521 docs(designs): snapshot dialogs redesign mockup + Mermaid flow (WIP) (#136)
* docs(designs): snapshot dialogs redesign mockup + Mermaid flow (WIP)

Design exploration for consolidating the Snapshot Settings cog into the
Save and Load dialogs, plus the related autosave-newer fix. Discussion
artifacts only — no code changes.

- docs/designs/snapshot-dialogs.html: side-by-side mockups across six
  sections (toolbar, Save dialog, Load dialog with default / Recovery
  expanded / inline delete-confirm states, autosave-newer modal,
  button-state pattern, folder-browse picker)
- docs/designs/snapshot-dialogs-redesign.md: scope sketch + Mermaid flow
- docs/designs/README.md: register the two new design files in the index

Single-library design stays (documented intent per
docs/maintainers/curated-sidebar.md). Redesign only consolidates surface;
implementation lands in a separate PR after layout details are locked in.

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

* docs(designs): refine autosave-newer modal + single-row recovery groups

Two scoped tweaks to the snapshot dialogs mockup:

- Section 4 (autosave-newer): drop the "Replace current state?" question
  since the user already chose Load; promote "Auto-save is newer than
  the saved version" to the only head message. Render the two options
  as standard preset-rows (filename + picks/workflows/date meta) so the
  layout mirrors the Load dialog list. Buttons collapse to NO /
  YES — load latest on one row.
- Section 3 (recovery expanded): show only the newest row per
  <preset>_autosave/ group — whichever of Pre-load or Periodic is most
  recent — instead of listing both kinds. Annotation also notes the
  Recovery section auto-expands when a preset row is clicked.

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

* docs(designs): lock recovery behavior — hidden by default, one row per clicked preset

When the user clicks a preset row (Koolook_v03, starter, etc.) the recovery
section opens and reveals only that preset's <preset>_autosave/ folder,
with a single row inside — whichever of Pre-load or Periodic is newest
(the system picks). _unsaved_autosave/ no longer renders inside this
preset-scoped view since it isn't tied to the clicked preset.

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

* docs(designs): polish section 6 + recovery row layout

Folder-browse picker (section 6):
- Switch to standard navigate-into model — clicking a folder drills in,
  body lists that folder's contents (subfolders clickable, files greyed-
  out for context). Path input shows where you are; "Use this folder"
  commits the current path. Drops the sibling-list + selected-tag model.
- Single-line toolbar: ↑ Up next to the path input; secondary parent-
  path strip removed.
- Path input keeps the END visible when overflowing (direction:rtl +
  text-align:left), so a long path shows the meaningful trailing folder
  instead of /Users/… — and example path is now long enough to
  demonstrate it.
- Header restyled to match other modal heads (15px title, dark bg,
  bigger padding); descriptive subtitle removed.
- Bottom bar: New folder… stays left; Cancel + Use this folder push
  right (.picker-foot .spacer rule, since the existing flex-spacer was
  scoped to .modal-actions).
- Redundant "SELECTED FOLDER" stripe removed — selected-row blue accent
  is sufficient confirmation; folder list gains the freed vertical room.
- Example folder rename _unsaved_autosave -> default_autosave (label
  only — on-disk constant unchanged; rename is an open suggestion to
  drop the self-contradicting "unsaved autosave" term).

Recovery row (section 3):
- Layout: badge on top, single combined meta line below in order
  "<picks> · <workflows> · <date>" — matches preset-row shape so both
  rows read the same way.
- Padding bumped to 10px 12px so recovery-row and preset-row are the
  same box size; badge nudged 1px up + meta nudged 1px down for
  breathing room.
- Recovery section now scopes to the clicked preset only: orphan
  _unsaved_autosave/ group removed from the "Koolook_v03 clicked" view,
  since when a preset row is clicked the dropdown should only show that
  preset's auto-saves.

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

* docs: require visual verification before requesting design-driven review

When implementing a UI change that has a corresponding mockup in
docs/designs/ (e.g. snapshot-dialogs.html, sidebar-icon-proposals.html),
the design file is the spec. Before the work is brought to the
maintainer for review, the implementing agent must:

  1. Render the implementation in a real browser — dev-sync for runtime
     sidebar work, python3 -m http.server (or the Launch preview panel)
     for standalone HTML mockups.
  2. Screenshot the implemented UI at every state the mockup shows.
  3. Diff each screenshot against the mockup. Match it or document the
     deviation with a rationale.
  4. Only then request review.

The maintainer should never be the first set of eyes on rendered
output. Type checks and tests verify code correctness, not visual
fidelity to the design — if a mockup exists for the change, visual
verification is a hard gate, not optional polish.

Rule lives in three places, each hit from a different entry point:
- CLAUDE.md (loaded at every session start; canonical)
- docs/designs/README.md (discovered when opening the designs folder)
- docs/maintainers/dev-iteration-loop.md (step 4 in the iteration loop)

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

* skills(tdd): add project-specific TDD skill modeled on ForgeFlow's

When implementation work on docs/designs/ mockups begins, /tdd on this
repo should pick up a project-level skill that knows ComfyUI-Koolook's
actual machinery — not just generic TDD principles.

Two files:

- SKILL.md — keeps the global TDD philosophy (vertical slices,
  behaviour-not-implementation, refactor only after GREEN) and adds a
  project-specific tail covering:
  • code surface: where Python nodes vs sidebar JS vs design specs live
  • test surface: what CI runs today (ruff, compileall, bandit,
    preflight_release.py) and explicit guidance on introducing pytest /
    vitest "only when there's a behaviour that genuinely needs unit
    testing" — same PR as the first test, never speculative
  • the dev-sync iteration loop for runtime sidebar work
  • high-value test targets (node registration, route validation, VAE
    dispatch, snapshot row augmentation)
  • drift patterns specific to this codebase (vendored upstream into
    MAIN, renamed namespaced node IDs in saved workflows, hardcoded
    user paths, dev-sync triggered from an automated flow, …)
  • final scope-alignment check routing (clean → proceed; drift → show
    report, never revert)

- visual-verification.md — the secondary procedure for cycles whose
  assertion is CSS / layout / structural. Procedure split for the two
  shapes of work in this repo:
  • runtime sidebar (web/sidebar/*.js): dev-sync → restart ComfyUI →
    screenshot → diff against docs/designs/<feature>.html
  • standalone HTML mockups (docs/designs/*.html): Launch preview panel
    or python3 -m http.server (no new package installs per
    supply-chain rules)
  Plus a structured definition of what "diff against the mockup" means
  (spatial, hierarchy, states, chrome, text — not pixel-equality), so
  it isn't a vague gate.

Generic companions (deep-modules, mocking, refactoring, interface-
design, scope-check, tests) intentionally not copied — agents follow
the global ~/.claude/skills/tdd/ pointers in SKILL.md for those. Avoids
sync-drift on principle docs and keeps this skill focused on what's
genuinely project-specific.

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-14 17:43:55 +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
bf02db8ad8 feat(easy_image_batch): source_batch + source_frames list + selected_… (#134)
* feat(easy_image_batch): source_batch + source_frames list + selected_* outputs

Three-mode input system, two new "selected only" outputs, and an alpha
output convention that's explicit about its role.

Inputs
- source_batch (IMAGE, optional): pre-batched stack, e.g. straight from
  Load Video.
- source_frames (multiline STRING): VFX frame numbers separated by
  commas, newlines, spaces or any mix. Picks from source_batch and
  places at the same timeline position. Removes the 4-keyframe ceiling.
- placeholder_color: now offers Black / Gray / White (was Black / Gray).
- invert_alpha (BOOLEAN, default off = "inpaint"): flips alpha to
  compositing convention (selected=white) when on.
- image1 is now optional (you can drive the node entirely from
  source_batch + source_frames). At least one of image1 or source_batch
  must be connected.
- start_frame default changed from 0 -> 1 to match VFX 1-based numbering.

Mode rules
- Mode 1 -- per-slot images: connect imageN, set imageN_frame.
- Mode 2 -- source_batch + 4 slots: when imageN is unconnected,
  imageN_frame doubles as the source pick index AND timeline target.
- Mode 3 -- source_batch + source_frames list: when the list is
  non-empty, the 4 slots contribute ONLY where imageN is explicitly
  connected -- the imageN_frame defaults (0, 4, 8, 12) do NOT pick from
  source_batch in this mode, so the list fully controls the selection
  unless an imageN override is wired.
- Priority: source_frames list runs first; manual slots run after and
  override at the same target index. Output dedup is automatic
  (set-based).

Outputs
- image_batch (IMAGE): full timeline with placeholders. Unchanged.
- alpha_batch (MASK): renamed from mask_batch; same default convention
  (selected=0.0/black, empty=1.0/white) so existing wires work
  unchanged. Toggle invert_alpha for compositing-style.
- selected_image_batch (IMAGE, NEW): only the keyframes that landed,
  packed back-to-back in timeline order. Empty (length 0) if none
  placed.
- selected_frames (STRING, NEW): comma-separated VFX frame numbers,
  sorted ascending and deduped. Same format as the source_frames input
  -- round-trips into another easy_ImageBatch instance.

Docs
- Updated class docstring with input modes + worked example (surfaces
  in ComfyUI's node tooltip).
- New per-node user guide:
  docs/user_guide/nodes/koolook_image/easy_image_batch.md.
- docs/user_guide/README.md graduates easy_ImageBatch from "TBD" to a
  real link.
- CHANGELOG: Unreleased Added/Changed entries cover the full change set.

Backward compatibility
- Existing workflows wired to image_batch + mask_batch keep working;
  the second output slot position is unchanged.
- The 4-keyframe screenshot pattern (source_batch + 4 slots, no list)
  is unchanged in behaviour.
- start_frame default change affects only newly added nodes; saved
  workflows keep the value they were saved with.

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

* feat(easy_image_batch): cut window — rename start_frame -> cut_start_frame, fix source-pick math

The 2nd output of Easy Image Batch (Koolook) gets a built-in cut
window, replacing the workflow pattern of feeding the output through
an external ImageFromBatch (batch_index, length) slicer.

Conceptual rename
- `start_frame` -> `cut_start_frame` everywhere.
- The output now represents VFX frames
  [cut_start_frame .. cut_start_frame + total_frames - 1].
- Frames placed outside the cut window are dropped from the cut
  silently and listed in a single end-of-run summary log (replaces
  the previous per-frame "out of range" warnings).

Math fix (real bug)
The previous code conflated source pick index and output index under a
single `target_index = vfx - start_frame`. That happened to work when
start_frame=1 (source pick index = vfx - 1, by coincidence), but
produced a wrong source pick whenever start_frame != 1. Now:

    source pick index   = vfx_frame - 1                  # hardcoded VFX 1-based
    output (cut) index  = vfx_frame - cut_start_frame    # cut window

The two are validated independently:
- source-side range error -> per-frame warning ("VFX frame N doesn't
  exist in source_batch")
- output-side range -> silent drop, collected into the end-of-run
  summary

Defaults
- cut_start_frame=1 (was start_frame=1, so default behaviour for
  source_batch starting at VFX frame 1 is identical).
- total_frames default 16 -> 81 (matches typical VFX shot length).
- imageN_frame defaults 0/4/8/12 -> 1/5/9/13 (valid 1-based).
- imageN_frame min 0 -> 1 (frame 0 is invalid under VFX 1-based).

Console summary
Always logged so it's easy to confirm what the node produced. Shape:

    [easy_ImageBatch] cut window: frames 41..121 (81 frames).
    6 placed; 2 outside cut: 1, 27.

Docs
- Class docstring rewritten with the cut-window model + Examples A
  (cut_start_frame=1, no cut) and B (cut_start_frame=41, cut).
- docs/user_guide/nodes/koolook_image/easy_image_batch.md fully
  updated to match.
- CHANGELOG: new "Changed" entry under [Unreleased] explaining the
  rename, math fix, defaults bump, and migration notes.

Migration notes for users of the previous PR commit
- Saved workflows that had start_frame=1 (the default) keep producing
  identical output -- the math is unchanged in that case.
- Saved workflows that set start_frame to a non-1 value will see
  different source picks (the old code was actually picking the wrong
  source frame, the new code picks correctly). Re-check those.

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

* feat(easy_image_batch): shift imageN_frame defaults to 5/9/13/17

The previous 1/5/9/13 cluster put all four default keyframes inside the
first 13 frames of an 81-frame timeline. Shifting to 5/9/13/17 keeps the
same stride-of-4 but leaves a small gap at the start, which reads
cleaner in the node UI and gives a more representative spread of where
keyframes commonly land.

Updates the doc table and CHANGELOG bullet to match.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:37:49 +02:00
k_flame_99andkflame99 dc58e7b224 fix(snapshot-settings): make folder save explicit and non-destructive (#130)
* fix(snapshot-settings): make folder save explicit and non-destructive

* docs(changelog): note snapshot-settings folder-save behavior fix

---------

Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
2026-05-13 19:19:45 +02:00
malkuthro 3b914d4eaa Add Comfy Registry download tracker (#127) 2026-05-10 22:18:54 +02:00
malkuthro e1f0bfca33 docs: require confirmation for external repo targets (#125) 2026-05-10 21:57:18 +02:00
malkuthro ea188a4c70 Release v0.3.1: KForge spotlight onboarding (#126) v0.3.1 2026-05-10 21:51:41 +02:00