218 lines
8.2 KiB
Python
218 lines
8.2 KiB
Python
"""
|
|
OliMegaStringList — Mega String List node.
|
|
|
|
Collects strings from multiple sources into a unified list:
|
|
- optional_prompt_list (LIST) — always included when enable=True, or
|
|
returned unchanged when enable=False (pass-through / chaining)
|
|
- per-row string slots (string1, string2, …) — each slot is a canvas
|
|
widget row with a bullet connector; accepts typed text (widget dict
|
|
{on, text}) or any connected node output (STRING, LIST, etc.)
|
|
|
|
INPUT_IS_LIST = True prevents ComfyUI from batch-expanding LIST outputs
|
|
connected to our string slots — we receive the full list at once and
|
|
insert it at the correct position in the result.
|
|
|
|
Items in the text widget are separated by NEWLINES (one item per line).
|
|
The delimiter is only used to JOIN items in the `string` output.
|
|
Escape sequences are decoded at runtime (\\n → real newline, etc.).
|
|
|
|
Strings equal to "none" (case-insensitive) are filtered from all sources
|
|
— SDXL Prompt Styler empty/ignore sentinel.
|
|
|
|
Outputs:
|
|
prompt_list — LIST for PromptLinePick / easy promptList chaining
|
|
prompt_strings — STRING (output list), same format as easy promptList
|
|
num_strings — INT count of items in the combined list
|
|
string — STRING items joined by delimiter (single value)
|
|
"""
|
|
|
|
|
|
class _AnyType(str):
|
|
"""A type string that compares equal to any other type."""
|
|
|
|
def __ne__(self, other):
|
|
return False
|
|
|
|
|
|
_any = _AnyType("*")
|
|
|
|
|
|
class _FlexibleInputs(dict):
|
|
"""Dict that accepts any key — lets dynamic string slot inputs
|
|
(string1, string2, …) pass ComfyUI validation and reach execute(**kwargs).
|
|
"""
|
|
|
|
def __init__(self, fallback_type, data=None):
|
|
self._fallback = fallback_type
|
|
super().__init__(data or {})
|
|
|
|
def __contains__(self, key):
|
|
return True
|
|
|
|
def __getitem__(self, key):
|
|
return super().__getitem__(key) if key in self.keys() else (self._fallback,)
|
|
|
|
|
|
class OliMegaStringList:
|
|
"""Combine strings from multiple sources into a unified list."""
|
|
|
|
CATEGORY = "Oli/prompt"
|
|
|
|
# Prevents ComfyUI from batch-expanding list inputs; we handle the full
|
|
# list ourselves so that it can be inserted at the correct slot position.
|
|
INPUT_IS_LIST = True
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {},
|
|
"optional": _FlexibleInputs(_any, {
|
|
"optional_prompt_list": ("LIST", {}),
|
|
"enable": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "pass-through"}),
|
|
"delimiter": ("STRING", {"default": ", "}),
|
|
"_disabled_slots": ("STRING", {"default": ""}),
|
|
# string1, string2, … are added dynamically by the JS frontend.
|
|
}),
|
|
}
|
|
|
|
RETURN_TYPES = ("LIST", "STRING", "INT", "STRING")
|
|
RETURN_NAMES = ("prompt_list", "prompt_strings", "num_strings", "string")
|
|
# string output (index 3): OUTPUT_IS_LIST=True + 1-element list return
|
|
# so ComfyUI iterates once and downstream nodes receive a plain STRING.
|
|
# (With INPUT_IS_LIST=True, OUTPUT_IS_LIST=False would wrap the value in
|
|
# a list that some nodes reject as a list instead of a scalar.)
|
|
# string output (index 3): OUTPUT_IS_LIST=True + 1-element list so downstream
|
|
# nodes receive a plain STRING scalar, not a list.
|
|
OUTPUT_IS_LIST = (False, True, False, True)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(
|
|
self,
|
|
enable=None,
|
|
delimiter=None,
|
|
optional_prompt_list=None,
|
|
**kwargs,
|
|
):
|
|
# INPUT_IS_LIST=True wraps scalar widget values in lists; unwrap them.
|
|
enable = _first(_ensure_list(enable), default=True)
|
|
delimiter = _first(_ensure_list(delimiter), default=", ")
|
|
decoded_delim = _decode_escapes(delimiter)
|
|
|
|
# Disabled slots: comma-separated slot names toggled off in the UI.
|
|
disabled_raw = _first(_ensure_list(kwargs.pop("_disabled_slots", None)), default="")
|
|
disabled = set(s.strip() for s in disabled_raw.split(",") if s.strip())
|
|
|
|
# When disabled: pass through optional_prompt_list unchanged.
|
|
if not enable:
|
|
result = []
|
|
for item in _ensure_list(optional_prompt_list):
|
|
result.extend(_val_to_strings(item, decoded_delim))
|
|
result = _filter_none(result)
|
|
num = len(result)
|
|
return (result, result, num, [decoded_delim.join(result)])
|
|
|
|
result = []
|
|
|
|
# 1. optional_prompt_list (chaining input; INPUT_IS_LIST may wrap it)
|
|
for item in _ensure_list(optional_prompt_list):
|
|
result.extend(_val_to_strings(item, decoded_delim))
|
|
|
|
# 2. Per-row string slots — sorted by numeric suffix = widget order
|
|
for key in _sorted_slot_keys(kwargs, "string"):
|
|
if key in disabled:
|
|
continue
|
|
slot_val = kwargs[key]
|
|
for val in _ensure_list(slot_val):
|
|
result.extend(_val_to_strings(val, decoded_delim))
|
|
|
|
result = _filter_none(result)
|
|
num = len(result)
|
|
# string output: 1-element list so OUTPUT_IS_LIST=True passes it as a
|
|
# single STRING to downstream nodes (see OUTPUT_IS_LIST comment above).
|
|
return (result, result, num, [decoded_delim.join(result)])
|
|
|
|
|
|
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
|
|
def _val_to_strings(val, decoded_delim=""):
|
|
"""Recursively convert any input value to a flat list of strings."""
|
|
if val is None:
|
|
return []
|
|
if isinstance(val, dict):
|
|
# Unconnected widget: {on: bool, text: str}
|
|
if not val.get("on", True):
|
|
return []
|
|
text = val.get("text", "")
|
|
return _split_text(text, decoded_delim) if text.strip() else []
|
|
if isinstance(val, list):
|
|
out = []
|
|
for item in val:
|
|
out.extend(_val_to_strings(item, decoded_delim))
|
|
return out
|
|
# Scalar (str, int, …) — skip blank values
|
|
s = str(val)
|
|
return [s] if s.strip() else []
|
|
|
|
|
|
def _split_text(text, delimiter):
|
|
"""Split text by delimiter, stripping and filtering empty parts.
|
|
|
|
If the text starts with the delimiter (e.g. absolute path '/foo' with
|
|
delimiter '/'), the whole text is returned as a single item to avoid
|
|
silently discarding the leading character.
|
|
"""
|
|
if not delimiter:
|
|
stripped = text.strip()
|
|
return [stripped] if stripped else []
|
|
# Leading delimiter → treat whole value as one item (e.g. absolute path)
|
|
if text.lstrip().startswith(delimiter):
|
|
stripped = text.strip()
|
|
return [stripped] if stripped else []
|
|
return [p.strip() for p in text.split(delimiter) if p.strip()]
|
|
|
|
|
|
def _ensure_list(val):
|
|
"""Wrap a non-list value in a list; return [] for None."""
|
|
if val is None:
|
|
return []
|
|
return val if isinstance(val, list) else [val]
|
|
|
|
|
|
def _first(lst, default=None):
|
|
"""Return the first item of a list, or default if empty."""
|
|
return lst[0] if lst else default
|
|
|
|
|
|
def _sorted_slot_keys(mapping, prefix):
|
|
"""Return keys matching prefix+digits, sorted numerically by suffix."""
|
|
pairs = []
|
|
for key in mapping:
|
|
if not key.startswith(prefix):
|
|
continue
|
|
suffix = key[len(prefix):]
|
|
if suffix.isdigit():
|
|
pairs.append((int(suffix), key))
|
|
pairs.sort()
|
|
return [key for _, key in pairs]
|
|
|
|
|
|
def _decode_escapes(s):
|
|
r"""Decode Python string escape sequences (\n → newline, \t → tab, …)."""
|
|
if not s:
|
|
return s
|
|
try:
|
|
return s.encode("raw_unicode_escape").decode("unicode_escape")
|
|
except Exception:
|
|
return s
|
|
|
|
|
|
def _filter_none(lst):
|
|
"""Remove strings equal to 'none' (case-insensitive) — SDXL sentinel."""
|
|
return [s for s in lst if s.strip().lower() != "none"]
|
|
|
|
|
|
# ── Registrations ──────────────────────────────────────────────────────────────
|
|
|
|
NODE_CLASS_MAPPINGS = {"OliMegaStringList": OliMegaStringList}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {"OliMegaStringList": "Mega String List (Oli)"}
|