From 447465ecd817829bba16f4b2c36fbc91d9555350 Mon Sep 17 00:00:00 2001 From: malkuthro <166633396+malkuthro@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:47:25 +0200 Subject: [PATCH] refactor(nodes): remove superseded LTX Director extra nodes + harden registry (#198) (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 40 +- __init__.py | 192 +++--- .../LTX-2.3/audio-lipsync/CHEATSHEET.md | 57 +- .../LTX-2.3/audio-lipsync/README.md | 45 +- k_audio_timeline.py | 588 ------------------ k_timeline_editor.py | 61 -- pyproject.toml | 3 - scripts/sync_to_dev_audio.py | 16 +- scripts/transcribe_audio_timeline.py | 247 -------- tests/scripts/test_sync_to_dev.py | 78 +++ tests/scripts/test_sync_to_dev_audio.py | 16 + .../scripts/test_transcribe_audio_timeline.py | 418 ------------- web/whatdreamscost_koolook/ltx_director.js | 3 +- 13 files changed, 223 insertions(+), 1541 deletions(-) delete mode 100644 k_audio_timeline.py delete mode 100644 k_timeline_editor.py delete mode 100644 scripts/transcribe_audio_timeline.py create mode 100644 tests/scripts/test_sync_to_dev.py delete mode 100644 tests/scripts/test_transcribe_audio_timeline.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6918a57..e805395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,23 @@ The format is inspired by Keep a Changelog and SemVer. (Koolook)` for a second workflow stage without parsing VHS's mixed JSON/video list. +### Removed +- **Standalone audio-transcript tooling.** Removed the experimental + `KoolookAudioTranscriptTimeline` / `KoolookTimelineEditor` nodes, the + `scripts/transcribe_audio_timeline.py` CLI, and the `[audio]` + (`faster-whisper`) optional dependency — superseded by the Koolook + Director's built-in `audio_transcript_json` input (#198). Anyone using + `pip install -e '.[audio]'` or that CLI out-of-tree should pin a + pre-removal commit. + ### Fixed +- **Node registry survives one broken or missing module.** The root + `__init__.py` now imports each node group independently and installs the + Kforge Labs snapshot/preset routes (`/koolook/presets/*`) regardless of + node-import results. Previously a single missing or failing node module + raised at import time and zeroed *every* Koolook node **and** the snapshot + routes at once (sidebar disk browse/save returning 404). A new guard test + keeps every root `k_*.py` listed in dev-sync's `RUNTIME_PATHS`. - **`loop-audio`: Director card reads execution-active values.** The audio-lipsync card now reads Director widgets from the saved Director widget order before falling back to generic input-name lookup, fixing @@ -39,14 +55,6 @@ The format is inspired by Keep a Changelog and SemVer. audio sync now removes the old `web/whatdreamscost_koolook_v1_3_2/` folder after the stable v1.3.9 web-extension rename, preventing legacy workflows from loading two identical timeline editors in dev installs. -- **Audio Transcript Timeline now follows every timeline audio clip.** When - the node receives `timeline_data` from Koolook Timeline Editor it now - transcribes all `audioSegments`, applies each clip's timeline start, - trim start, and visible length, then merges the phrases into one ordered - Prompt Relay timing sequence. This fixes separated audio clips where only - the first file affected `local_prompts` / `segment_lengths`. Generated - speech/pause prompts now also include the active image segment's prompt, - with a first-non-empty timeline prompt fallback for empty image segments. - **LTX Director timeline images persist after canvas/setup switches.** The timeline editor keeps saved workflow payloads light by omitting preview-only image blobs, but now rebuilds image previews from the persisted `imageFile` @@ -224,18 +232,10 @@ The format is inspired by Keep a Changelog and SemVer. *Loop* and *Run* entries are rewritten to point at the new structure. ### Added -- **Koolook Audio Transcript Timeline node for custom-audio sync tests.** - New optional `.[audio]` dependency group installs `faster-whisper`, and - the new `Koolook Audio Transcript Timeline` Comfy node converts a speech - file into Director-shaped `timeline_data`, `local_prompts`, and - `segment_lengths` with explicit closed-mouth pause segments. The - Koolook Director now also accepts the node's `transcript_json` directly - via `audio_transcript_json` and builds those Director fields internally - before Prompt Relay conditioning runs. The companion - `scripts/transcribe_audio_timeline.py` produces the same data from the - command line. This gives the audio-lipsync loop a repeatable way to test - whether LTX needs semantic speech timing in addition to the raw - custom-audio latent. +- **Koolook Director accepts an audio transcript for speech timing.** The + Koolook LTX Director gained an `audio_transcript_json` input that converts + phrase-timed transcript JSON into Director `timeline_data`, `local_prompts`, + and `segment_lengths` internally before Prompt Relay conditioning runs. - **`forks/whatdreamscost_koolook/v1_3_9/` — updated Koolook LTX Director fork.** Upgrades the Koolook Director to upstream WhatDreamsCost-ComfyUI `3b65410` (pyproject/README version `1.3.9`; no `v1.3.9` git tag at fork time). diff --git a/__init__.py b/__init__.py index f31990d..22308a0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,21 @@ +"""ComfyUI-Koolook node registry. + +Two layers of resilience guard the import: + +1. Duplicate-install guard (#162) — if two Koolook folders exist in + ``custom_nodes/``, only the alphabetical winner registers; the loser + short-circuits before any heavy imports or route install. +2. Per-group registration (#198 / #183) — each node group is imported + independently so a single missing or broken module degrades to "that + node absent" instead of taking down the whole pack, and the Kforge Labs + snapshot/preset routes install regardless of node-import results. The + failure this guards against: a module that ``__init__`` imported but + ``dev-sync`` never copied raised ``ImportError`` at startup, which zeroed + every node *and* 404'd every snapshot route. +""" +from __future__ import annotations + +import importlib from pathlib import Path # Duplicate-install guard (#162). Detection + winner selection live in the @@ -23,6 +41,29 @@ except ImportError: # pragma: no cover - exercised only outside package context detect_duplicate_koolook_installs, ) +NODE_CLASS_MAPPINGS: dict = {} +NODE_DISPLAY_NAME_MAPPINGS: dict = {} + + +def _merge_node_group(label: str, module_name: str) -> None: + """Import one node group's mappings, isolating any import failure. + + A missing module, a broken import inside it, or an absent + ``NODE_CLASS_MAPPINGS`` / ``NODE_DISPLAY_NAME_MAPPINGS`` attribute is + caught and logged; the rest of the registry (and the route install + below) is unaffected. + """ + try: + module = importlib.import_module(module_name, __package__) + class_mappings = module.NODE_CLASS_MAPPINGS + display_mappings = module.NODE_DISPLAY_NAME_MAPPINGS + except (ImportError, AttributeError) as exc: + print(f"[Koolook] {label} registry skipped: {exc}") + return + NODE_CLASS_MAPPINGS.update(class_mappings) + NODE_DISPLAY_NAME_MAPPINGS.update(display_mappings) + + _here = Path(__file__).resolve().parent # The guard must never take down the plugin: any failure to scan or parse # siblings degrades to "register normally" (fail-safe) instead of aborting @@ -44,91 +85,51 @@ except Exception as _guard_err: # pragma: no cover - defensive backstop if not _is_winning_install: - # Non-winning duplicate (#162). Short-circuit BEFORE any of the - # heavy node imports (torch, PIL, diffusers via the radiance/ - # whatdreamscost forks) or the aiohttp route install — the winner - # owns those for this session and there's no point paying their - # import cost just to discard the mappings afterward. Critical log - # was already printed by ``build_duplicate_report`` above. - NODE_CLASS_MAPPINGS = {} - NODE_DISPLAY_NAME_MAPPINGS = {} + # Non-winning duplicate (#162). Short-circuit BEFORE any of the heavy + # node imports (torch, PIL, diffusers via the radiance/whatdreamscost + # forks) or the aiohttp route install — the winner owns those for this + # session. No WEB_DIRECTORY either, so the loser doesn't double-register + # the sidebar web assets. Critical log already printed above. __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] else: + # Probe for a real package context. ComfyUI's custom-node loader provides + # one; pytest's package collector and static-analysis tools import this + # file as a standalone module, where relative imports raise. In that case + # we register nothing (a single message) and let tooling import the + # package directory without crashing. ``koolook_versioning`` is a + # pure-stdlib helper, so the probe can only fail on the context, never on + # its body. try: - from .k_easy_wan22_prompt import NODE_CLASS_MAPPINGS as wan_mappings - from .k_easy_wan22_prompt import NODE_DISPLAY_NAME_MAPPINGS as wan_display - - from .k_easy_resize import NODE_CLASS_MAPPINGS as resize_mappings - from .k_easy_resize import NODE_DISPLAY_NAME_MAPPINGS as resize_display - - from .k_ai_pipeline import NODE_CLASS_MAPPINGS as pipeline_mappings - from .k_ai_pipeline import NODE_DISPLAY_NAME_MAPPINGS as pipeline_display - - from .k_easy_image_batch import NODE_CLASS_MAPPINGS as batch_mappings # Added for new VFX batch node - from .k_easy_image_batch import NODE_DISPLAY_NAME_MAPPINGS as batch_display # Added for new VFX batch node - - from .k_easy_track import NODE_CLASS_MAPPINGS as cam_loader_mappings - from .k_easy_track import NODE_DISPLAY_NAME_MAPPINGS as cam_loader_display - - from .k_easy_pattern import NODE_CLASS_MAPPINGS as pattern_mappings - from .k_easy_pattern import NODE_DISPLAY_NAME_MAPPINGS as pattern_display - - from .k_easy_utility import NODE_CLASS_MAPPINGS as utility_mappings - from .k_easy_utility import NODE_DISPLAY_NAME_MAPPINGS as utility_display - - from .k_audio_timeline import NODE_CLASS_MAPPINGS as audio_timeline_mappings - from .k_audio_timeline import NODE_DISPLAY_NAME_MAPPINGS as audio_timeline_display - - from .k_timeline_editor import NODE_CLASS_MAPPINGS as timeline_editor_mappings - from .k_timeline_editor import NODE_DISPLAY_NAME_MAPPINGS as timeline_editor_display - - from .forks.radiance_koolook import NODE_CLASS_MAPPINGS as radiance_koolook_mappings - from .forks.radiance_koolook import NODE_DISPLAY_NAME_MAPPINGS as radiance_koolook_display - - from .forks.whatdreamscost_koolook import NODE_CLASS_MAPPINGS as whatdreamscost_koolook_mappings - from .forks.whatdreamscost_koolook import NODE_DISPLAY_NAME_MAPPINGS as whatdreamscost_koolook_display - except ImportError as _node_import_exc: - # The relative imports above require a parent-package context that - # only ComfyUI's custom-node loader provides. Pytest's package - # collector (which walks up from test files looking for ``__init__.py``) - # and static-analysis tools load this file as a standalone module, - # where the imports raise. Falling back to empty registries lets - # tooling import the package directory without crashing — tests - # access sibling modules (e.g. ``koolook_routes``) via absolute - # imports that don't need the package context. + from . import koolook_versioning as _context_probe # noqa: F401 + except ImportError as _context_exc: print( f"[Koolook] node registry skipped (non-Comfy import context): " - f"{_node_import_exc}" + f"{_context_exc}" ) - NODE_CLASS_MAPPINGS = {} - NODE_DISPLAY_NAME_MAPPINGS = {} __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] else: - # VHS-dependent wrappers self-skip when VideoHelperSuite is absent, and - # these guards keep a local import surprise from hiding the core nodes. - video_load_mappings = {} - video_load_display = {} - try: - from .k_video_load import NODE_CLASS_MAPPINGS as video_load_mappings - from .k_video_load import NODE_DISPLAY_NAME_MAPPINGS as video_load_display - except ImportError as _exc: - print(f"[Koolook] Easy_LoadVideo registry skipped: {_exc}") + # Order is cosmetic (menu grouping); each group is independent. + _merge_node_group("Wan 2.2 Easy Prompt", ".k_easy_wan22_prompt") + _merge_node_group("Easy Resize", ".k_easy_resize") + _merge_node_group("Easy AI Pipeline", ".k_ai_pipeline") + _merge_node_group("Easy Image Batch", ".k_easy_image_batch") + _merge_node_group("Koolook Camera Loader", ".k_easy_track") + _merge_node_group("Easy Pattern", ".k_easy_pattern") + _merge_node_group("Easy Utility", ".k_easy_utility") + # The VHS-dependent video wrappers degrade to empty mappings when + # VideoHelperSuite is absent (they catch the import internally); the + # guard also covers a hard ImportError should one ever slip through. + _merge_node_group("Easy Load Video", ".k_video_load") + _merge_node_group("Easy Video Combine", ".k_video_combine") + _merge_node_group("Radiance Koolook VAE", ".forks.radiance_koolook") + _merge_node_group("WhatDreamsCost LTX Director", ".forks.whatdreamscost_koolook") - video_combine_mappings = {} - video_combine_display = {} - try: - from .k_video_combine import NODE_CLASS_MAPPINGS as video_combine_mappings - from .k_video_combine import NODE_DISPLAY_NAME_MAPPINGS as video_combine_display - except ImportError as _exc: - print(f"[Koolook] Easy_VideoCombine registry skipped: {_exc}") - - # Register the Kforge Labs snapshot/preset endpoints. Failure here is - # non-fatal — the node mappings still load and the rest of the plugin - # works; only the snapshot feature in the sidebar is unavailable. Both - # the import itself AND the `install()` call are guarded: a missing - # `aiohttp` (extremely unusual since it's a ComfyUI hard-dep) or any - # other module-load exception falls through to the print without - # crashing the node registry. + # Register the Kforge Labs snapshot/preset endpoints. Installed + # regardless of the node-import results above, so a broken node + # module can no longer take the /koolook/presets/* routes down with + # it. Both the import and the install() call are guarded: a missing + # aiohttp (a ComfyUI hard-dep) or any other load error degrades to + # "snapshot feature unavailable", never a crash. try: from . import koolook_routes # noqa: E402 @@ -137,40 +138,11 @@ else: "[Koolook] PromptServer unavailable at import time; preset routes " "(/koolook/presets/*) not registered for this session." ) - except Exception as _exc: # pragma: no cover - print(f"[Koolook] preset routes import failed; node mappings unaffected: {_exc}") - - NODE_CLASS_MAPPINGS = { - **wan_mappings, - **resize_mappings, - **pipeline_mappings, - **batch_mappings, - **cam_loader_mappings, - **pattern_mappings, - **utility_mappings, - **audio_timeline_mappings, - **timeline_editor_mappings, - **video_load_mappings, - **video_combine_mappings, - **radiance_koolook_mappings, - **whatdreamscost_koolook_mappings, - } - - NODE_DISPLAY_NAME_MAPPINGS = { - **wan_display, - **resize_display, - **pipeline_display, - **batch_display, - **cam_loader_display, - **pattern_display, - **utility_display, - **audio_timeline_display, - **timeline_editor_display, - **video_load_display, - **video_combine_display, - **radiance_koolook_display, - **whatdreamscost_koolook_display, - } + except Exception as _routes_exc: # pragma: no cover + print( + f"[Koolook] preset routes import failed; node mappings unaffected: " + f"{_routes_exc}" + ) WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/docs/automations/LTX-2.3/audio-lipsync/CHEATSHEET.md b/docs/automations/LTX-2.3/audio-lipsync/CHEATSHEET.md index bf9cb56..f3ef5e2 100644 --- a/docs/automations/LTX-2.3/audio-lipsync/CHEATSHEET.md +++ b/docs/automations/LTX-2.3/audio-lipsync/CHEATSHEET.md @@ -19,7 +19,6 @@ look as clean as baseline. | Phrase | Script | What it does | |---|---|---| | `loop-audio` | [`scripts/loop_audio.py`](../../../../scripts/loop_audio.py) | Snapshots the most recent render into `runs/run-NNN_