Add Z-Image prompt refining and Qwen conditioning support

This commit is contained in:
OnekoSL
2026-06-10 20:24:35 +02:00
parent 1f0fff7201
commit acfc51135a
15 changed files with 2596 additions and 61 deletions
+5
View File
@@ -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.
+18 -1
View File
@@ -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.
+4
View File
@@ -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
+189
View File
@@ -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)",
}
+34 -18
View File
@@ -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)",
}
+39 -37
View File
@@ -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
View File
@@ -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
1 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
+1
View File
@@ -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
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
View File
File diff suppressed because one or more lines are too long
+153
View File
@@ -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;
};
},
});