544 lines
24 KiB
Python
544 lines
24 KiB
Python
# ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||
# █▓▒░ ░▒▓█
|
||
# █▓▒░ MD_Nodes/AdvancedTextNode – Text input with wildcards v1.9.0 ░▒▓█
|
||
# █▓▒░ ░▒▓█
|
||
# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||
# ╠═ © 2026 MDMAchine
|
||
# ╠═ License: GNU General Public License v3.0 (GPL v3)
|
||
# ║
|
||
# ║ This program is free software: you can redistribute it and/or modify
|
||
# ║ it under the terms of the GNU General Public License as published by
|
||
# ║ the Free Software Foundation, either version 3 of the License, or
|
||
# ║ (at your option) any later version.
|
||
# ║
|
||
# ║ This program is distributed in the hope that it will be useful,
|
||
# ║ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# ║ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# ║ GNU General Public License for more details.
|
||
# ║
|
||
# ║ You should have received a copy of the GNU General Public License
|
||
# ║ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
# ╠════════════════════════════════════════════════════════════════════════════
|
||
# ║ ░▒▓ ORIGIN & DEV:
|
||
# ║ • Cast into the void by: MDMAchine
|
||
# ║ • Enhanced by: Gemini, Claude
|
||
# ║
|
||
# ║ ░▒▓ DESCRIPTION:
|
||
# ║ A versatile text input node with seed-controlled wildcard support, text
|
||
# ║ transformations (case, whitespace), and a companion Text File Loader node.
|
||
# ║ NOTE: As a string utility node, this runs entirely in the public wrapper domain.
|
||
# ║
|
||
# ║ ░▒▓ FEATURES:
|
||
# ║ ✓ Large multiline text input.
|
||
# ║ ✓ Seed-controlled wildcards: {option1|option2} or __option1|option2__.
|
||
# ║ ✓ Nested wildcard support with robust error handling.
|
||
# ║ ✓ Text transformations: lowercase, uppercase, whitespace control.
|
||
# ║ ✓ Multiple seed modes: fixed, random, increment (auto-increment for batch).
|
||
# ║ ✓ Companion 'Text File Loader' node for external file import.
|
||
# ║
|
||
# ║ ░▒▓ CHANGELOG:
|
||
# ║ - v1.9.0 (Enterprise Standards - Feb 2026):
|
||
# ║ • ADDED: PerformanceProfiler class (v1.5.3 standard).
|
||
# ║ • ADDED: debug_mode parameter.
|
||
# ║ • REFACTOR: Tooltips strictly updated to 5-part v1.5.4 standard.
|
||
# ║ - v1.8.0 (Seed Precision Fix - Nov 2025):
|
||
# ║ • CRITICAL FIX: Capped seed range to JavaScript's MAX_SAFE_INTEGER.
|
||
# ╚════════════════════════════════════════════════════════════════════════════
|
||
|
||
# =================================================================================
|
||
# == Standard Library Imports ==
|
||
# =================================================================================
|
||
VERSION = "v1.9.0" # UPS v1.5.8
|
||
|
||
|
||
import logging
|
||
import os
|
||
import random
|
||
import re
|
||
import secrets
|
||
import traceback
|
||
import time
|
||
|
||
# =================================================================================
|
||
# == Seed Constants (JavaScript Safe Range) ==
|
||
# =================================================================================
|
||
SEED_MIN = 0
|
||
SEED_MAX = 9007199254740991 # JS-safe range for full reproducibility
|
||
|
||
# =================================================================================
|
||
# == Helper Classes (Enterprise Standards) ==
|
||
# =================================================================================
|
||
|
||
class PerformanceProfiler:
|
||
"""Standard performance profiler for MD_Nodes."""
|
||
def __init__(self, enabled=True):
|
||
self.enabled = enabled
|
||
self.timings = {}
|
||
self.start_times = {}
|
||
|
||
def start(self, operation_name):
|
||
if not self.enabled: return
|
||
self.start_times[operation_name] = time.perf_counter()
|
||
|
||
def stop(self, operation_name):
|
||
if not self.enabled: return
|
||
if operation_name in self.start_times:
|
||
elapsed = time.perf_counter() - self.start_times[operation_name]
|
||
if operation_name not in self.timings:
|
||
self.timings[operation_name] = []
|
||
self.timings[operation_name].append(elapsed)
|
||
del self.start_times[operation_name]
|
||
|
||
def get_total_time(self):
|
||
if not self.enabled or not self.timings: return 0.0
|
||
return sum(sum(times) for times in self.timings.values())
|
||
|
||
def print_report(self):
|
||
if not self.enabled or not self.timings: return
|
||
logging.info("\n⏱️ PERFORMANCE (Text Parse):")
|
||
total = self.get_total_time()
|
||
logging.info(f" • Total Time: {total:.4f}s")
|
||
for op_name, times in sorted(self.timings.items()):
|
||
avg = sum(times) / len(times)
|
||
if len(times) == 1:
|
||
logging.info(f" • {op_name}: {avg:.4f}s")
|
||
else:
|
||
logging.info(f" • {op_name}: {avg:.4f}s avg ({len(times)}x)")
|
||
|
||
# =================================================================================
|
||
# == Core Node Class: AdvancedTextNode ==
|
||
# =================================================================================
|
||
|
||
class AdvancedTextNode:
|
||
"""
|
||
A versatile text input node with seed-controlled wildcard processing,
|
||
text transformations, and multiple output options including text length.
|
||
"""
|
||
|
||
_PATTERN_CURLY = re.compile(r'\{([^{}]+?)\}')
|
||
_PATTERN_UNDERSCORE = re.compile(r'__([^_]+?)__')
|
||
|
||
@staticmethod
|
||
def _validate_seed(seed_value):
|
||
try:
|
||
int_value = int(seed_value)
|
||
except (ValueError, TypeError):
|
||
logging.warning(f"[AdvancedTextNode] Invalid seed value: {seed_value}. Using {SEED_MIN}.")
|
||
return SEED_MIN
|
||
|
||
if int_value > SEED_MAX:
|
||
logging.warning(f"[AdvancedTextNode] Seed {int_value} exceeds JS-safe range. Clamping to {SEED_MAX}.")
|
||
return SEED_MAX
|
||
elif int_value < SEED_MIN:
|
||
logging.warning(f"[AdvancedTextNode] Seed {int_value} below minimum. Clamping to {SEED_MIN}.")
|
||
return SEED_MIN
|
||
|
||
return int_value
|
||
|
||
@staticmethod
|
||
def _generate_random_seed():
|
||
return secrets.randbelow(SEED_MAX + 1)
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"text": ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"dynamicPrompts": False,
|
||
"tooltip": (
|
||
"TEXT INPUT\n"
|
||
"• Purpose: Main text input area for prompts, YAML, JSON, etc.\n"
|
||
"• Usage: Type raw text or use wildcards like {option1|option2}.\n"
|
||
"• Note: Multi-line compatible."
|
||
)
|
||
}),
|
||
},
|
||
"optional": {
|
||
"seed": ("INT", {
|
||
"default": 0,
|
||
"min": SEED_MIN,
|
||
"max": SEED_MAX,
|
||
"step": 1,
|
||
"tooltip": (
|
||
"RANDOM SEED\n"
|
||
"• Purpose: Controls randomization for wildcard selection.\n"
|
||
"• Range: 0 to 9,007,199,254,740,991 (JS Safe Max).\n"
|
||
"• Note: Ignored if 'seed_mode' is set to 'random'.\n"
|
||
"\n⭐ Recommended: Fixed value for reproducible generation."
|
||
)
|
||
}),
|
||
"seed_mode": (["fixed", "random", "increment"], {
|
||
"default": "fixed",
|
||
"tooltip": (
|
||
"SEED MODE\n"
|
||
"• Purpose: Defines how the seed updates per execution.\n"
|
||
"• Options:\n"
|
||
" - fixed: Uses the exact seed value provided.\n"
|
||
" - random: Generates a new seed every run.\n"
|
||
" - increment: Auto-adds 1 to the seed (great for batches).\n"
|
||
"\n⭐ Recommended: 'fixed' for testing, 'increment' for batching."
|
||
)
|
||
}),
|
||
"seed_list": ("STRING", {
|
||
"default": "",
|
||
"multiline": True,
|
||
"tooltip": (
|
||
"SEED LIST (WILDCARD)\n"
|
||
"• Purpose: Randomly pick ONE seed from a wildcard string.\n"
|
||
"• Example: {1234|5678|9012} or __42|777|1337__\n"
|
||
"• Output: Outputs chosen value to the 'selected_seed' INT.\n"
|
||
"\n⭐ Recommended: Leave empty unless injecting specific seeds."
|
||
)
|
||
}),
|
||
"seed_offset": ("INT", {
|
||
"default": 0,
|
||
"min": SEED_MIN,
|
||
"max": SEED_MAX,
|
||
"step": 1,
|
||
"tooltip": (
|
||
"SEED OFFSET\n"
|
||
"• Purpose: Adds an integer offset to the seed before processing seed_list.\n"
|
||
"• Effect: Helps reduce repetition in batch workflows.\n"
|
||
"\n⭐ Recommended: 0."
|
||
)
|
||
}),
|
||
"wildcard_mode": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "enabled",
|
||
"label_off": "disabled",
|
||
"tooltip": (
|
||
"WILDCARD MODE\n"
|
||
"• Purpose: Enable processing of {opt1|opt2} or __opt1|opt2__ patterns.\n"
|
||
"• Effect: If disabled, text passes through exactly as written.\n"
|
||
"\n⭐ Recommended: True if writing dynamic prompts."
|
||
)
|
||
}),
|
||
"strip_whitespace": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "enabled",
|
||
"label_off": "disabled",
|
||
"tooltip": (
|
||
"STRIP WHITESPACE\n"
|
||
"• Purpose: Removes leading/trailing whitespace from the ENTIRE block.\n"
|
||
"\n⭐ Recommended: False (unless cleaning bad JSON/YAML)."
|
||
)
|
||
}),
|
||
"lowercase": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "enabled",
|
||
"label_off": "disabled",
|
||
"tooltip": (
|
||
"FORCE LOWERCASE\n"
|
||
"• Purpose: Converts entire output text to lowercase.\n"
|
||
"• Note: Overrides 'Force Uppercase' if both are true.\n"
|
||
"\n⭐ Recommended: False."
|
||
)
|
||
}),
|
||
"uppercase": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "enabled",
|
||
"label_off": "disabled",
|
||
"tooltip": (
|
||
"FORCE UPPERCASE\n"
|
||
"• Purpose: Converts entire output text to UPPERCASE.\n"
|
||
"\n⭐ Recommended: False."
|
||
)
|
||
}),
|
||
"remove_extra_spaces": ("BOOLEAN", {
|
||
"default": True,
|
||
"label_on": "enabled",
|
||
"label_off": "disabled",
|
||
"tooltip": (
|
||
"REMOVE EXTRA SPACES\n"
|
||
"• Purpose: Collapses multiple spaces into single spaces.\n"
|
||
"\n⭐ Recommended: True (keeps prompts clean)."
|
||
)
|
||
}),
|
||
"wildcard_syntax": (["curly_braces", "double_underscore"], {
|
||
"default": "curly_braces",
|
||
"tooltip": (
|
||
"WILDCARD SYNTAX\n"
|
||
"• Purpose: Choose the pattern style for wildcard parsing.\n"
|
||
"• Options: {curly_braces} or __double_underscore__.\n"
|
||
"\n⭐ Recommended: curly_braces."
|
||
)
|
||
}),
|
||
"debug_mode": (["0 - Silent", "1 - Info"], {
|
||
"default": "0 - Silent",
|
||
"tooltip": "LOGGING VERBOSITY\n• Controls console output and parser profiling."
|
||
}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "INT", "INT")
|
||
RETURN_NAMES = ("processed_text", "original_text", "seed_used", "selected_seed", "text_length", "wildcard_count")
|
||
FUNCTION = "process_text"
|
||
CATEGORY = "MD_Nodes/Text"
|
||
|
||
@classmethod
|
||
def IS_CHANGED(cls, text, seed=0, seed_mode="fixed", seed_list="", seed_offset=0, wildcard_mode=False, strip_whitespace=False,
|
||
lowercase=False, uppercase=False, remove_extra_spaces=True,
|
||
wildcard_syntax="curly_braces", debug_mode="0 - Silent"):
|
||
if seed_mode in ["random", "increment"]:
|
||
return secrets.token_hex(16)
|
||
return (text, seed, seed_mode, seed_list, seed_offset, wildcard_mode, strip_whitespace, lowercase, uppercase,
|
||
remove_extra_spaces, wildcard_syntax)
|
||
|
||
def _process_wildcards_recursive(self, text, pattern, rng):
|
||
iteration = 0
|
||
wildcard_count = 0
|
||
max_iterations = 100
|
||
|
||
while iteration < max_iterations:
|
||
match = pattern.search(text)
|
||
if not match:
|
||
break
|
||
|
||
options_str = match.group(1)
|
||
options = [opt.strip() for opt in options_str.split('|') if opt.strip()]
|
||
|
||
if not options:
|
||
logging.warning(f"[AdvancedTextNode] Empty wildcard detected: {match.group(0)}. Replacing with empty string.")
|
||
options = [""]
|
||
|
||
chosen_option = rng.choice(options)
|
||
text = text[:match.start()] + chosen_option + text[match.end():]
|
||
iteration += 1
|
||
wildcard_count += 1
|
||
|
||
if iteration == max_iterations:
|
||
logging.warning("[AdvancedTextNode] Max wildcard processing iterations reached. Possible runaway recursion?")
|
||
|
||
return text, wildcard_count
|
||
|
||
def process_wildcards_curly(self, text, seed):
|
||
rng = random.Random(seed)
|
||
return self._process_wildcards_recursive(text, self._PATTERN_CURLY, rng)
|
||
|
||
def process_wildcards_underscore(self, text, seed):
|
||
rng = random.Random(seed)
|
||
return self._process_wildcards_recursive(text, self._PATTERN_UNDERSCORE, rng)
|
||
|
||
def process_text(self, text, seed=0, seed_mode="fixed", seed_list="", seed_offset=0, wildcard_mode=False, strip_whitespace=False,
|
||
lowercase=False, uppercase=False, remove_extra_spaces=True,
|
||
wildcard_syntax="curly_braces", debug_mode="0 - Silent"):
|
||
|
||
debug_level = int(debug_mode.split(" ")[0])
|
||
profiler = PerformanceProfiler(enabled=(debug_level >= 1))
|
||
profiler.start("total_processing")
|
||
|
||
original_text = text
|
||
processed_text = text
|
||
seed_used = seed
|
||
selected_seed = 0
|
||
wildcard_count = 0
|
||
|
||
try:
|
||
# Handle seed_mode
|
||
if seed_mode == "random":
|
||
seed_used = self._generate_random_seed()
|
||
if debug_level >= 1: print(f"[AdvancedTextNode] 🎲 Random mode: Generated new seed: {seed_used}")
|
||
elif seed_mode == "increment":
|
||
seed_used = self._validate_seed((seed + 1) % (SEED_MAX + 1))
|
||
if debug_level >= 1: print(f"[AdvancedTextNode] ➕ Increment mode: Using seed: {seed_used}")
|
||
else:
|
||
seed_used = self._validate_seed(seed)
|
||
|
||
# Process seed_list if provided
|
||
if seed_list and seed_list.strip():
|
||
profiler.start("parse_seed_list")
|
||
selection_seed = self._validate_seed((seed_used + seed_offset) % (SEED_MAX + 1))
|
||
|
||
seed_list_wc_count = 0
|
||
if wildcard_syntax == "curly_braces":
|
||
selected_seed_str, seed_list_wc_count = self.process_wildcards_curly(seed_list, selection_seed)
|
||
elif wildcard_syntax == "double_underscore":
|
||
selected_seed_str, seed_list_wc_count = self.process_wildcards_underscore(seed_list, selection_seed)
|
||
else:
|
||
selected_seed_str = seed_list
|
||
|
||
wildcard_count += seed_list_wc_count
|
||
|
||
try:
|
||
selected_seed_raw = int(selected_seed_str.strip())
|
||
selected_seed = self._validate_seed(selected_seed_raw)
|
||
if selected_seed != selected_seed_raw and debug_level >= 1:
|
||
logging.warning(f"[AdvancedTextNode] ⚠️ Seed from list ({selected_seed_raw}) clamped to JS-safe: {selected_seed}")
|
||
except ValueError:
|
||
logging.warning(f"[AdvancedTextNode] Could not convert '{selected_seed_str}' to integer. Using {SEED_MIN}.")
|
||
selected_seed = SEED_MIN
|
||
profiler.stop("parse_seed_list")
|
||
else:
|
||
selected_seed = seed_used
|
||
|
||
if wildcard_mode:
|
||
profiler.start("parse_wildcards")
|
||
text_wc_count = 0
|
||
if wildcard_syntax == "curly_braces":
|
||
processed_text, text_wc_count = self.process_wildcards_curly(processed_text, seed_used)
|
||
elif wildcard_syntax == "double_underscore":
|
||
processed_text, text_wc_count = self.process_wildcards_underscore(processed_text, seed_used)
|
||
wildcard_count += text_wc_count
|
||
profiler.stop("parse_wildcards")
|
||
|
||
profiler.start("string_formatting")
|
||
if strip_whitespace:
|
||
processed_text = processed_text.strip()
|
||
|
||
if remove_extra_spaces:
|
||
processed_text = re.sub(r' +', ' ', processed_text)
|
||
processed_text = '\n'.join(line.strip() for line in processed_text.split('\n'))
|
||
|
||
if lowercase:
|
||
processed_text = processed_text.lower()
|
||
elif uppercase:
|
||
processed_text = processed_text.upper()
|
||
profiler.stop("string_formatting")
|
||
|
||
text_length = len(processed_text)
|
||
|
||
profiler.stop("total_processing")
|
||
if debug_level >= 1:
|
||
logging.info("\n" + "=" * 60)
|
||
logging.info("📊 [AdvancedTextNode] ANALYTICS REPORT")
|
||
logging.info("=" * 60)
|
||
logging.info("📝 TEXT STATS:")
|
||
logging.info(f" • Text Length: {text_length} chars")
|
||
logging.info(f" • Wildcards: {wildcard_count} processed")
|
||
profiler.print_report()
|
||
logging.info("=" * 60)
|
||
|
||
return (processed_text, original_text, seed_used, selected_seed, text_length, wildcard_count)
|
||
|
||
except Exception as e:
|
||
logging.error(f"[AdvancedTextNode] Error processing text: {e}")
|
||
logging.debug(traceback.format_exc())
|
||
return (original_text, original_text, seed_used, 0, len(original_text), 0)
|
||
|
||
# =================================================================================
|
||
# == Core Node Class: TextFileLoader ==
|
||
# =================================================================================
|
||
|
||
class TextFileLoader:
|
||
"""Companion node to load text content from external files."""
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"file_path": ("STRING", {
|
||
"default": "",
|
||
"multiline": False,
|
||
"tooltip": (
|
||
"FILE PATH\n"
|
||
"• Purpose: Full path to the text file (.txt, .yaml, .json, etc.).\n"
|
||
"• Note: Supports relative paths from ComfyUI root or absolute paths.\n"
|
||
"• Example: `C:/data/config.yaml`"
|
||
)
|
||
}),
|
||
},
|
||
"optional": {
|
||
"encoding": (["utf-8", "ascii", "latin-1"], {
|
||
"default": "utf-8",
|
||
"tooltip": (
|
||
"FILE ENCODING\n"
|
||
"• Purpose: Character encoding of the file.\n"
|
||
"• Tip: If you see garbled text, try a different encoding.\n"
|
||
"\n⭐ Recommended: utf-8"
|
||
)
|
||
}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
RETURN_NAMES = ("text",)
|
||
FUNCTION = "load_file"
|
||
CATEGORY = "MD_Nodes/Text"
|
||
|
||
@classmethod
|
||
def IS_CHANGED(cls, file_path, encoding="utf-8"):
|
||
try:
|
||
norm_path = os.path.normpath(file_path)
|
||
if not os.path.exists(norm_path):
|
||
m_time = -1
|
||
else:
|
||
m_time = os.path.getmtime(norm_path)
|
||
except Exception as e:
|
||
logging.warning(f"[TextFileLoader] IS_CHANGED check failed for path '{file_path}': {e}")
|
||
m_time = -2
|
||
return (norm_path, encoding, m_time)
|
||
|
||
def load_file(self, file_path, encoding="utf-8"):
|
||
logging.info(f"[TextFileLoader] 📂 Attempting to load file: {file_path}")
|
||
try:
|
||
if not file_path or not isinstance(file_path, str):
|
||
error_msg = "[TextFileLoader] Error: Invalid file path provided."
|
||
logging.error(error_msg)
|
||
return (error_msg,)
|
||
|
||
if not os.path.exists(file_path):
|
||
error_msg = f"[TextFileLoader] Error: File not found at path: {file_path}"
|
||
logging.error(error_msg)
|
||
return (error_msg,)
|
||
|
||
if not os.path.isfile(file_path):
|
||
error_msg = f"[TextFileLoader] Error: Path exists but is not a file: {file_path}"
|
||
logging.error(error_msg)
|
||
return (error_msg,)
|
||
|
||
with open(file_path, 'r', encoding=encoding) as f:
|
||
text = f.read()
|
||
logging.info(f"[TextFileLoader] ✅ Successfully loaded file: {file_path}")
|
||
return (text,)
|
||
|
||
except Exception as e:
|
||
error_msg = f"[TextFileLoader] Error loading file '{file_path}': {e}"
|
||
logging.error(error_msg)
|
||
logging.debug(traceback.format_exc())
|
||
return (error_msg,)
|
||
|
||
# =================================================================================
|
||
# == Node Registration ==
|
||
# =================================================================================
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"AdvancedTextNode": AdvancedTextNode,
|
||
"TextFileLoader": TextFileLoader,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"AdvancedTextNode": "MD: Advanced Text Input",
|
||
"TextFileLoader": "MD: Text File Loader",
|
||
}
|
||
|
||
|
||
# ==============================================================================
|
||
# == Unit Tests (smoke — runs without ComfyUI)
|
||
# ==============================================================================
|
||
|
||
if __name__ == "__main__":
|
||
print("\n🧪 Smoke tests: MD_TextInput")
|
||
print(" VERSION :", VERSION)
|
||
_pass = _fail = 0
|
||
|
||
def _check(label, expr):
|
||
global _pass, _fail
|
||
if expr:
|
||
print(f" ✅ {label}")
|
||
_pass += 1
|
||
else:
|
||
print(f" ❌ {label}")
|
||
_fail += 1
|
||
|
||
_check("VERSION defined", VERSION == "v1.9.0")
|
||
_check("NODE_CLASS_MAPPINGS defined",
|
||
isinstance(NODE_CLASS_MAPPINGS, dict) and len(NODE_CLASS_MAPPINGS) > 0)
|
||
_check(" class AdvancedTextNode in map", "AdvancedTextNode" in NODE_CLASS_MAPPINGS)
|
||
_check(" class TextFileLoader in map", "TextFileLoader" in NODE_CLASS_MAPPINGS)
|
||
|
||
print(f"\n {_pass} passed, {_fail} failed")
|
||
if _fail == 0:
|
||
print(" 🎉 All good.")
|