feat(nodes): Easy Image Batch offset mode via keyframe_batch input (#242)
Add the inverse of the node's select behaviour. When the new optional keyframe_batch IMAGE input is connected, the node scatters a packed sequence back onto the source_frames positions (i-th frame -> i-th listed number, ascending), placeholder fill in between, at total_frames length. This closes the select -> process -> reconstruct loop: feed a processed selected_image_batch plus the same selected_frames list back in to rebuild the original spacing. The switch is wire-driven (source_batch and image1-4 are ignored while keyframe_batch is connected, with a console note); count mismatches and outside-cut positions are reported in the end-of-run summary. Placement logic is a pure, torch-free planner (plan_offset_placements) so it is unit-tested in CI, where torch is unavailable. Fully backward-compatible: unconnected keyframe_batch preserves the existing select path exactly. Tests: 10 new torch-free tests; full suite 630 passed, 1 skipped; preflight_release green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ae2480eee9
commit
df34ddcf1a
@@ -7,6 +7,21 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Easy Image Batch — offset mode (`keyframe_batch`).** A new optional
|
||||
`keyframe_batch` input adds the inverse of the node's select behaviour.
|
||||
When connected, the node switches from *selecting* sparse keyframes out of
|
||||
a dense `source_batch` to *scattering* an already-packed short sequence
|
||||
back onto the `source_frames` positions — placing the *i*-th incoming
|
||||
frame at the *i*-th listed number (ascending), placeholder colour in
|
||||
between, at the original sequence length. This closes the
|
||||
select → process → reconstruct loop: feed a processed copy of a previous
|
||||
`selected_image_batch` plus the same `selected_frames` list back in to
|
||||
rebuild the original spacing. The switch is wire-driven (`source_batch`
|
||||
and `image1…image4` are ignored while `keyframe_batch` is connected, with
|
||||
a console note); frame-count vs list-length mismatches and outside-cut
|
||||
positions are reported in the end-of-run summary. Placement logic is a
|
||||
pure, torch-free planner (`plan_offset_placements`) so it is unit-tested
|
||||
in CI.
|
||||
- **Publish setup success feedback (#227).** Publishing a saved sidebar
|
||||
workflow no longer closes the dialog silently — a confirmation card now
|
||||
shows where the setup was saved (the registry `setups.json` path) and
|
||||
|
||||
@@ -23,6 +23,15 @@ carry real image data and the rest tell the model "no latent update
|
||||
here". The cut window lets you grab any sub-range of a longer sequence
|
||||
without needing an external `ImageFromBatch` slicer.
|
||||
|
||||
**Offset mode (the inverse).** Connect a packed sequence to
|
||||
`keyframe_batch` and the node flips around: instead of *selecting* sparse
|
||||
keyframes out of a dense source, it *scatters* an already-dense short
|
||||
sequence back onto the keyframe positions from the frame list — placing
|
||||
frame *i* at the *i*-th listed number, placeholder in between. Use it to
|
||||
rebuild a full-length sequence from frames you selected, exported,
|
||||
processed, and want back in their original spacing (step 2 of a
|
||||
select → process → reconstruct workflow).
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required widgets
|
||||
@@ -40,12 +49,14 @@ without needing an external `ImageFromBatch` slicer.
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| `keyframe_batch` | `IMAGE` | Packed sequence for **offset mode**. When connected, the node switches from *select* to *offset*: frame *i* is placed at the *i*-th position in `source_frames` (ascending), placeholder elsewhere. `source_batch` and `image1…image4` are ignored while this is connected. |
|
||||
| `source_batch` | `IMAGE` | Pre-batched image stack, e.g. straight from `Load Video`. `source_batch[0]` is treated as VFX frame 1; `source_batch[k]` is VFX frame `k + 1`. |
|
||||
| `image1` … `image4` | `IMAGE` | Per-slot keyframes. Each must be a single-frame `IMAGE` (batch size 1). Pre-batched stacks must come through `source_batch` instead. |
|
||||
|
||||
At least one of `image1` or `source_batch` must be connected; otherwise
|
||||
the node raises a clear `ValueError`. All connected inputs must share
|
||||
the same `H × W × C`.
|
||||
the same `H × W × C`. (In **offset mode** — `keyframe_batch` connected —
|
||||
that input alone is sufficient and the others are ignored.)
|
||||
|
||||
## Outputs
|
||||
|
||||
@@ -118,6 +129,25 @@ When more than one mode contributes:
|
||||
3. The set of placed keyframes is deduped automatically (`selected_*`
|
||||
outputs reflect the final, deduped state).
|
||||
|
||||
### Mode 4 — Offset / reconstruct (`keyframe_batch`)
|
||||
|
||||
The inverse of Modes 1–3. Connect a packed `IMAGE` sequence (e.g. frames
|
||||
you previously pulled via `selected_image_batch`, processed, and
|
||||
re-loaded) to `keyframe_batch`. The node switches to **offset mode** and
|
||||
reads `source_frames` as *destination positions*: the *i*-th frame of
|
||||
`keyframe_batch` is placed at output index `position[i] - cut_start_frame`,
|
||||
everything else filled by `placeholder_color`. Positions are taken
|
||||
ascending and de-duplicated, so it round-trips cleanly with the ascending
|
||||
`selected_image_batch` + `selected_frames` a select-mode node produces.
|
||||
|
||||
- **Wire-driven switch.** Offset mode is active whenever `keyframe_batch`
|
||||
is connected; `source_batch` and `image1…image4` are ignored (a one-line
|
||||
console note fires if they're connected, so nothing is silently dropped).
|
||||
- **Counts needn't match.** Extra incoming frames beyond the list are
|
||||
reported as unused; positions beyond the available frames are reported as
|
||||
missing. Outside-cut positions are dropped, same as select mode.
|
||||
- **Empty list ⇒ nothing placed** (all placeholder), with a console note.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **1-based VFX numbering.** `imageN_frame`, `source_frames`,
|
||||
@@ -171,6 +201,29 @@ This replaces the workflow pattern of feeding the Easy Image Batch
|
||||
output through an external `ImageFromBatch (batch_index=40, length=81)`
|
||||
node — the cut is built in.
|
||||
|
||||
## Example C — Offset mode (reconstruct a selection)
|
||||
|
||||
Step 1 produced 5 enhanced frames (a 5-frame sequence) that originally
|
||||
came from VFX frames 1, 10, 19, 30, 40 of a 41-frame shot. Step 2 rebuilds
|
||||
the original length:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `keyframe_batch` | the 5 processed frames (e.g. `Load EXR` / image batch) |
|
||||
| `source_frames` | `"1, 10, 19, 30, 40"` |
|
||||
| `cut_start_frame` | `1` |
|
||||
| `total_frames` | `41` |
|
||||
| `placeholder_color` | `Black` |
|
||||
|
||||
| Output | What you get |
|
||||
|---|---|
|
||||
| `image_batch` | 41 frames; the 5 processed frames land at indices 0 / 9 / 18 / 29 / 39. Rest is black. |
|
||||
| `alpha_batch` | 41-frame mask; those 5 indices are `0.0`, the other 36 are `1.0` — ready as an inpaint mask for the gaps. |
|
||||
| `selected_image_batch` | the 5 frames, packed back-to-back. |
|
||||
| `selected_frames` | `"1, 10, 19, 30, 40"` |
|
||||
|
||||
Console summary: `offset mode: cut window frames 1..41 (41 frames). 5 placed.`
|
||||
|
||||
## Tips
|
||||
|
||||
- **Round-tripping selections.** Wire `selected_frames` of one node
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
|
||||
import torch
|
||||
@@ -12,6 +15,62 @@ _PLACEHOLDER_FILL = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OffsetPlan:
|
||||
"""Result of planning offset-mode placements (pure; no torch).
|
||||
|
||||
- ``placements`` — ``(source_index, output_index, vfx_frame)`` for
|
||||
each incoming frame that lands inside the cut window, ascending by
|
||||
position. ``source_index`` is the frame's ordinal in ``keyframe_batch``.
|
||||
- ``outside_cut`` — VFX frame numbers paired to a frame but whose
|
||||
output index fell outside ``[0, total_frames)`` (dropped, like select
|
||||
mode).
|
||||
- ``unused_source`` — count of incoming frames with no position to fill
|
||||
(more frames than listed positions).
|
||||
- ``missing_positions`` — listed positions with no incoming frame left
|
||||
(more positions than frames).
|
||||
"""
|
||||
|
||||
placements: list[tuple[int, int, int]]
|
||||
outside_cut: list[int]
|
||||
unused_source: int
|
||||
missing_positions: list[int]
|
||||
|
||||
|
||||
def plan_offset_placements(
|
||||
positions: list[int],
|
||||
source_count: int,
|
||||
total_frames: int,
|
||||
cut_start_frame: int,
|
||||
) -> OffsetPlan:
|
||||
"""Map a packed sequence of ``source_count`` frames onto keyframe positions.
|
||||
|
||||
Offset mode (the inverse of selecting): the i-th incoming frame is assigned
|
||||
to the i-th position, with positions taken ascending and de-duplicated so
|
||||
the mapping round-trips cleanly with the ascending ``selected_frames``
|
||||
output. A position is placed iff its output index ``vfx - cut_start_frame``
|
||||
falls inside ``[0, total_frames)``; otherwise it is reported in
|
||||
``outside_cut`` and dropped (matching select-mode cut behaviour).
|
||||
|
||||
Pure Python — no torch — so the placement logic is unit-testable in CI
|
||||
without the heavyweight tensor dependency.
|
||||
"""
|
||||
ordered = sorted(dict.fromkeys(int(p) for p in positions))
|
||||
paired = min(source_count, len(ordered))
|
||||
placements: list[tuple[int, int, int]] = []
|
||||
outside_cut: list[int] = []
|
||||
for source_index in range(paired):
|
||||
vfx = ordered[source_index]
|
||||
output_index = vfx - cut_start_frame
|
||||
if 0 <= output_index < total_frames:
|
||||
placements.append((source_index, output_index, vfx))
|
||||
else:
|
||||
outside_cut.append(vfx)
|
||||
unused_source = max(0, source_count - len(ordered))
|
||||
missing_positions = ordered[source_count:] if source_count < len(ordered) else []
|
||||
return OffsetPlan(placements, outside_cut, unused_source, missing_positions)
|
||||
|
||||
|
||||
class easy_ImageBatch:
|
||||
"""
|
||||
Place up to N keyframes on a 1-based VFX timeline, then output a
|
||||
@@ -90,6 +149,13 @@ class easy_ImageBatch:
|
||||
one of `image1` or `source_batch` must be provided. All keyframes
|
||||
(and `source_batch` frames) must share the same H/W/C.
|
||||
|
||||
Offset mode (the inverse) — connect a packed sequence to
|
||||
`keyframe_batch` and the node instead *scatters* those frames back onto
|
||||
the `source_frames` positions (i-th frame → i-th position, ascending),
|
||||
filling the rest with the placeholder. `source_batch` and the manual
|
||||
slots are ignored while `keyframe_batch` is connected. Round-trips with
|
||||
`selected_image_batch` + `selected_frames`. See `create_batch_from_frames`.
|
||||
|
||||
Useful for preparing sparse control sequences for video models like
|
||||
Wan 2.2, where placeholder frames indicate no latent update.
|
||||
|
||||
@@ -170,6 +236,20 @@ class easy_ImageBatch:
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"keyframe_batch": ("IMAGE", {
|
||||
"tooltip": (
|
||||
"Connect a packed sequence of already-selected/processed "
|
||||
"frames to switch the node into OFFSET mode: each incoming "
|
||||
"frame is placed onto the timeline at the matching number "
|
||||
"from the frame list (1st frame -> 1st number, 2nd -> 2nd, "
|
||||
"...), with every other position filled by "
|
||||
"placeholder_color. The frame list (source_frames) supplies "
|
||||
"the positions; source_batch and image1-4 are ignored while "
|
||||
"this is connected. Use it to rebuild a full-length sequence "
|
||||
"from frames you exported, processed, and want back in their "
|
||||
"original positions."
|
||||
),
|
||||
}),
|
||||
"source_batch": ("IMAGE", ),
|
||||
"image1": ("IMAGE", ),
|
||||
"image2": ("IMAGE", ),
|
||||
@@ -224,7 +304,25 @@ class easy_ImageBatch:
|
||||
image3_frame=None,
|
||||
image4=None,
|
||||
image4_frame=None,
|
||||
keyframe_batch=None,
|
||||
):
|
||||
# Offset mode: when keyframe_batch is connected, switch behaviour from
|
||||
# "select frames out of source_batch" to "scatter this packed sequence
|
||||
# back onto the frame-list positions". See create_batch_from_frames.
|
||||
if keyframe_batch is not None:
|
||||
ignored = source_batch is not None or any(
|
||||
img is not None for img in (image1, image2, image3, image4)
|
||||
)
|
||||
return self.create_batch_from_frames(
|
||||
keyframe_batch,
|
||||
source_frames,
|
||||
total_frames,
|
||||
cut_start_frame,
|
||||
placeholder_color,
|
||||
invert_alpha,
|
||||
ignored_inputs_connected=ignored,
|
||||
)
|
||||
|
||||
# Validate per-slot image inputs (must be single-frame tensors,
|
||||
# not pre-batched). source_batch is the only accepted multi-frame input.
|
||||
for slot_name, slot_img in (
|
||||
@@ -413,6 +511,110 @@ class easy_ImageBatch:
|
||||
|
||||
return (image_batch, alpha_batch, selected_image_batch, selected_frames_out, )
|
||||
|
||||
def create_batch_from_frames(
|
||||
self,
|
||||
keyframe_batch,
|
||||
source_frames,
|
||||
total_frames,
|
||||
cut_start_frame,
|
||||
placeholder_color,
|
||||
invert_alpha,
|
||||
ignored_inputs_connected=False,
|
||||
):
|
||||
"""Offset mode — active when ``keyframe_batch`` is connected.
|
||||
|
||||
Scatter the packed ``keyframe_batch`` sequence back onto the timeline
|
||||
at the positions listed in ``source_frames`` (i-th frame → i-th
|
||||
position, ascending), filling the rest with ``placeholder_color``. This
|
||||
is the inverse of the node's select behaviour: feed back a processed
|
||||
copy of a previous ``selected_image_batch`` together with the same
|
||||
``selected_frames`` list to rebuild a full-length sequence in the
|
||||
original spacing. ``source_batch`` and the 4 manual slots are ignored
|
||||
in this mode (the wire-driven switch keeps the behaviour unambiguous).
|
||||
|
||||
Outputs match the select path: ``image_batch`` (cut window),
|
||||
``alpha_batch`` (placed = 0.0 / empty = 1.0, flipped by
|
||||
``invert_alpha``), ``selected_image_batch`` (the placed frames packed
|
||||
ascending), and ``selected_frames`` (their VFX numbers, round-trippable).
|
||||
"""
|
||||
if ignored_inputs_connected:
|
||||
print(
|
||||
"[easy_ImageBatch] offset mode (keyframe_batch connected): "
|
||||
"source_batch and image1-4 are ignored; the frame list controls "
|
||||
"placement."
|
||||
)
|
||||
|
||||
h, w, c = keyframe_batch.shape[1:]
|
||||
device = keyframe_batch.device
|
||||
dtype = keyframe_batch.dtype
|
||||
|
||||
fill_value = _PLACEHOLDER_FILL[placeholder_color]
|
||||
image_batch = torch.full((total_frames, h, w, c), fill_value, device=device, dtype=dtype)
|
||||
# Same convention as the select path: 1.0 (white/empty), 0.0 on placed.
|
||||
alpha_batch = torch.ones((total_frames, h, w), device=device, dtype=torch.float32)
|
||||
|
||||
# Parse the position list (mirrors the select-mode tokenizer: commas,
|
||||
# spaces, tabs, newlines, or any mix).
|
||||
positions: list[int] = []
|
||||
source_frames_str = (source_frames or "").strip()
|
||||
for tok in re.split(r"[,\s]+", source_frames_str):
|
||||
if not tok:
|
||||
continue
|
||||
try:
|
||||
positions.append(int(tok))
|
||||
except ValueError:
|
||||
print(f"[easy_ImageBatch] source_frames: ignoring non-integer token '{tok}'.")
|
||||
|
||||
source_count = keyframe_batch.shape[0]
|
||||
plan = plan_offset_placements(positions, source_count, total_frames, cut_start_frame)
|
||||
|
||||
placed_indices: list[int] = []
|
||||
for source_index, output_index, _vfx in plan.placements:
|
||||
image_batch[output_index] = keyframe_batch[source_index]
|
||||
alpha_batch[output_index] = 0.0
|
||||
placed_indices.append(output_index)
|
||||
|
||||
if invert_alpha:
|
||||
alpha_batch = 1.0 - alpha_batch
|
||||
|
||||
sorted_indices = sorted(placed_indices)
|
||||
if sorted_indices:
|
||||
selected_image_batch = image_batch[sorted_indices]
|
||||
else:
|
||||
selected_image_batch = image_batch[:0]
|
||||
selected_frames_out = ", ".join(
|
||||
str(idx + cut_start_frame) for idx in sorted_indices
|
||||
)
|
||||
|
||||
# One-line summary (mirrors select mode) with offset-specific notes so
|
||||
# silent surprises (extra frames, short lists, outside-cut, empty list)
|
||||
# are visible without scrolling per-frame logs.
|
||||
cut_end = cut_start_frame + total_frames - 1
|
||||
notes: list[str] = []
|
||||
if plan.outside_cut:
|
||||
notes.append(
|
||||
f"{len(plan.outside_cut)} outside cut: "
|
||||
+ ", ".join(str(f) for f in plan.outside_cut)
|
||||
)
|
||||
if plan.unused_source:
|
||||
notes.append(
|
||||
f"{plan.unused_source} extra source frame(s) had no listed position"
|
||||
)
|
||||
if plan.missing_positions:
|
||||
notes.append(
|
||||
f"{len(plan.missing_positions)} position(s) had no source frame: "
|
||||
+ ", ".join(str(f) for f in plan.missing_positions)
|
||||
)
|
||||
if not source_frames_str:
|
||||
notes.append("frame list is empty — nothing placed")
|
||||
suffix = (" " + "; ".join(notes) + ".") if notes else ""
|
||||
print(
|
||||
f"[easy_ImageBatch] offset mode: cut window frames {cut_start_frame}.."
|
||||
f"{cut_end} ({total_frames} frames). {len(sorted_indices)} placed.{suffix}"
|
||||
)
|
||||
|
||||
return (image_batch, alpha_batch, selected_image_batch, selected_frames_out, )
|
||||
|
||||
# Node mappings for ComfyUI registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy_ImageBatch": easy_ImageBatch
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for ``easy_ImageBatch`` offset mode.
|
||||
|
||||
The node imports ``torch`` at module load, but the offset *planner*
|
||||
(``plan_offset_placements``) is pure Python and never touches torch — it
|
||||
just computes which incoming frame lands at which output index. CI installs
|
||||
only ``pytest`` / ``aiohttp`` / ``Pillow`` (no torch), so we stub the module
|
||||
to import the node torch-free and test the placement logic directly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
# Must run before importing the node module (its top-level ``import torch``).
|
||||
# ``setdefault`` keeps a real torch if one happens to be installed.
|
||||
sys.modules.setdefault("torch", types.ModuleType("torch"))
|
||||
|
||||
from k_easy_image_batch import ( # noqa: E402 (import after stub)
|
||||
easy_ImageBatch,
|
||||
plan_offset_placements,
|
||||
)
|
||||
|
||||
|
||||
def _frames(plan):
|
||||
"""VFX frame numbers placed, in order."""
|
||||
return [vfx for _src, _out, vfx in plan.placements]
|
||||
|
||||
|
||||
def test_basic_offset_maps_packed_frames_to_listed_positions():
|
||||
# 5 processed frames, list 1,10,19,30,40, full 41-frame window (cut=1).
|
||||
plan = plan_offset_placements([1, 10, 19, 30, 40], source_count=5, total_frames=41, cut_start_frame=1)
|
||||
assert plan.placements == [
|
||||
(0, 0, 1),
|
||||
(1, 9, 10),
|
||||
(2, 18, 19),
|
||||
(3, 29, 30),
|
||||
(4, 39, 40),
|
||||
]
|
||||
assert plan.outside_cut == []
|
||||
assert plan.unused_source == 0
|
||||
assert plan.missing_positions == []
|
||||
|
||||
|
||||
def test_pairing_is_by_ascending_position_not_list_order():
|
||||
# Out-of-order list still pairs source[0] -> lowest position, etc., so it
|
||||
# round-trips with selected_frames (which is emitted ascending).
|
||||
plan = plan_offset_placements([40, 1, 19], source_count=3, total_frames=41, cut_start_frame=1)
|
||||
assert _frames(plan) == [1, 19, 40]
|
||||
assert [src for src, _out, _vfx in plan.placements] == [0, 1, 2]
|
||||
|
||||
|
||||
def test_more_source_frames_than_positions_reports_unused():
|
||||
plan = plan_offset_placements([1, 10, 19], source_count=5, total_frames=41, cut_start_frame=1)
|
||||
assert _frames(plan) == [1, 10, 19]
|
||||
assert plan.unused_source == 2
|
||||
assert plan.missing_positions == []
|
||||
|
||||
|
||||
def test_fewer_source_frames_than_positions_reports_missing():
|
||||
plan = plan_offset_placements([1, 10, 19, 30], source_count=2, total_frames=41, cut_start_frame=1)
|
||||
assert _frames(plan) == [1, 10]
|
||||
assert plan.unused_source == 0
|
||||
assert plan.missing_positions == [19, 30]
|
||||
|
||||
|
||||
def test_positions_outside_cut_window_are_dropped():
|
||||
# output index = vfx - cut_start_frame; window [0, 20).
|
||||
# frame 1 -> -9 (out), 15 -> 5 (in), 40 -> 30 (out).
|
||||
plan = plan_offset_placements([1, 15, 40], source_count=3, total_frames=20, cut_start_frame=10)
|
||||
assert _frames(plan) == [15]
|
||||
assert plan.outside_cut == [1, 40]
|
||||
|
||||
|
||||
def test_duplicate_positions_are_deduped():
|
||||
plan = plan_offset_placements([10, 10, 19], source_count=3, total_frames=41, cut_start_frame=1)
|
||||
assert _frames(plan) == [10, 19]
|
||||
|
||||
|
||||
def test_empty_position_list_places_nothing():
|
||||
plan = plan_offset_placements([], source_count=5, total_frames=41, cut_start_frame=1)
|
||||
assert plan.placements == []
|
||||
assert plan.unused_source == 5
|
||||
|
||||
|
||||
def test_cut_start_offsets_output_indices():
|
||||
# cut_start_frame=41: frame 41 -> idx 0, 63 -> idx 22.
|
||||
plan = plan_offset_placements([41, 63], source_count=2, total_frames=81, cut_start_frame=41)
|
||||
assert plan.placements == [(0, 0, 41), (1, 22, 63)]
|
||||
|
||||
|
||||
def test_input_types_exposes_keyframe_batch_with_tooltip():
|
||||
spec = easy_ImageBatch.INPUT_TYPES()
|
||||
assert "keyframe_batch" in spec["optional"], "new offset-mode input must be declared"
|
||||
entry = spec["optional"]["keyframe_batch"]
|
||||
assert entry[0] == "IMAGE"
|
||||
tooltip = entry[1]["tooltip"].lower()
|
||||
assert "offset" in tooltip
|
||||
assert "frame list" in tooltip
|
||||
|
||||
|
||||
def test_registration_exports_unchanged():
|
||||
from k_easy_image_batch import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
assert NODE_CLASS_MAPPINGS["easy_ImageBatch"] is easy_ImageBatch
|
||||
assert NODE_DISPLAY_NAME_MAPPINGS["easy_ImageBatch"] == "Easy Image Batch (Koolook)"
|
||||
Reference in New Issue
Block a user