* feat(matte): first-party mask-guided one-step video matting nodes
Adds matte/ — an independent implementation of the published VideoMaMa
one-step SVD method (matte/pipeline.py) plus Koolook's temporal windowing,
subject-focused crop/stitch, resolution control (/64, shortest-side) and
auto OOM resolution-backoff. Nodes: KoolookMatteLoader, KoolookMatteSampler,
KoolookMatteFocusCrop, KoolookMatteFocusStitch.
Registered via the existing guarded _merge_node_group("Koolook Matte",
".matte"), so a missing diffusers disables only these nodes. diffusers is
already pulled in by the radiance/whatdreamscost forks, so no new dependency.
Loads (never bundles) the VideoMaMa UNet (CC-BY-NC) + SVD VAE at runtime;
matte/NOTICE records the model licenses and user responsibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(dev-sync): include matte/ in RUNTIME_PATHS
So `sync_to_dev` copies the new matte node package into the live ComfyUI
install alongside the rest of the koolook pack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matte): raw cond latents; fp16 default; preserve checkpoints on dev-sync
pipeline.py: _encode_latents must NOT divide by vae scaling_factor. The
VideoMaMa UNet is trained on raw VAE latents (matching okdalto's
VideoInferencePipeline, which multiplies by scaling_factor then divides it
back out -> net x1). Dividing over-scaled the conditioning ~5.5x and produced
a systematically blurry matte on every frame. Verified 5/5 vs the desktop /
vanilla reference on identical single-frame inputs.
nodes.py: harden the loader precision fallback so fp16 is the true default -
only an explicit "bf16" opts out; a stale "auto" widget now resolves to fp16
instead of bf16.
sync_to_dev.py: the rmtree+copytree mirror deleted matte/checkpoints/ (the
gitignored ~10GB SVD+VideoMaMa weights) on every matte sync, forcing a
re-download. Add PRESERVE_IN_DEST + selective _clean_dest so runtime-
downloaded model data survives a sync.
docs: add docs/maintainers/matte-pipeline.md (latent-scaling invariant,
black-output causes, fidelity levers, single-frame + vanilla-oracle debug
loop, checkpoint preservation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matte): address PR #269 review
- license: align matte to the repo's GPL-3.0 (was self-contradictory: claimed
"MIT licensed - see LICENSE" while LICENSE is GPL-3.0). pipeline.py header,
NOTICE, and the maintainer doc updated. NOTICE still credits VideoMaMa
(CC BY-NC) + SVD (Stability Community License).
- nodes.py _ensure_2d_mask: squeeze first, then handle [H,W,C]/[C,H,W]
explicitly and RAISE on ambiguous shapes -- previously [H,W,3] silently
became [W,3] (wrong geometry).
- nodes.py OOM backoff: catch RuntimeError (OutOfMemoryError subclasses it, but
cuBLAS/cuDNN/older paths raise plain "RuntimeError: CUDA out of memory"); the
existing message guard still filters non-OOM errors.
- nodes.py: warn when the guide mask is empty instead of silently falling back
to full-frame.
- sync_to_dev.py: PRESERVE_IN_DEST is now an exact relative path
("matte/checkpoints"), not a bare name matched at any depth; handle symlinks
before is_dir() in _clean_dest so a symlinked dir isn't recursed/rmtree'd.
- CHANGELOG: add the Koolook Matte nodes under [Unreleased] with the
non-commercial model-license caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matte): address PR #269 re-review
- sync_to_dev.py [BLOCKING]: a top-level symlinked dest (e.g. target/matte -> X)
was passed straight into _clean_dest()/copytree(), which follow the link and
can mutate files outside the target. Now unlink a symlinked dest and own the
path as a real dir (checked before exists(), which follows symlinks). Verified
with a repro: the symlink target's file survives.
- nodes.py: force float32 on a CPU fallback (CUDA unavailable). Half precision
is unreliable on CPU; fp16 kernels would error. Warn that CPU is very slow.
- docs: correct the preserved-path guidance to the target-relative
"matte/checkpoints" (was a bare "checkpoints" name); reword "near-verbatim
reimplementation" -> "independent implementation targeting behavioural parity"
to match the NOTICE/pipeline framing.
- nodes.py: also warn on a degenerate (<8px) guide-mask region before the
full-frame fallback (was silent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(matte): address PR #269 re-review (CI red + FocusCrop frame check)
- nodes.py: KoolookMatteFocusCrop now validates images vs coarse_mask frame
counts (raises like KoolookMatteSampler.run_inference). A mismatch previously
made cropped_images/cropped_mask diverge and silently drop/misalign frames at
FocusStitch.
- pipeline.py: split "vae.to('cpu'); _clear_cache(...)" onto two lines (Ruff
E702 was failing Lint & Smoke).
- constraints-test.txt: bump pillow 12.2.0 -> 12.3.0 (pip-audit CVE, fixed in
12.3.0). Pillow is a leaf dep so the closure is unaffected; CI re-audits.
- README: note the Koolook Matte nodes need diffusers (already via the LTX /
Radiance forks) and huggingface_hub for runtime weight downloads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: AI Machine <ai.machine@x.baconcph.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
8.2 KiB
Python
172 lines
8.2 KiB
Python
"""ComfyUI-Koolook node registry.
|
|
|
|
@title: ComfyUI-Koolook
|
|
@nickname: Kforge Labs
|
|
@description: Project-kit sidebar plus Koolook nodes for VFX, image, and video workflows.
|
|
|
|
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
|
|
# sibling ``koolook_install_guard`` module so the logic can be unit-tested
|
|
# from ``tests/test_install_guard.py`` without needing the relative-import
|
|
# parent-package context that ComfyUI's custom-node loader supplies (and
|
|
# pytest does not). See that module's docstring for the failure mode this
|
|
# guards against; the short version is: two Koolook folders in
|
|
# ``custom_nodes/`` race over ``/userdata/koolook_workflows.json`` and
|
|
# silently corrupt it.
|
|
#
|
|
# Import shape mirrors ``koolook_routes`` below — relative inside the
|
|
# package, absolute as a fallback so an out-of-package test harness can
|
|
# still load this file as a script.
|
|
try:
|
|
from .koolook_install_guard import ( # noqa: E402
|
|
build_duplicate_report,
|
|
detect_duplicate_koolook_installs,
|
|
)
|
|
except ImportError: # pragma: no cover - exercised only outside package context
|
|
from koolook_install_guard import ( # type: ignore[no-redef]
|
|
build_duplicate_report,
|
|
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
|
|
if "__path__" not in globals():
|
|
# Registry scanners commonly load custom-node entrypoints directly from
|
|
# ``__init__.py`` instead of as an installed package. Mark this module as a
|
|
# package so the same relative imports ComfyUI uses can still resolve.
|
|
__path__ = [str(_here)] # type: ignore[var-annotated]
|
|
__package__ = __name__
|
|
if __spec__ is not None:
|
|
__spec__.submodule_search_locations = [str(_here)]
|
|
|
|
# The guard must never take down the plugin: any failure to scan or parse
|
|
# siblings degrades to "register normally" (fail-safe) instead of aborting
|
|
# the import (fail-silent — the plugin would vanish entirely, taking the
|
|
# alphabetical winner with it). detect_/read_ already swallow per-entry
|
|
# OSError/ValueError; this try is the final backstop for anything unforeseen.
|
|
_is_winning_install = True
|
|
try:
|
|
_siblings = detect_duplicate_koolook_installs(_here)
|
|
if _siblings:
|
|
_is_winning_install, _report = build_duplicate_report(_here, _siblings)
|
|
print(_report)
|
|
except Exception as _guard_err: # pragma: no cover - defensive backstop
|
|
print(
|
|
f"[Koolook] install-guard check failed ({_guard_err!r}); "
|
|
"registering normally."
|
|
)
|
|
_is_winning_install = True
|
|
|
|
|
|
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. 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 . import koolook_versioning as _context_probe # noqa: F401
|
|
except ImportError as _context_exc:
|
|
print(
|
|
f"[Koolook] node registry skipped (non-Comfy import context): "
|
|
f"{_context_exc}"
|
|
)
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
else:
|
|
# 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")
|
|
_merge_node_group("Koolook Loop Status", ".k_loop_status")
|
|
_merge_node_group("Koolook Publish Contract", ".k_publish_contract")
|
|
_merge_node_group("LTX A/V Bind Schedule", ".k_ltx_av_bind_schedule")
|
|
_merge_node_group("Clean Latent Slice", ".k_clean_latent_slice")
|
|
_merge_node_group("LTX Guide Reference Strength", ".k_ltx_guide_reference_strength")
|
|
_merge_node_group("LTX Reference Bind Schedule", ".k_ltx_reference_bind_schedule")
|
|
_merge_node_group("LTX Keyframe Soften Schedule", ".k_ltx_keyframe_soften_schedule")
|
|
# 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")
|
|
# First-party matte nodes. Load diffusers (already present via the forks
|
|
# above); the guard skips them cleanly if diffusers is ever absent.
|
|
_merge_node_group("Koolook Matte", ".matte")
|
|
|
|
# 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
|
|
|
|
if not koolook_routes.install():
|
|
print(
|
|
"[Koolook] PromptServer unavailable at import time; preset routes "
|
|
"(/koolook/presets/*) not registered for this session."
|
|
)
|
|
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"]
|