Files
ba37ec3d41 feat: LTX Director 2.0.2 fork, keyframe grid-snap, and audio→video bind schedule (#259)
* feat(forks): LTX Director v2_0_2 fork + keyframe latent-grid snap (#258)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: gitignore root-level generated workflow JSONs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: mark vendored WhatDreamsCost fork files linguist-vendored

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:15:41 +02:00
..

Automations

AI-managed ComfyUI iteration loops. Each automation module is one self-contained workflow: own ComfyUI session, own workflow JSON, own narrative, own findings — typically focused on one specific generation task (a single-stage render path, an audio-driven lip-sync, an upscale recipe, etc.). The same diffusion model can host several modules side by side; one folder per task, not one per model.

The agent's role inside each module is the same: watch the maintainer's workflow edits, score feedback, render tracking cards via /make-card, and — when the task needs it — modify code in forks/ or scripts/.

Want the visual walkthrough? → index.html presents the setup and loop as an HTML overview.

Already know the loop? → CHEATSHEET.md (one page, dense bullets).

Where things live

In the repo

ComfyUI-Koolook/
├── .env                                     gitignored — your machine's config
│                                            (KOLOOK_AUTOMATIONS_WORK_DIR=…)
├── .env.example                             committed template — documents the var
├── .claude/skills/make-card/
│   └── SKILL.md                             the `/make-card` slash command
├── scripts/
│   ├── make_card.py                         renders a card PNG from workflow JSON
│   └── watch_cards.py                       optional polling watcher
├── forks/                                   ← any modified upstream node code an
│                                            automation needs (e.g. whatdreamscost_koolook/)
└── docs/automations/
    ├── README.md                            you are here
    ├── CONVENTIONS.md                       cross-cutting loop contract
    ├── CHEATSHEET.md                        one-page reminder
    └── LTX-2.3/                             grouping by diffusion model
        ├── base-1step/                       ← one automation module
        │   ├── README.md                     loop entry point for this task
        │   ├── handoff-checklist.md
        │   ├── findings.md                   locked-in conclusions for THIS task
        │   ├── backstory/                    narrative that produced the module
        │   └── example-cards/                rendered cards from past runs
        └── audio-lipsync/                    ← another automation module, same model
            ├── README.md
            ├── handoff-checklist.md
            ├── findings.md
            ├── backstory/
            └── runs/                         per-render snapshots in the repo
                ├── LOOP.md                   per-task iteration protocol
                └── log.md                    rolling table of runs

The model-name folder (LTX-2.3/) is only a grouping — it doesn't carry shared docs of its own. Each automation under it is independent (own README, own findings). Model-architecture facts that genuinely apply to every module on that model can live in any one module's findings.md and be cross-linked from siblings (or lifted to a shared file later if a third module materialises and the duplication is real).

Per-project working folder (lives outside the repo)

Pointed at by KOLOOK_AUTOMATIONS_WORK_DIR in .env. One folder per project. Holds:

<working folder>/                            e.g. JOBS/<client>/<scene>/ComfyUI-working-folder
├── <workflow>.json                          ← user content — ComfyUI workflow export
├── <workflow>.mp4                           ← user content — rendered video
├── …more JSONs/MP4s as you iterate
└── _AI/                                     ← AI-managed — written by make_card.py
    ├── card.png                             stable tracking card (overwritten each run)
    └── iterations.md                        append-only log, one row per render

The _AI/ split lets you wipe agent artifacts without touching user content (or vice-versa). You can delete _AI/ to reset the history; you can delete the JSONs and MP4s without losing the iteration log.

For automations that ship in-repo runs/ snapshots (the audio-lipsync module does this — because the iteration touches fork code, not just widgets), the snapshots live inside the module folder. The _AI/iterations.md outside-the-repo log is still the authoritative running record; the in-repo runs/log.md is the condensed cross-machine summary.

What lives here

Module Model Status Task
LTX-2.3/base-1step/ LTX 2.3 active Single-stage render: brief once, sample 8 steps, no Phase-2 upscale. Model-generated audio.
LTX-2.3/audio-lipsync/ LTX 2.3 active Audio-file-driven lip-sync. Modified LTXDirector (Prompt-Relay paper σ + relay_overrides widget). Ships a fork at forks/whatdreamscost_koolook/.

The pattern (every module looks like this)

  1. One working folder per project holds everything for the iteration: workflow JSON + rendered video at the root (user content), plus an _AI/ subfolder for the agent-managed card + iterations log.
  2. Three named Text Multiline nodes inside the ComfyUI workflow form the data contract:
    • Working_Folder_PATH — authoritative output path.
    • OVERLAY - INFO — the run params summary + free-text BASE (notes): for what changed this run.
    • OVERLAY - FEEDBACK — observations on the rendered video + score lines (motion: 4/5, sync: 5/5, sharp: 4/5).
  3. The /make-card skill (or scripts/make_card.py directly) reads the latest JSON in the working folder, extracts every parameter, renders _AI/card.png, and appends a row to _AI/iterations.md. Cards travel alongside the video into the NLE for side-by-side comparison.
  4. Code modifications go in forks/ when a module needs to alter an upstream node. The agent edits the fork file in the repo; the maintainer runs dev-sync to copy it into the live install. See ../../forks/README.md.
  5. Findings get promoted from per-run notes into the module's findings.md when a knob's behaviour is stable enough to lock in (≥ 3 confirming runs).

Full cross-cutting contract: CONVENTIONS.md.

Starting a new automation module

  1. Pick a folder name — short, lowercase, hyphen-separated, descriptive of the task (e.g. LTX-2.3/two-stage-upscale/, wan-2.1/i2v-from-still/). The model-name grouping is optional but recommended once there are 2+ modules on the same model.
  2. Copy LTX-2.3/base-1step/README.md and handoff-checklist.md to the new folder as templates.
  3. Rewrite the task scope paragraph at the top. Replace task-specific node names, parameter sets, and finding placeholders with the new module's.
  4. Add a row to the "What lives here" table above.
  5. If the module needs to modify upstream node code, run the add-external-fork skill first to register the fork properly; then the module can reference forks/<name>_koolook/.
  6. The cross-cutting tooling (scripts/make_card.py, .claude/skills/make-card/) is module-agnostic — no changes needed there unless the new module needs different fields extracted from its workflow.