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
2026-05-16 01:25:39 +02:00
2025-09-28 14:48:07 +02:00

ComfyUI-Koolook

Kforge Labs for ComfyUI: a project-kit sidebar plus Koolook nodes for VFX, image, and video workflows.

Start here: KForge Labs ComfyUI-Koolook overview

That overview explains the sidebar, snapshots, saved workflows, modules, recovery tools, and included nodes for new users.

The main experience is the Kforge Labs sidebar. It helps you keep a project setup together: favorite nodes, saved workflows, reusable modules, tags, snapshots, and recovery tools.

Kforge Labs Sidebar

Use the sidebar as a reusable project kit.

Feature What it does
Snapshots Save and load the whole Kforge Labs state: favorite nodes, workflows, modules, tags, archive, and recovery data.
Search Find nodes, workflows, directories, and tags from one field. Tags keep exact capitalization, so Depth and depth can be separate folders.
Nodes Keep favorite nodes close, grouped by theme or repository name.
Workflows Save the whole script, save a selection of nodes, load workflows, or insert saved modules into the current canvas.
Recovery Use autosaves, starter presets, snapshot library settings, and missing-pack install helpers.

New here? Start with the KForge overview. Using it inside ComfyUI? Open the operational guide:

Included Nodes

Node ID Display name Category
EasyWan22Prompt Wan 2.2 Easy Prompt (Koolook) Koolook/Wan_Video
EasyResize_Koolook Easy Resize (Koolook) Koolook/Image
EasyAIPipeline Easy AI Pipeline (Koolook) Koolook/Pipeline
easy_ImageBatch Easy Image Batch (Koolook) Koolook/Image
KoolookLoadCameraPosesAbsolute Koolook Load Camera Poses (Absolute Path) Koolook/Camera
Easy_hdr_VAE_encode Easy HDR VAE Encode (Koolook) Koolook/VAE
Easy_hdr_VAE_decode Easy HDR VAE Decode (Koolook) Koolook/VAE
Easy_Pattern Easy Pattern (Koolook) Koolook/Testing
Easy_VideoCombine Easy Video Combine (Koolook) Koolook/Video

The root Koolook nodes and the slim Radiance Koolook VAE wrappers have no extra Python dependencies beyond torch, which ComfyUI already requires.

Install

ComfyUI-Manager

  1. Open ComfyUI Manager.
  2. Choose Install Custom Nodes.
  3. Install by Git URL: https://github.com/malkuthro/ComfyUI-Koolook.git
  4. Restart ComfyUI.
  5. Open the Kforge Labs sidebar tab.

Manual

Clone this repository into ComfyUI/custom_nodes/:

git clone https://github.com/malkuthro/ComfyUI-Koolook.git

Restart ComfyUI after installation.

Learn More

Stability

For production work, install a pinned release tag or commit instead of following a moving branch head. Node IDs are treated carefully so saved ComfyUI workflows continue to load across compatible updates.

License

GPL-3.0. See LICENSE.

This package incorporates and adapts GPL-3.0 code from fxtdstudios/radiance under forks/radiance_koolook/. Attribution and per-fork modification notes are tracked in forks/THIRD_PARTY.md and forks/forks_manifest.yaml.

S
Description
No description provided
Readme GPL-3.0
220 MiB
Languages
Python 67.9%
JavaScript 30.3%
HTML 1%
CSS 0.5%
Shell 0.2%
Other 0.1%