Add Z-Image prompt refining and Qwen conditioning support
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.0
|
||||
|
||||
- Added `Multi Vocab String List (Nukun)` for combining four selectable vocabulary files with deterministic random fills and optional direct word selectors.
|
||||
- Added `resources/camera_composition.csv` for camera, framing, focus, perspective, and composition prompt terms.
|
||||
|
||||
## v0.4.0
|
||||
|
||||
- Added deduplicated `little_doom_*.csv` Illustrious LoRA keyword resources and a reproducible generator utility.
|
||||
|
||||
@@ -32,6 +32,8 @@ The main node set and core examples work with a standard ComfyUI installation. T
|
||||
- `NukunCheckpointPairCyclerLoader` - display name `Checkpoint Pair Cycler Loader (Nukun)`, category `Nukun/Loaders`
|
||||
- `NukunIncrementingIntString` - display name `Incrementing Int to String (Nukun)`, category `Nukun/Text`
|
||||
- `NukunRandomVocabStringList` - display name `Random Vocab String List (Nukun)`, category `Nukun/Text`
|
||||
- `NukunVocabMultiStringList` - display name `Multi Vocab String List (Nukun)`, category `Nukun/Text`
|
||||
- `NukunOllamaPromptRefiner` - display name `Ollama Prompt Refiner (Nukun)`, category `Nukun/Text`
|
||||
- `LoadImagewithSubfolders` - display name `Load Image with Subfolders`, category `Nukun/Image`
|
||||
- `T5Balancer` - display name `T5 Token-based Prompt Balancer`, category `Nukun/Conditioning`
|
||||
- `NukunT5EqualLengthBalancer` - display name `T5 Equal-Length Prompt Balancer (Nukun)`, category `Nukun/Conditioning`
|
||||
@@ -96,10 +98,25 @@ This node reads a selected comma-separated plain-text word list and outputs a de
|
||||
It can use `ComfyUI/user/vocab.json` or bundled files such as `resources/english_words.csv`; add more `.csv`, `.txt`, or `.json` files to `resources/` to make them selectable.
|
||||
Set `amount` for the number of words and use the `seed` widget's control-after-generate behavior to keep, increment, decrement, or randomize selections between queued runs.
|
||||
Words are sampled without duplicates; if `amount` is larger than the available vocabulary, the output is clamped to the full list.
|
||||
Bundled category vocabularies include places/environments, objects, animals and mythical creatures, verbs, nouns, adjectives, `*ing` words, person names, countries, and cities.
|
||||
Bundled category vocabularies include places/environments, objects, animals and mythical creatures, verbs, nouns, adjectives, `*ing` words, camera/composition terms, person names, countries, and cities.
|
||||
Bundled Little Doom LoRA keyword resources are also available as `little_doom_*.csv`, including clean, character/source, visual feature, clothing, action, setting, style, mature, and dark/gore subsets.
|
||||
Connect the optional `chain` input to append generated words after an existing string and build prompt chains.
|
||||
|
||||
`Multi Vocab String List (Nukun)` combines four selectable vocab files in one node. Each slot has its own amount and three optional direct word selectors; keep a selector on `(random)` to fill it from the chosen vocabulary. The node returns a combined string plus one output per slot.
|
||||
|
||||
## Ollama Prompt Refiner
|
||||
|
||||
This node sends a random vocabulary string to a local Ollama model and returns one curated prompt pair for the selected `target_profile`: `pony_v6`, `illustrious`, or `pony_v7`.
|
||||
It is designed to sit after `Random Vocab String List (Nukun)` or `Multi Vocab String List (Nukun)`: connect the random `STRING` output to `word_salad`, choose one `target_profile`, then connect `positive` and `negative` to your text encoders.
|
||||
The default Ollama endpoint is `http://127.0.0.1:11434` and the default model is `autoren-darkidol-llama-3-1-8b:latest`.
|
||||
The `ollama_model` widget is a dropdown populated from the local Ollama `/api/tags` list when ComfyUI loads the node.
|
||||
Set `style_cluster` to choose the Pony v7 `style_cluster_XXXX` header value; the default is `430`.
|
||||
Use `style_anchor` for fixed character names, LoRA trigger words, quality tags, or motifs that should survive the rewrite even when the random input is noisy.
|
||||
The node outputs `positive`, `negative`, and `report`. Pony v6 and Illustrious outputs are normalized without commas to reduce prompt tokens; Pony v7 keeps commas for its structured caption and Danbooru tag block.
|
||||
Reka Flash models are sent with their `human: ... <sep> assistant:` prompt format and `<sep>`/`<|endoftext|>` stop strings because the GGUF import uses a plain prompt template.
|
||||
The node asks Ollama for strict JSON and retries once with a repair prompt if the model returns malformed JSON.
|
||||
This is a breaking output change from older versions that exposed separate `pony_v6_*`, `illustrious_*`, and `pony_v7_*` outputs; reconnect old workflows to `positive` and `negative`.
|
||||
|
||||
## Regional Split Regions
|
||||
|
||||
This node creates simple horizontal or vertical 2/3-way split masks and returns A8R8 `AttentionCouple` compatible regions.
|
||||
|
||||
Binary file not shown.
@@ -6,6 +6,8 @@ from .nodes.incrementing_int_string import NODE_CLASS_MAPPINGS as INT_STRING_CLA
|
||||
from .nodes.incrementing_int_string import NODE_DISPLAY_NAME_MAPPINGS as INT_STRING_DISPLAY_MAPPINGS
|
||||
from .nodes.random_vocab_string_list import NODE_CLASS_MAPPINGS as RANDOM_VOCAB_STRING_LIST_CLASS_MAPPINGS
|
||||
from .nodes.random_vocab_string_list import NODE_DISPLAY_NAME_MAPPINGS as RANDOM_VOCAB_STRING_LIST_DISPLAY_MAPPINGS
|
||||
from .nodes.ollama_prompt_refiner import NODE_CLASS_MAPPINGS as OLLAMA_PROMPT_REFINER_CLASS_MAPPINGS
|
||||
from .nodes.ollama_prompt_refiner import NODE_DISPLAY_NAME_MAPPINGS as OLLAMA_PROMPT_REFINER_DISPLAY_MAPPINGS
|
||||
from .nodes.t5_token_balancer import NODE_CLASS_MAPPINGS as T5_CLASS_MAPPINGS
|
||||
from .nodes.t5_token_balancer import NODE_DISPLAY_NAME_MAPPINGS as T5_DISPLAY_MAPPINGS
|
||||
from .nodes.t5_equal_length_balancer import NODE_CLASS_MAPPINGS as T5_EQUAL_LENGTH_CLASS_MAPPINGS
|
||||
@@ -49,6 +51,7 @@ NODE_CLASS_MAPPINGS = {}
|
||||
NODE_CLASS_MAPPINGS.update(CHECKPOINT_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(INT_STRING_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(RANDOM_VOCAB_STRING_LIST_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(OLLAMA_PROMPT_REFINER_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(LOAD_IMAGE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(T5_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(T5_EQUAL_LENGTH_CLASS_MAPPINGS)
|
||||
@@ -73,6 +76,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(CHECKPOINT_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(INT_STRING_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(RANDOM_VOCAB_STRING_LIST_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(OLLAMA_PROMPT_REFINER_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LOAD_IMAGE_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(T5_DISPLAY_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(T5_EQUAL_LENGTH_DISPLAY_MAPPINGS)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,18 @@ import random
|
||||
|
||||
import folder_paths
|
||||
|
||||
try:
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
except ImportError:
|
||||
web = None
|
||||
PromptServer = None
|
||||
|
||||
RESOURCE_EXTENSIONS = (".csv", ".txt", ".json")
|
||||
USER_VOCAB_LABEL = "user/vocab.json"
|
||||
RANDOM_WORD_LABEL = "(random)"
|
||||
MULTI_SLOT_COUNT = 4
|
||||
MULTI_WORD_COUNT = 3
|
||||
|
||||
|
||||
def _vocab_path():
|
||||
@@ -75,6 +85,65 @@ def _load_words(vocab_file):
|
||||
return words
|
||||
|
||||
|
||||
def _dedupe_preserve_order(words):
|
||||
seen = set()
|
||||
result = []
|
||||
for word in words:
|
||||
if word in seen:
|
||||
continue
|
||||
seen.add(word)
|
||||
result.append(word)
|
||||
return result
|
||||
|
||||
|
||||
def _slot_seed(seed, slot_index):
|
||||
return (int(seed) + (slot_index * 1000003)) & 0xffffffffffffffff
|
||||
|
||||
|
||||
def _generate_slot(vocab_file, amount, seed, slot_index, selections):
|
||||
amount = max(0, int(amount))
|
||||
if amount <= 0:
|
||||
return ""
|
||||
|
||||
words = _load_words(vocab_file)
|
||||
wordset = set(words)
|
||||
manual = [
|
||||
str(selection).strip()
|
||||
for selection in selections
|
||||
if str(selection).strip() and str(selection).strip() != RANDOM_WORD_LABEL
|
||||
]
|
||||
manual = _dedupe_preserve_order([word for word in manual if word in wordset])
|
||||
selected = manual[:amount]
|
||||
remaining_count = amount - len(selected)
|
||||
|
||||
if remaining_count > 0:
|
||||
available = [word for word in words if word not in set(selected)]
|
||||
pick_count = min(remaining_count, len(available))
|
||||
if pick_count > 0:
|
||||
selected.extend(random.Random(_slot_seed(seed, slot_index)).sample(available, pick_count))
|
||||
|
||||
return " ".join(selected)
|
||||
|
||||
|
||||
def _register_routes():
|
||||
if web is None or PromptServer is None:
|
||||
return
|
||||
|
||||
routes = PromptServer.instance.routes
|
||||
|
||||
@routes.get("/nukun/vocab/words")
|
||||
async def get_vocab_words(request):
|
||||
vocab_file = request.query.get("file", "")
|
||||
try:
|
||||
words = _load_words(vocab_file)
|
||||
except RuntimeError as error:
|
||||
return web.json_response({"file": vocab_file, "error": str(error)}, status=400)
|
||||
return web.json_response({"file": vocab_file, "words": words})
|
||||
|
||||
|
||||
_register_routes()
|
||||
|
||||
|
||||
class NukunRandomVocabStringList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -158,10 +227,130 @@ class NukunRandomVocabStringList:
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
class NukunVocabMultiStringList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
available_files = _available_vocab_files()
|
||||
default_file = USER_VOCAB_LABEL if USER_VOCAB_LABEL in available_files else available_files[0]
|
||||
|
||||
required = {
|
||||
"seed": (
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xffffffffffffffff,
|
||||
"control_after_generate": True,
|
||||
"tooltip": "Base seed for deterministic random selections across all slots.",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
for slot in range(1, MULTI_SLOT_COUNT + 1):
|
||||
required[f"vocab_file_{slot}"] = (
|
||||
available_files,
|
||||
{
|
||||
"default": default_file,
|
||||
"tooltip": f"Comma-separated word list for slot {slot}.",
|
||||
},
|
||||
)
|
||||
required[f"amount_{slot}"] = (
|
||||
"INT",
|
||||
{
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 10000,
|
||||
"tooltip": f"Number of words to output from slot {slot}. Use 0 to disable the slot.",
|
||||
},
|
||||
)
|
||||
for word_index in range(1, MULTI_WORD_COUNT + 1):
|
||||
required[f"word_{slot}_{word_index}"] = (
|
||||
"STRING",
|
||||
{
|
||||
"default": RANDOM_WORD_LABEL,
|
||||
"multiline": False,
|
||||
"tooltip": f"Optional fixed word {word_index} for slot {slot}. Keep {RANDOM_WORD_LABEL} to fill randomly.",
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"optional": {
|
||||
"chain": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "",
|
||||
"defaultInput": True,
|
||||
"forceInput": True,
|
||||
"multiline": False,
|
||||
"tooltip": "Optional incoming string. When connected, the combined output is appended after it.",
|
||||
},
|
||||
),
|
||||
},
|
||||
"required": required,
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("combined", "slot_1", "slot_2", "slot_3", "slot_4")
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "Nukun/Text"
|
||||
DESCRIPTION = "Combines four selectable vocabulary files into one deterministic prompt string with optional fixed words per slot."
|
||||
|
||||
def generate(self, seed, chain="", **kwargs):
|
||||
slot_outputs = []
|
||||
for slot in range(1, MULTI_SLOT_COUNT + 1):
|
||||
selections = [kwargs.get(f"word_{slot}_{word_index}", RANDOM_WORD_LABEL) for word_index in range(1, MULTI_WORD_COUNT + 1)]
|
||||
slot_outputs.append(
|
||||
_generate_slot(
|
||||
kwargs[f"vocab_file_{slot}"],
|
||||
kwargs[f"amount_{slot}"],
|
||||
seed,
|
||||
slot,
|
||||
selections,
|
||||
)
|
||||
)
|
||||
|
||||
combined_parts = [slot_output for slot_output in slot_outputs if slot_output]
|
||||
combined = " ".join(combined_parts)
|
||||
chain = chain.strip()
|
||||
if chain and combined:
|
||||
combined = f"{chain} {combined}"
|
||||
elif chain:
|
||||
combined = chain
|
||||
|
||||
return (combined, *slot_outputs)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, seed, chain="", **kwargs):
|
||||
digest = hashlib.sha256()
|
||||
digest.update(str(int(seed)).encode("utf-8"))
|
||||
digest.update(str(chain).encode("utf-8"))
|
||||
|
||||
for slot in range(1, MULTI_SLOT_COUNT + 1):
|
||||
vocab_file = kwargs.get(f"vocab_file_{slot}", "")
|
||||
digest.update(str(vocab_file).encode("utf-8"))
|
||||
digest.update(str(int(kwargs.get(f"amount_{slot}", 0))).encode("utf-8"))
|
||||
for word_index in range(1, MULTI_WORD_COUNT + 1):
|
||||
digest.update(str(kwargs.get(f"word_{slot}_{word_index}", RANDOM_WORD_LABEL)).encode("utf-8"))
|
||||
|
||||
try:
|
||||
path = _resolve_vocab_path(vocab_file)
|
||||
except RuntimeError:
|
||||
path = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, "rb") as vocab_file_handle:
|
||||
digest.update(vocab_file_handle.read())
|
||||
else:
|
||||
digest.update(b"missing")
|
||||
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"NukunRandomVocabStringList": NukunRandomVocabStringList,
|
||||
"NukunVocabMultiStringList": NukunVocabMultiStringList,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"NukunRandomVocabStringList": "Random Vocab String List (Nukun)",
|
||||
"NukunVocabMultiStringList": "Multi Vocab String List (Nukun)",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
SUPPORTED_T5_KEYS = ("pile_t5xl", "t5xxl", "umt5xxl", "mt5xl", "t5base")
|
||||
SUPPORTED_TEXT_STREAM_KEYS = (
|
||||
"pile_t5xl",
|
||||
"t5xxl",
|
||||
"umt5xxl",
|
||||
"mt5xl",
|
||||
"t5base",
|
||||
"qwen3_4b",
|
||||
"qwen3_8b",
|
||||
"qwen25_7b",
|
||||
"qwen25_3b",
|
||||
"qwen3_2b",
|
||||
"qwen3_06b",
|
||||
"qwen3vl_8b",
|
||||
)
|
||||
|
||||
# Backwards-compatible name for older imports.
|
||||
SUPPORTED_T5_KEYS = SUPPORTED_TEXT_STREAM_KEYS
|
||||
|
||||
|
||||
class NukunT5EqualLengthBalancer:
|
||||
@@ -6,7 +22,7 @@ class NukunT5EqualLengthBalancer:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tokenizer": ("CLIP", {"tooltip": "The loaded T5 tokenizer to use."}),
|
||||
"tokenizer": ("CLIP", {"tooltip": "The loaded T5/Qwen tokenizer to use."}),
|
||||
"target": (
|
||||
"INT",
|
||||
{
|
||||
@@ -34,24 +50,24 @@ class NukunT5EqualLengthBalancer:
|
||||
FUNCTION = "balance"
|
||||
CATEGORY = "Nukun/Conditioning"
|
||||
DESCRIPTION = (
|
||||
"Encodes positive and negative prompts with a supported T5 tokenizer, "
|
||||
"Encodes positive and negative prompts with a supported T5/Qwen tokenizer, "
|
||||
"padding both to the same effective token length and returning token diagnostics."
|
||||
)
|
||||
|
||||
def _set_t5_options(self, clip, min_length=None, min_padding=None):
|
||||
for key in SUPPORTED_T5_KEYS:
|
||||
def _set_text_stream_options(self, clip, min_length=None, min_padding=None):
|
||||
for key in SUPPORTED_TEXT_STREAM_KEYS:
|
||||
if min_length is not None:
|
||||
clip.set_tokenizer_option(f"{key}_min_length", min_length)
|
||||
if min_padding is not None:
|
||||
clip.set_tokenizer_option(f"{key}_min_padding", min_padding)
|
||||
|
||||
def _detect_t5_key(self, *token_sets):
|
||||
def _detect_text_stream_key(self, *token_sets):
|
||||
available = set()
|
||||
for tokens in token_sets:
|
||||
if isinstance(tokens, dict):
|
||||
available.update(tokens.keys())
|
||||
|
||||
for key in SUPPORTED_T5_KEYS:
|
||||
for key in SUPPORTED_TEXT_STREAM_KEYS:
|
||||
if key in available:
|
||||
return key
|
||||
return None
|
||||
@@ -79,27 +95,27 @@ class NukunT5EqualLengthBalancer:
|
||||
raise RuntimeError("ERROR: A valid tokenizer is required.")
|
||||
|
||||
measure = tokenizer.clone()
|
||||
self._set_t5_options(measure, min_length=0, min_padding=0)
|
||||
self._set_text_stream_options(measure, min_length=0, min_padding=0)
|
||||
raw_positive_tokens = measure.tokenize(positive)
|
||||
raw_negative_tokens = measure.tokenize(negative)
|
||||
|
||||
t5_key = self._detect_t5_key(raw_positive_tokens, raw_negative_tokens)
|
||||
if t5_key is None:
|
||||
expected = ", ".join(SUPPORTED_T5_KEYS)
|
||||
stream_key = self._detect_text_stream_key(raw_positive_tokens, raw_negative_tokens)
|
||||
if stream_key is None:
|
||||
expected = ", ".join(SUPPORTED_TEXT_STREAM_KEYS)
|
||||
available = self._available_token_keys(raw_positive_tokens, raw_negative_tokens)
|
||||
raise RuntimeError(
|
||||
"ERROR: No supported T5 token stream found. "
|
||||
"ERROR: No supported text token stream found. "
|
||||
f"Expected one of: {expected}. Available token streams: {available}."
|
||||
)
|
||||
|
||||
positive_raw_count = self._token_count(raw_positive_tokens, t5_key)
|
||||
negative_raw_count = self._token_count(raw_negative_tokens, t5_key)
|
||||
positive_raw_count = self._token_count(raw_positive_tokens, stream_key)
|
||||
negative_raw_count = self._token_count(raw_negative_tokens, stream_key)
|
||||
requested_target = max(0, int(target))
|
||||
effective_target = max(requested_target, positive_raw_count, negative_raw_count)
|
||||
|
||||
encoder = tokenizer.clone()
|
||||
self._set_t5_options(encoder, min_padding=0)
|
||||
encoder.set_tokenizer_option(f"{t5_key}_min_length", effective_target)
|
||||
self._set_text_stream_options(encoder, min_padding=0)
|
||||
encoder.set_tokenizer_option(f"{stream_key}_min_length", effective_target)
|
||||
|
||||
positive_tokens = encoder.tokenize(positive)
|
||||
negative_tokens = encoder.tokenize(negative)
|
||||
@@ -107,7 +123,7 @@ class NukunT5EqualLengthBalancer:
|
||||
cond_negative = encoder.encode_from_tokens_scheduled(negative_tokens)
|
||||
|
||||
report = (
|
||||
f"T5 key: {t5_key}; "
|
||||
f"text stream: {stream_key}; "
|
||||
f"positive raw: {positive_raw_count}; "
|
||||
f"negative raw: {negative_raw_count}; "
|
||||
f"requested target: {requested_target}; "
|
||||
@@ -129,5 +145,5 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"NukunT5EqualLengthBalancer": "T5 Equal-Length Prompt Balancer (Nukun)",
|
||||
"NukunT5EqualLengthBalancer": "T5/Qwen Equal-Length Prompt Balancer (Nukun)",
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import numbers
|
||||
import torch
|
||||
import comfy.model_management as model_management
|
||||
|
||||
from .t5_equal_length_balancer import SUPPORTED_T5_KEYS
|
||||
from .t5_equal_length_balancer import SUPPORTED_TEXT_STREAM_KEYS
|
||||
|
||||
|
||||
SCULPT_METHODS = ("forward", "backward", "maximum_absolute", "add_minimum_absolute")
|
||||
@@ -26,7 +26,7 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tokenizer": ("CLIP", {"tooltip": "The loaded T5 tokenizer to use."}),
|
||||
"tokenizer": ("CLIP", {"tooltip": "The loaded T5/Qwen tokenizer to use."}),
|
||||
"target": (
|
||||
"INT",
|
||||
{
|
||||
@@ -78,24 +78,24 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
FUNCTION = "balance"
|
||||
CATEGORY = "Nukun/Conditioning"
|
||||
DESCRIPTION = (
|
||||
"Encodes positive and negative prompts with equal T5 token length, "
|
||||
"optionally sculpting eligible T5 token embeddings before scheduled encoding."
|
||||
"Encodes positive and negative prompts with equal T5/Qwen token length, "
|
||||
"optionally sculpting eligible text token embeddings before scheduled encoding."
|
||||
)
|
||||
|
||||
def _set_t5_options(self, clip, min_length=None, min_padding=None):
|
||||
for key in SUPPORTED_T5_KEYS:
|
||||
def _set_text_stream_options(self, clip, min_length=None, min_padding=None):
|
||||
for key in SUPPORTED_TEXT_STREAM_KEYS:
|
||||
if min_length is not None:
|
||||
clip.set_tokenizer_option(f"{key}_min_length", min_length)
|
||||
if min_padding is not None:
|
||||
clip.set_tokenizer_option(f"{key}_min_padding", min_padding)
|
||||
|
||||
def _detect_t5_key(self, *token_sets):
|
||||
def _detect_text_stream_key(self, *token_sets):
|
||||
available = set()
|
||||
for tokens in token_sets:
|
||||
if isinstance(tokens, dict):
|
||||
available.update(tokens.keys())
|
||||
|
||||
for key in SUPPORTED_T5_KEYS:
|
||||
for key in SUPPORTED_TEXT_STREAM_KEYS:
|
||||
if key in available:
|
||||
return key
|
||||
return None
|
||||
@@ -118,14 +118,16 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
except (KeyError, TypeError):
|
||||
raise RuntimeError(f"ERROR: Token stream '{key}' was not found in tokenizer output.")
|
||||
|
||||
def _get_t5_submodel(self, clip, t5_key):
|
||||
submodel = getattr(clip.cond_stage_model, t5_key, None)
|
||||
def _get_text_submodel(self, clip, stream_key):
|
||||
submodel = getattr(clip.cond_stage_model, stream_key, None)
|
||||
if submodel is None:
|
||||
raise RuntimeError(f"ERROR: T5 submodel '{t5_key}' was not found on cond_stage_model.")
|
||||
raise RuntimeError(f"ERROR: Text encoder submodel '{stream_key}' was not found on cond_stage_model.")
|
||||
if not hasattr(submodel, "transformer"):
|
||||
raise RuntimeError(f"ERROR: T5 submodel '{t5_key}' has no transformer.")
|
||||
raise RuntimeError(f"ERROR: Text encoder submodel '{stream_key}' has no transformer.")
|
||||
if not hasattr(submodel.transformer, "get_input_embeddings"):
|
||||
raise RuntimeError(f"ERROR: T5 submodel '{t5_key}' has no input embeddings accessor.")
|
||||
raise RuntimeError(
|
||||
f"ERROR: Text encoder submodel '{stream_key}' has no input embeddings accessor."
|
||||
)
|
||||
return submodel
|
||||
|
||||
def _special_token_ids(self, submodel):
|
||||
@@ -229,9 +231,9 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
new_weight = new_weight * pre_mag / new_norm
|
||||
return new_weight.detach().cpu(), len(selected_weights)
|
||||
|
||||
def _eligible_entries(self, tokens, t5_key, special_ids):
|
||||
def _eligible_entries(self, tokens, stream_key, special_ids):
|
||||
coords = []
|
||||
for batch_index, batch in enumerate(tokens[t5_key]):
|
||||
for batch_index, batch in enumerate(tokens[stream_key]):
|
||||
for token_index, token_weight in enumerate(batch):
|
||||
if len(token_weight) < 2:
|
||||
continue
|
||||
@@ -243,15 +245,15 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
coords.append((batch_index, token_index, int(token_id)))
|
||||
return coords
|
||||
|
||||
def _apply_sculpting(self, tokens, t5_key, submodel, intensity, method, normalization, top_k):
|
||||
def _apply_sculpting(self, tokens, stream_key, submodel, intensity, method, normalization, top_k):
|
||||
if float(intensity) <= 0 and normalization == "none":
|
||||
return 0, 0
|
||||
|
||||
if t5_key not in tokens:
|
||||
raise RuntimeError(f"ERROR: Token stream '{t5_key}' was not found for sculpting.")
|
||||
if stream_key not in tokens:
|
||||
raise RuntimeError(f"ERROR: Token stream '{stream_key}' was not found for sculpting.")
|
||||
|
||||
special_ids = self._special_token_ids(submodel)
|
||||
coords = self._eligible_entries(tokens, t5_key, special_ids)
|
||||
coords = self._eligible_entries(tokens, stream_key, special_ids)
|
||||
if len(coords) == 0:
|
||||
return 0, 0
|
||||
|
||||
@@ -263,7 +265,7 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
mean_coords = []
|
||||
|
||||
for batch_index, token_index, token_id in coords:
|
||||
token_weight = tokens[t5_key][batch_index][token_index]
|
||||
token_weight = tokens[stream_key][batch_index][token_index]
|
||||
attn_weight = float(token_weight[1])
|
||||
if float(intensity) > 0:
|
||||
cache_key = (token_id, method, float(intensity), int(top_k))
|
||||
@@ -282,17 +284,17 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
mean_mag += torch.norm(new_vector).item()
|
||||
mean_coords.append((batch_index, token_index, attn_weight))
|
||||
|
||||
tokens[t5_key][batch_index][token_index] = (new_vector, attn_weight)
|
||||
tokens[stream_key][batch_index][token_index] = (new_vector, attn_weight)
|
||||
|
||||
if normalization in ("mean", "mean * attention") and len(mean_coords) > 0:
|
||||
mean_mag /= len(mean_coords)
|
||||
for batch_index, token_index, attn_weight in mean_coords:
|
||||
token_vector, current_attn = tokens[t5_key][batch_index][token_index]
|
||||
token_vector, current_attn = tokens[stream_key][batch_index][token_index]
|
||||
norm = torch.norm(token_vector)
|
||||
if norm == 0:
|
||||
continue
|
||||
scale = mean_mag * (attn_weight if normalization == "mean * attention" else 1.0)
|
||||
tokens[t5_key][batch_index][token_index] = (token_vector / norm * scale, current_attn)
|
||||
tokens[stream_key][batch_index][token_index] = (token_vector / norm * scale, current_attn)
|
||||
|
||||
del all_weights
|
||||
return sculpted_count, neighbor_count
|
||||
@@ -315,35 +317,35 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
raise RuntimeError("ERROR: A valid tokenizer is required.")
|
||||
|
||||
measure = tokenizer.clone()
|
||||
self._set_t5_options(measure, min_length=0, min_padding=0)
|
||||
self._set_text_stream_options(measure, min_length=0, min_padding=0)
|
||||
raw_positive_tokens = measure.tokenize(positive)
|
||||
raw_negative_tokens = measure.tokenize(negative)
|
||||
|
||||
t5_key = self._detect_t5_key(raw_positive_tokens, raw_negative_tokens)
|
||||
if t5_key is None:
|
||||
expected = ", ".join(SUPPORTED_T5_KEYS)
|
||||
stream_key = self._detect_text_stream_key(raw_positive_tokens, raw_negative_tokens)
|
||||
if stream_key is None:
|
||||
expected = ", ".join(SUPPORTED_TEXT_STREAM_KEYS)
|
||||
available = self._available_token_keys(raw_positive_tokens, raw_negative_tokens)
|
||||
raise RuntimeError(
|
||||
"ERROR: No supported T5 token stream found. "
|
||||
"ERROR: No supported text token stream found. "
|
||||
f"Expected one of: {expected}. Available token streams: {available}."
|
||||
)
|
||||
|
||||
positive_raw_count = self._token_count(raw_positive_tokens, t5_key)
|
||||
negative_raw_count = self._token_count(raw_negative_tokens, t5_key)
|
||||
positive_raw_count = self._token_count(raw_positive_tokens, stream_key)
|
||||
negative_raw_count = self._token_count(raw_negative_tokens, stream_key)
|
||||
requested_target = max(0, int(target))
|
||||
effective_target = max(requested_target, positive_raw_count, negative_raw_count)
|
||||
|
||||
encoder = tokenizer.clone()
|
||||
self._set_t5_options(encoder, min_padding=0)
|
||||
encoder.set_tokenizer_option(f"{t5_key}_min_length", effective_target)
|
||||
submodel = self._get_t5_submodel(encoder, t5_key)
|
||||
self._set_text_stream_options(encoder, min_padding=0)
|
||||
encoder.set_tokenizer_option(f"{stream_key}_min_length", effective_target)
|
||||
submodel = self._get_text_submodel(encoder, stream_key)
|
||||
|
||||
positive_tokens = encoder.tokenize(positive)
|
||||
negative_tokens = encoder.tokenize(negative)
|
||||
|
||||
positive_sculpted, positive_neighbors = self._apply_sculpting(
|
||||
positive_tokens,
|
||||
t5_key,
|
||||
stream_key,
|
||||
submodel,
|
||||
positive_intensity,
|
||||
positive_method,
|
||||
@@ -352,7 +354,7 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
)
|
||||
negative_sculpted, negative_neighbors = self._apply_sculpting(
|
||||
negative_tokens,
|
||||
t5_key,
|
||||
stream_key,
|
||||
submodel,
|
||||
negative_intensity,
|
||||
negative_method,
|
||||
@@ -364,7 +366,7 @@ class NukunT5SculptEqualLengthBalancer:
|
||||
cond_negative = encoder.encode_from_tokens_scheduled(negative_tokens)
|
||||
|
||||
report = (
|
||||
f"T5 key: {t5_key}; "
|
||||
f"text stream: {stream_key}; "
|
||||
f"positive raw: {positive_raw_count}; "
|
||||
f"negative raw: {negative_raw_count}; "
|
||||
f"requested target: {requested_target}; "
|
||||
@@ -395,5 +397,5 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"NukunT5SculptEqualLengthBalancer": "T5 Sculpt Equal-Length Prompt Balancer (Nukun)",
|
||||
"NukunT5SculptEqualLengthBalancer": "T5/Qwen Sculpt Equal-Length Prompt Balancer (Nukun)",
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
alligator, alpaca, ant, antelope, ape, badger, bat, bear, beaver, bee, beetle, bird, bison, boar, buffalo, butterfly, camel, cat, cheetah, chicken, cobra, cow, coyote, crab, crocodile, crow, deer, dog, dolphin, donkey, dragon, duck, eagle, elephant, elf, fairy, falcon, fish, flamingo, fox, frog, giraffe, goat, goblin, goose, gorilla, griffin, hamster, hare, hawk, hedgehog, hippo, horse, hydra, jaguar, jellyfish, kangaroo, kitten, koala, kraken, leopard, lion, lizard, llama, lobster, mammoth, mermaid, monkey, moose, mouse, octopus, owl, panda, panther, parrot, penguin, phoenix, pig, pigeon, pony, rabbit, raccoon, rat, raven, rhino, rooster, salmon, shark, sheep, shrimp, skunk, snake, spider, squirrel, swan, tiger, tortoise, troll, turkey, turtle, unicorn, vampire, werewolf, whale, wolf, zebra, zombie
|
||||
aardvark, adlet, african_elephant, albatross, alicorn, alligator, alpaca, amarok, ammit, anaconda, anansi, angel, angelfish, ankylosaurus, ant, anteater, antelope, ape, aphid, arctic_fox, arctic_wolf, armadillo, aswang, aurochs, axolotl, baboon, badger, baku, bald_eagle, banshee, barracuda, basilisk, bass, bat, bear, beaver, bee, beetle, behemoth, bengal_tiger, bird, bison, blackbird, blue_jay, blue_whale, boar, bobcat, bonobo, brown_bear, budgerigar, buffalo, bull, bunyip, butterfly, cait_sith, calf, camel, canary, capybara, cardinal, caribou, cassowary, cat, caterpillar, centaur, cerberus, chameleon, cheetah, chicken, chimera, chimpanzee, chipmunk, chupacabra, clam, cobra, cockatoo, cockatrice, cod, cougar, cow, coyote, crab, cricket, crocodile, crow, cyclops, deer, deerhound, dhole, dingo, djinn, dodo, dog, dolphin, donkey, doppelganger, dragon, dragonfly, dryad, duck, dwarf, eagle, eel, elephant, elf, emperor_penguin, emu, erlking, fairy, falcon, faun, fenghuang, ferret, finch, firefly, fish, flamingo, flea, fly, fox, frog, gargoyle, gazelle, gecko, gerbil, ghoul, gibbon, giraffe, gnome, goat, goatling, goblin, goldfish, golem, goose, goosefish, gorilla, grasshopper, great_white_shark, green_iguana, greyhound, griffin, grootslang, groundhog, guinea_pig, gull, hamster, harbor_seal, hare, harpy, hawk, hedgehog, hellhound, heron, herring, hippo, hippogriff, honeybee, horse, hound, huldra, hummingbird, husky, hydra, hyena, ibex, ibis, ifrit, iguana, imp, incubus, jackal, jackalope, jaguar, jellyfish, jiangshi, jinn, jorogumo, kangaroo, kappa, kelpie, kingfisher, kitsune, kitten, kiwi, klabautermann, koala, kobold, komodo_dragon, kraken, ladybug, lamia, lemur, leopard, leshy, leviathan, lich, lion, lizard, llama, lobster, loch_ness_monster, lynx, magpie, mammoth, manatee, mandrake, manta_ray, manticore, mare, mastiff, meerkat, melusine, mermaid, minotaur, mole, monarch_butterfly, monkey, moose, moth, mouse, mule, naga, narwhal, newt, nightingale, nixie, nuckelavee, nymph, octopus, ogre, okapi, oni, opossum, orangutan, orc, ork, ostrich, otter, owl, ox, oyster, panda, panther, parrot, peacock, pegasus, pelican, penguin, phoenix, pig, pigeon, plesiosaur, polar_bear, pony, poodle, porcupine, possum, prawn, pterosaur, pufferfish, qilin, quail, rabbit, raccoon, rakshasa, rat, raven, red_fox, red_panda, reindeer, rhino, roc, rooster, rusalka, salamander, salmon, satyr, scallop, sea_lion, sea_serpent, seahorse, seal, selkie, serval, shark, sheep, shrimp, simurgh, siren, skunk, sleipnir, snail, snake, snow_leopard, sphinx, spider, sprite, squirrel, stag, starfish, stegosaurus, stingray, stork, strigoi, succubus, swallow, swan, sylph, tanuki, tapir, tarantula, tengu, termite, thornback_ray, thunderbird, tiger, timber_wolf, toad, tomte, tortoise, toucan, troll, trout, tuna, turkey, turtle, typhon, undine, unicorn, valkyrie, vampire, velociraptor, viper, vodyanoy, vulture, walrus, warthog, wasp, weasel, wendigo, werewolf, whale, white_tiger, wildcat, wildebeest, witch, wizard, wolf, wolpertinger, wombat, woodpecker, worm, wyvern, yak, yaoguai, yeti, yokai, yurei, zebra, zombie
|
||||
|
||||
|
@@ -0,0 +1 @@
|
||||
abstract_composition,action_shot,aerial_view,ambient_occlusion,anamorphic,aperture_priority,asymmetrical_composition,atmospheric_depth,back_view,background_blur,background_focus,backlit_composition,balanced_composition,bird_eye_view,bokeh,bokeh_background,bottom_up_view,camera_flash,camera_tilt,candid_shot,centered_composition,centered_subject,chaotic_composition,character_close_up,cinematic_angle,cinematic_close_up,cinematic_framing,cinematic_lighting,cinematic_still,clean_background,close_crop,close_focus,close_shot,close_up,cluttered_background,color_grading,composition_study,compositional_balance,compressed_perspective,contact_sheet,contextual_framing,cool_lighting,corner_framing,cropped_composition,cropped_forehead,cropped_subject,crowd_shot,cutaway_shot,deep_depth_of_field,deep_focus,deep_perspective,depth_cue,depth_layering,depth_of_field,detail_shot,diagonal_composition,diagonal_lines,direct_flash,distant_shot,documentary_framing,dolly_zoom,double_exposure,dramatic_angle,dramatic_backlight,dramatic_framing,drone_view,dutch_angle,dutch_composition,dutch_tilt,dynamic_angle,dynamic_composition,editorial_shot,empty_background,environmental_portrait,environmental_shot,establishing_shot,extreme_close_up,extreme_long_shot,extreme_low_angle,extreme_wide_shot,eye_level,eye_level_view,face_focus,face_portrait,fashion_editorial,fast_shutter,fill_the_frame,film_grain,film_still,fine_detail,first_person_view,fisheye,flash_photography,flat_lay,flat_perspective,focal_point,focus_stack,focus_transition,forced_perspective,foreground_blur,foreground_element,foreground_framing,foreground_interest,four_point_perspective,frame_within_frame,framed_subject,freeze_frame,frontal_view,full_body,full_body_shot,full_frame,full_scene,geometric_composition,glossy_photo,golden_ratio,grainy_photo,group_shot,half_body,hand_focus,handheld,head_and_shoulders,hero_shot,high_angle,high_angle_view,high_contrast_lighting,high_fashion_shot,high_key_lighting,horizon_line,horizontal_composition,implied_motion,instant_photo,interior_shot,intimate_framing,isometric_view,landscape_orientation,layered_composition,layered_depth,leading_lines,lens_bokeh,lens_compression,lens_distortion,lens_flare,light_leak,light_rays,long_exposure,long_lens,long_shot,loose_framing,low_angle,low_angle_view,low_key_lighting,low_perspective,macro,macro_shot,magazine_cover,medium_close_up,medium_full_shot,medium_shot,minimal_background,moody_lighting,motion_blur,motion_streaks,natural_framing,natural_light,near_focus,negative_space,negative_space_composition,noir_lighting,object_focus,off_center_composition,one_point_perspective,open_composition,over_the_shoulder,overexposed,overhead_shot,overhead_view,painterly_composition,panning_shot,panoramic_view,partial_view,perspective_depth,photo_portrait,photo_still,photographic_composition,point_of_view,portrait_orientation,portrait_shot,product_shot,profile_shot,profile_view,pull_focus,rack_focus,radial_composition,reaction_shot,rear_view,reflection_shot,rim_lighting,rule_of_thirds,shallow_depth_of_field,shallow_focus,shallow_perspective,side_profile,side_view,silhouette_composition,single_subject,slow_shutter,soft_background,soft_focus,soft_lighting,split_composition,square_composition,studio_lighting,studio_portrait,subject_isolation,subject_tracking,surveillance_view,symmetrical_composition,symmetrical_framing,telephoto,telephoto_compression,three_quarter_view,tight_crop,tight_framing,tilted_frame,top_down_view,tracking_shot,two_point_perspective,underexposed,vanishing_point,vertical_composition,viewpoint,vignette,vintage_photo,wide_angle,wide_composition,wide_lens,wide_shot,worm_eye_view,zoom_blur
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,153 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
const TARGET_NODE = "NukunVocabMultiStringList";
|
||||
const TARGET_DISPLAY_NAME = "Multi Vocab String List (Nukun)";
|
||||
const RANDOM_LABEL = "(random)";
|
||||
const SLOT_COUNT = 4;
|
||||
const WORD_COUNT = 3;
|
||||
const vocabCache = new Map();
|
||||
const configuredNodes = new WeakSet();
|
||||
|
||||
function widget(node, name) {
|
||||
return node.widgets?.find((candidate) => candidate?.name === name);
|
||||
}
|
||||
|
||||
async function loadWords(vocabFile) {
|
||||
if (!vocabFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (vocabCache.has(vocabFile)) {
|
||||
return vocabCache.get(vocabFile);
|
||||
}
|
||||
|
||||
const response = await api.fetchApi(`/nukun/vocab/words?file=${encodeURIComponent(vocabFile)}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load vocabulary: ${vocabFile}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const words = Array.isArray(payload.words) ? payload.words : [];
|
||||
vocabCache.set(vocabFile, words);
|
||||
return words;
|
||||
}
|
||||
|
||||
function configureWordWidget(wordWidget) {
|
||||
if (!wordWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
wordWidget.type = "combo";
|
||||
wordWidget.options = wordWidget.options || {};
|
||||
wordWidget.options.values = wordWidget.options.values || [RANDOM_LABEL];
|
||||
if (!wordWidget.value) {
|
||||
wordWidget.value = RANDOM_LABEL;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWordWidget(wordWidget, words) {
|
||||
if (!wordWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = [RANDOM_LABEL, ...words];
|
||||
wordWidget.type = "combo";
|
||||
wordWidget.options = wordWidget.options || {};
|
||||
wordWidget.options.values = values;
|
||||
|
||||
if (!values.includes(wordWidget.value)) {
|
||||
wordWidget.value = RANDOM_LABEL;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSlotWords(node, slot) {
|
||||
const vocabWidget = widget(node, `vocab_file_${slot}`);
|
||||
const vocabFile = vocabWidget?.value;
|
||||
let words = [];
|
||||
|
||||
try {
|
||||
words = await loadWords(vocabFile);
|
||||
} catch (error) {
|
||||
console.warn(`[Nukun] ${error.message}`);
|
||||
}
|
||||
|
||||
for (let wordIndex = 1; wordIndex <= WORD_COUNT; wordIndex += 1) {
|
||||
updateWordWidget(widget(node, `word_${slot}_${wordIndex}`), words);
|
||||
}
|
||||
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
function setupNode(node) {
|
||||
node.serialize_widgets = true;
|
||||
|
||||
for (let slot = 1; slot <= SLOT_COUNT; slot += 1) {
|
||||
for (let wordIndex = 1; wordIndex <= WORD_COUNT; wordIndex += 1) {
|
||||
configureWordWidget(widget(node, `word_${slot}_${wordIndex}`));
|
||||
}
|
||||
|
||||
const vocabWidget = widget(node, `vocab_file_${slot}`);
|
||||
if (vocabWidget) {
|
||||
const previousCallback = vocabWidget.callback;
|
||||
vocabWidget.callback = function (value, ...args) {
|
||||
const result = previousCallback?.call(this, value, ...args);
|
||||
updateSlotWords(node, slot);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
updateSlotWords(node, slot);
|
||||
}
|
||||
}
|
||||
|
||||
function isTargetNode(nodeType, nodeData) {
|
||||
return (
|
||||
nodeData?.name === TARGET_NODE ||
|
||||
nodeData?.name === TARGET_DISPLAY_NAME ||
|
||||
nodeType?.comfyClass === TARGET_NODE ||
|
||||
nodeType?.title === TARGET_DISPLAY_NAME
|
||||
);
|
||||
}
|
||||
|
||||
function isTargetNodeInstance(node) {
|
||||
return (
|
||||
node?.comfyClass === TARGET_NODE ||
|
||||
node?.comfyClass === TARGET_DISPLAY_NAME ||
|
||||
node?.type === TARGET_NODE ||
|
||||
node?.type === TARGET_DISPLAY_NAME ||
|
||||
node?.title === TARGET_DISPLAY_NAME
|
||||
);
|
||||
}
|
||||
|
||||
function setupTargetNode(node) {
|
||||
if (!node || configuredNodes.has(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
configuredNodes.add(node);
|
||||
setupNode(node);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Nukun.VocabMultiSelect",
|
||||
nodeCreated(node) {
|
||||
if (isTargetNodeInstance(node)) {
|
||||
setupTargetNode(node);
|
||||
}
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (!isTargetNode(nodeType, nodeData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
setupTargetNode(this);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user