Refactor architecture and add new essential nodes (v1.5.1)
This commit is contained in:
+37
-64
@@ -1,71 +1,44 @@
|
||||
"""
|
||||
DebugPadawan's ComfyUI Essentials
|
||||
A collection of essential custom nodes for ComfyUI
|
||||
"""
|
||||
import os
|
||||
import importlib
|
||||
import glob
|
||||
|
||||
from .nodes.text_processing import NODE_CLASS_MAPPINGS as TEXT_NODES
|
||||
from .nodes.text_processing import NODE_DISPLAY_NAME_MAPPINGS as TEXT_DISPLAY_NAMES
|
||||
from .nodes.utilities import UTILITY_NODE_CLASS_MAPPINGS as UTILITY_NODES
|
||||
from .nodes.utilities import UTILITY_NODE_DISPLAY_NAME_MAPPINGS as UTILITY_DISPLAY_NAMES
|
||||
from .nodes.timing import TIMING_NODE_CLASS_MAPPINGS as TIMING_NODES
|
||||
from .nodes.timing import TIMING_NODE_DISPLAY_NAME_MAPPINGS as TIMING_DISPLAY_NAMES
|
||||
from .nodes.json import NODE_CLASS_MAPPINGS as JSON_NODES
|
||||
from .nodes.json import NODE_DISPLAY_NAME_MAPPINGS as JSON_DISPLAY_NAMES
|
||||
from .nodes.image import NODE_CLASS_MAPPINGS as IMAGE_NODES
|
||||
from .nodes.image import NODE_DISPLAY_NAME_MAPPINGS as IMAGE_DISPLAY_NAMES
|
||||
from .nodes.color_palette import NODE_CLASS_MAPPINGS as COLOR_PALETTE_NODES
|
||||
from .nodes.color_palette import NODE_DISPLAY_NAME_MAPPINGS as COLOR_PALETTE_DISPLAY_NAMES
|
||||
from .nodes.math_nodes import NODE_CLASS_MAPPINGS as MATH_NODES
|
||||
from .nodes.math_nodes import NODE_DISPLAY_NAME_MAPPINGS as MATH_DISPLAY_NAMES
|
||||
from .nodes.list_nodes import NODE_CLASS_MAPPINGS as LIST_NODES
|
||||
from .nodes.list_nodes import NODE_DISPLAY_NAME_MAPPINGS as LIST_DISPLAY_NAMES
|
||||
# Node registration dictionaries
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
from .nodes.json_to_text import NODE_CLASS_MAPPINGS as JSON_TO_TEXT_NODES
|
||||
from .nodes.json_to_text import NODE_DISPLAY_NAME_MAPPINGS as JSON_TO_TEXT_DISPLAY_NAMES
|
||||
from .nodes.string_formatter import NODE_CLASS_MAPPINGS as STRING_FORMATTER_NODES
|
||||
from .nodes.string_formatter import NODE_DISPLAY_NAME_MAPPINGS as STRING_FORMATTER_DISPLAY_NAMES
|
||||
from .nodes.text_compare import NODE_CLASS_MAPPINGS as TEXT_COMPARE_NODES
|
||||
from .nodes.text_compare import NODE_DISPLAY_NAME_MAPPINGS as TEXT_COMPARE_DISPLAY_NAMES
|
||||
from .nodes.type_conversion import NODE_CLASS_MAPPINGS as TYPE_CONVERSION_NODES
|
||||
from .nodes.type_conversion import NODE_DISPLAY_NAME_MAPPINGS as TYPE_CONVERSION_DISPLAY_NAMES
|
||||
from .nodes.number_utils import NODE_CLASS_MAPPINGS as NUMBER_UTILS_NODES
|
||||
from .nodes.number_utils import NODE_DISPLAY_NAME_MAPPINGS as NUMBER_UTILS_DISPLAY_NAMES
|
||||
# Automatically import all .py files from the nodes directory
|
||||
nodes_dir = os.path.join(os.path.dirname(__file__), "nodes")
|
||||
node_files = glob.glob(os.path.join(nodes_dir, "*.py"))
|
||||
|
||||
# Combine all node mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
**TEXT_NODES,
|
||||
**UTILITY_NODES,
|
||||
**TIMING_NODES,
|
||||
**JSON_NODES,
|
||||
**IMAGE_NODES,
|
||||
**COLOR_PALETTE_NODES,
|
||||
**MATH_NODES,
|
||||
**LIST_NODES,
|
||||
**JSON_TO_TEXT_NODES,
|
||||
**STRING_FORMATTER_NODES,
|
||||
**TEXT_COMPARE_NODES,
|
||||
**TYPE_CONVERSION_NODES,
|
||||
**NUMBER_UTILS_NODES,
|
||||
}
|
||||
for file_path in node_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
if file_name == "__init__.py":
|
||||
continue
|
||||
|
||||
module_name = f".nodes.{file_name[:-3]}"
|
||||
try:
|
||||
# Import the module
|
||||
module = importlib.import_module(module_name, package=__package__)
|
||||
|
||||
# Load mappings if they exist
|
||||
if hasattr(module, "NODE_CLASS_MAPPINGS"):
|
||||
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
||||
|
||||
# UTILITY_NODE_CLASS_MAPPINGS for backward compatibility
|
||||
if hasattr(module, "UTILITY_NODE_CLASS_MAPPINGS"):
|
||||
NODE_CLASS_MAPPINGS.update(module.UTILITY_NODE_CLASS_MAPPINGS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
**TEXT_DISPLAY_NAMES,
|
||||
**UTILITY_DISPLAY_NAMES,
|
||||
**TIMING_DISPLAY_NAMES,
|
||||
**JSON_DISPLAY_NAMES,
|
||||
**IMAGE_DISPLAY_NAMES,
|
||||
**COLOR_PALETTE_DISPLAY_NAMES,
|
||||
**MATH_DISPLAY_NAMES,
|
||||
**LIST_DISPLAY_NAMES,
|
||||
**JSON_TO_TEXT_DISPLAY_NAMES,
|
||||
**STRING_FORMATTER_DISPLAY_NAMES,
|
||||
**TEXT_COMPARE_DISPLAY_NAMES,
|
||||
**TYPE_CONVERSION_DISPLAY_NAMES,
|
||||
**NUMBER_UTILS_DISPLAY_NAMES,
|
||||
}
|
||||
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
# UTILITY_NODE_DISPLAY_NAME_MAPPINGS for backward compatibility
|
||||
if hasattr(module, "UTILITY_NODE_DISPLAY_NAME_MAPPINGS"):
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(module.UTILITY_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DebugPadawan Essentials] Failed to load module {module_name}: {e}")
|
||||
|
||||
# Version and Metadata
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# Version info
|
||||
__version__ = "1.5.0"
|
||||
__version__ = "1.5.1"
|
||||
__author__ = "DebugPadawan"
|
||||
+31
-25
@@ -1,9 +1,11 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
from typing import Tuple
|
||||
|
||||
class ColorPaletteExtractor:
|
||||
"""
|
||||
Node for extracting the most dominant colors from an image
|
||||
Node for extracting the most dominant colors from an image.
|
||||
Uses quantization and frequency analysis for speed and accuracy.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -19,45 +21,49 @@ class ColorPaletteExtractor:
|
||||
FUNCTION = "extract"
|
||||
CATEGORY = "DebugPadawan/Image"
|
||||
|
||||
def extract(self, image, color_count):
|
||||
def extract(self, image: torch.Tensor, color_count: int) -> Tuple[str, str, torch.Tensor]:
|
||||
# Image is typically [B, H, W, C]
|
||||
# We'll take the first image in the batch
|
||||
# We'll take the first image in the batch for analysis
|
||||
img = image[0]
|
||||
h, w, c = img.shape
|
||||
c = img.shape[-1]
|
||||
|
||||
# Rescale for performance using torch
|
||||
img_torch = img.permute(2, 0, 1).unsqueeze(0) # [1, C, H, W]
|
||||
img_small = torch.nn.functional.interpolate(img_torch, size=(128, 128), mode='area')
|
||||
img_np = img_small.squeeze(0).permute(1, 2, 0).numpy()
|
||||
# Using a smaller size for faster processing
|
||||
img_small = torch.nn.functional.interpolate(img_torch, size=(64, 64), mode='area')
|
||||
img_np = img_small.squeeze(0).permute(1, 2, 0).cpu().numpy()
|
||||
|
||||
# Flatten and scale to 0-255
|
||||
pixels = img_np.reshape(-1, c) * 255.0
|
||||
pixels = (img_np.reshape(-1, c) * 255.0).astype(np.int32)
|
||||
|
||||
# Simple quantization
|
||||
pixels = (pixels / 16).astype(int) * 16
|
||||
# Simple quantization (group colors together)
|
||||
# We group by 16 levels to reduce noise
|
||||
pixels = (pixels // 16) * 16
|
||||
|
||||
# Convert to hex strings
|
||||
hex_colors = []
|
||||
for p in pixels:
|
||||
r, g, b = p
|
||||
hex_colors.append(f'#{r:02x}{g:02x}{b:02x}')
|
||||
# Map each pixel to a unique integer color representation (R << 16 | G << 8 | B)
|
||||
# This is much faster than string formatting for all pixels
|
||||
rgb_int = (pixels[:, 0] << 16) | (pixels[:, 1] << 8) | pixels[:, 2]
|
||||
|
||||
# Count frequencies
|
||||
unique, counts = np.unique(hex_colors, return_counts=True)
|
||||
unique, counts = np.unique(rgb_int, return_counts=True)
|
||||
sorted_indices = np.argsort(-counts)
|
||||
|
||||
top_hex = unique[sorted_indices[:color_count]]
|
||||
dominant = top_hex[0] if len(top_hex) > 0 else "#000000"
|
||||
top_colors = unique[sorted_indices[:color_count]]
|
||||
|
||||
# Create a palette image
|
||||
palette_h = 64
|
||||
palette_w = color_count * 64
|
||||
palette_img = np.zeros((palette_h, palette_w, 3), dtype=np.float32)
|
||||
def int_to_hex(val):
|
||||
return f"#{val >> 16 & 0xFF:02x}{val >> 8 & 0xFF:02x}{val & 0xFF:02x}"
|
||||
|
||||
top_hex = [int_to_hex(c) for c in top_colors]
|
||||
dominant = top_hex[0] if top_hex else "#000000"
|
||||
|
||||
for i, hex_color in enumerate(top_hex):
|
||||
r = int(hex_color[1:3], 16) / 255.0
|
||||
g = int(hex_color[3:5], 16) / 255.0
|
||||
b = int(hex_color[5:7], 16) / 255.0
|
||||
# Create a visual palette image
|
||||
p_h, p_w = 64, color_count * 64
|
||||
palette_img = np.zeros((p_h, p_w, 3), dtype=np.float32)
|
||||
|
||||
for i, val in enumerate(top_colors):
|
||||
r = ((val >> 16) & 0xFF) / 255.0
|
||||
g = ((val >> 8) & 0xFF) / 255.0
|
||||
b = (val & 0xFF) / 255.0
|
||||
palette_img[:, i*64:(i+1)*64, 0] = r
|
||||
palette_img[:, i*64:(i+1)*64, 1] = g
|
||||
palette_img[:, i*64:(i+1)*64, 2] = b
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Any, Tuple
|
||||
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
generic_type = AnyType("*")
|
||||
|
||||
class NodeSearch:
|
||||
"""
|
||||
Utility node to list or search available ComfyUI nodes (by class or name).
|
||||
Helpful for developers to find node internal names.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"search_query": ("STRING", {"default": ""}),
|
||||
"search_mode": (["class_name", "display_name", "category"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LIST", "STRING")
|
||||
RETURN_NAMES = ("node_list", "count_summary")
|
||||
FUNCTION = "search_nodes"
|
||||
CATEGORY = "DebugPadawan/Utilities"
|
||||
|
||||
def search_nodes(self, search_query: str, search_mode: str) -> Tuple[list, str]:
|
||||
# This requires access to ComfyUI's internal node mapping
|
||||
# NOTE: In actual execution, we'd need to import it.
|
||||
# This is a bit of a trick as it's usually in `nodes.NODE_CLASS_MAPPINGS`
|
||||
try:
|
||||
import nodes as comfy_nodes
|
||||
mappings = comfy_nodes.NODE_CLASS_MAPPINGS
|
||||
display_names = comfy_nodes.NODE_DISPLAY_NAME_MAPPINGS
|
||||
except ImportError:
|
||||
return ([], "Could not access ComfyUI node mappings")
|
||||
|
||||
results = []
|
||||
query = search_query.lower()
|
||||
|
||||
for k, v in mappings.items():
|
||||
disp = display_names.get(k, k)
|
||||
cat = getattr(v, "CATEGORY", "Unknown")
|
||||
|
||||
match = False
|
||||
if not query:
|
||||
match = True
|
||||
elif search_mode == "class_name" and query in k.lower():
|
||||
match = True
|
||||
elif search_mode == "display_name" and query in disp.lower():
|
||||
match = True
|
||||
elif search_mode == "category" and query in cat.lower():
|
||||
match = True
|
||||
|
||||
if match:
|
||||
results.append(f"{k} | {disp} | {cat}")
|
||||
|
||||
results.sort()
|
||||
return (results, f"Found {len(results)} nodes matching '{search_query}'")
|
||||
|
||||
|
||||
class TextFileLoader:
|
||||
"""
|
||||
Loads text from a file.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"default": "example.txt"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "LIST")
|
||||
RETURN_NAMES = ("content", "lines")
|
||||
FUNCTION = "load_file"
|
||||
CATEGORY = "DebugPadawan/Utilities"
|
||||
|
||||
def load_file(self, file_path: str) -> Tuple[str, list]:
|
||||
if not os.path.exists(file_path):
|
||||
return (f"File not found: {file_path}", [])
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
lines = content.splitlines()
|
||||
return (content, lines)
|
||||
except Exception as e:
|
||||
return (f"Error loading file: {str(e)}", [])
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DebugPadawan_NodeSearch": NodeSearch,
|
||||
"DebugPadawan_TextFileLoader": TextFileLoader,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DebugPadawan_NodeSearch": "Node Search Utility",
|
||||
"DebugPadawan_TextFileLoader": "Load Text File",
|
||||
}
|
||||
+39
-12
@@ -1,8 +1,10 @@
|
||||
import torch
|
||||
from typing import Tuple
|
||||
|
||||
class ImageInfo:
|
||||
"""
|
||||
Node for getting width, height, and batch size from an Image
|
||||
Node for getting width, height, and batch size from an Image.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -13,25 +15,50 @@ class ImageInfo:
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "INT")
|
||||
RETURN_NAMES = ("width", "height", "batch_size")
|
||||
|
||||
FUNCTION = "get_info"
|
||||
CATEGORY = "DebugPadawan/Image"
|
||||
|
||||
def get_info(self, image):
|
||||
"""
|
||||
Extract dimensions from a ComfyUI image tensor
|
||||
ComfyUI images are formatted as [batch_size, height, width, channels]
|
||||
"""
|
||||
batch_size = image.shape[0]
|
||||
height = image.shape[1]
|
||||
width = image.shape[2]
|
||||
def get_info(self, image: torch.Tensor) -> Tuple[int, int, int]:
|
||||
# ComfyUI image tensor format: [B, H, W, C]
|
||||
batch_size = image.shape[0] if len(image.shape) > 0 else 0
|
||||
height = image.shape[1] if len(image.shape) > 1 else 0
|
||||
width = image.shape[2] if len(image.shape) > 2 else 0
|
||||
|
||||
return (width, height, batch_size)
|
||||
|
||||
class ImageBatchSlicer:
|
||||
"""
|
||||
Slices a batch of images to extract a specific range or single image.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"index": ("INT", {"default": 0, "min": 0, "max": 1000}),
|
||||
"count": ("INT", {"default": 1, "min": 1, "max": 1000}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT")
|
||||
RETURN_NAMES = ("sliced_images", "count")
|
||||
FUNCTION = "slice_batch"
|
||||
CATEGORY = "DebugPadawan/Image"
|
||||
|
||||
def slice_batch(self, images: torch.Tensor, index: int, count: int) -> Tuple[torch.Tensor, int]:
|
||||
num_images = images.shape[0]
|
||||
start_idx = min(index, num_images - 1)
|
||||
end_idx = min(start_idx + count, num_images)
|
||||
|
||||
sliced = images[start_idx:end_idx]
|
||||
return (sliced, sliced.shape[0])
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DebugPadawan_ImageInfo": ImageInfo,
|
||||
"DebugPadawan_ImageBatchSlicer": ImageBatchSlicer,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DebugPadawan_ImageInfo": "Image Info",
|
||||
"DebugPadawan_ImageInfo": "Image Dimensions Info",
|
||||
"DebugPadawan_ImageBatchSlicer": "Image Batch Slicer",
|
||||
}
|
||||
|
||||
+113
-80
@@ -1,14 +1,14 @@
|
||||
import torch
|
||||
import random
|
||||
|
||||
from typing import List, Any, Tuple, Union
|
||||
|
||||
class GetListItem:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_list": ("*", {"forceInput": True}),
|
||||
"index": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
"index": ("INT", {"default": 0, "min": -1000000, "max": 1000000}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,36 +17,101 @@ class GetListItem:
|
||||
FUNCTION = "get_item"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def get_item(self, input_list, index):
|
||||
def get_item(self, input_list: Any, index: int) -> Tuple[Any]:
|
||||
if not isinstance(input_list, list):
|
||||
# Attempt to convert to list if it's a ComfyUI tensor batch or similar
|
||||
if isinstance(input_list, torch.Tensor):
|
||||
input_list = input_list.tolist()
|
||||
elif hasattr(input_list, '__iter__') and not isinstance(input_list, str):
|
||||
input_list = list(input_list)
|
||||
else:
|
||||
# If it's a single item, wrap it in a list to allow indexing
|
||||
input_list = [input_list]
|
||||
|
||||
if not input_list:
|
||||
raise ValueError("Input list is empty.")
|
||||
if index < 0 or index >= len(input_list):
|
||||
raise IndexError(f"Index {index} out of bounds for list of length {len(input_list)}.")
|
||||
|
||||
return (input_list[index],)
|
||||
return (None,)
|
||||
|
||||
# Handle negative indexing
|
||||
try:
|
||||
return (input_list[index],)
|
||||
except IndexError:
|
||||
return (input_list[-1] if index >= 0 else input_list[0],)
|
||||
|
||||
|
||||
class ListSlicer:
|
||||
class ListCreate:
|
||||
"""
|
||||
Node for getting a slice of a list
|
||||
Node for creating a list from up to 8 individual inputs.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
f"input_{i}": ("*", {"forceInput": True}) for i in range(1, 9)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT")
|
||||
RETURN_NAMES = ("list", "count")
|
||||
FUNCTION = "create_list"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def create_list(self, **kwargs) -> Tuple[List[Any], int]:
|
||||
result = [v for k, v in kwargs.items() if v is not None]
|
||||
return (result, len(result))
|
||||
|
||||
|
||||
class ListFilter:
|
||||
"""
|
||||
Node for filtering a list based on string matching or numeric bounds.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_list": ("LIST",),
|
||||
"filter_mode": (["contains", "starts_with", "ends_with", "regex", "equals"],),
|
||||
"filter_value": ("STRING", {"default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"exclude": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT")
|
||||
RETURN_NAMES = ("filtered_list", "count")
|
||||
FUNCTION = "filter_list"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def filter_list(self, input_list: List[Any], filter_mode: str, filter_value: str, exclude: bool = False) -> Tuple[List[Any], int]:
|
||||
import re
|
||||
result = []
|
||||
for item in input_list:
|
||||
s_item = str(item)
|
||||
match = False
|
||||
|
||||
if filter_mode == "contains": match = filter_value in s_item
|
||||
elif filter_mode == "starts_with": match = s_item.startswith(filter_value)
|
||||
elif filter_mode == "ends_with": match = s_item.endswith(filter_value)
|
||||
elif filter_mode == "equals": match = s_item == filter_value
|
||||
elif filter_mode == "regex":
|
||||
try:
|
||||
match = bool(re.search(filter_value, s_item))
|
||||
except:
|
||||
match = False
|
||||
|
||||
if exclude: match = not match
|
||||
if match: result.append(item)
|
||||
|
||||
return (result, len(result))
|
||||
|
||||
|
||||
class ListSlicer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_list": ("LIST",),
|
||||
"start": ("INT", {"default": 0, "min": 0}),
|
||||
"end": ("INT", {"default": 0, "min": 0}),
|
||||
"end": ("INT", {"default": 1, "min": 0}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +120,9 @@ class ListSlicer:
|
||||
FUNCTION = "slice_list"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def slice_list(self, input_list, start, end):
|
||||
if end == 0:
|
||||
def slice_list(self, input_list: List, start: int, end: int) -> Tuple[List, int]:
|
||||
# If end is 0 or less than start, we treat as "to the end" or at least 1 item
|
||||
if end <= start:
|
||||
res = input_list[start:]
|
||||
else:
|
||||
res = input_list[start:end]
|
||||
@@ -64,9 +130,6 @@ class ListSlicer:
|
||||
|
||||
|
||||
class ListInfo:
|
||||
"""
|
||||
Node for getting information about a list
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -77,24 +140,16 @@ class ListInfo:
|
||||
|
||||
RETURN_TYPES = ("INT", "STRING", "STRING")
|
||||
RETURN_NAMES = ("count", "first_item", "last_item")
|
||||
|
||||
FUNCTION = "get_list_info"
|
||||
CATEGORY = "DebugPadawan/Utilities"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def get_list_info(self, input_list):
|
||||
count = len(input_list)
|
||||
first_item = str(input_list[0]) if input_list else ""
|
||||
last_item = str(input_list[-1]) if input_list else ""
|
||||
|
||||
return (count, first_item, last_item)
|
||||
def get_list_info(self, input_list: List) -> Tuple[int, str, str]:
|
||||
if not input_list:
|
||||
return (0, "", "")
|
||||
return (len(input_list), str(input_list[0]), str(input_list[-1]))
|
||||
|
||||
|
||||
class RandomListSelector:
|
||||
"""
|
||||
Node for randomly selecting one or more items from a list
|
||||
Supports seeded randomness for reproducible selections
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -108,20 +163,16 @@ class RandomListSelector:
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT", "*")
|
||||
RETURN_NAMES = ("selected_items", "count", "first_item")
|
||||
|
||||
FUNCTION = "select_random"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def select_random(self, input_list, seed, count, allow_duplicates):
|
||||
def select_random(self, input_list: List, seed: int, count: int, allow_duplicates: bool) -> Tuple[List, int, Any]:
|
||||
if not input_list:
|
||||
raise ValueError("Input list is empty.")
|
||||
return ([], 0, None)
|
||||
|
||||
rng = random.Random(seed)
|
||||
list_len = len(input_list)
|
||||
|
||||
if count > list_len and not allow_duplicates:
|
||||
count = list_len
|
||||
|
||||
if allow_duplicates:
|
||||
selected = [rng.choice(input_list) for _ in range(count)]
|
||||
else:
|
||||
@@ -132,11 +183,6 @@ class RandomListSelector:
|
||||
|
||||
|
||||
class ListShuffler:
|
||||
"""
|
||||
Node for shuffling a list with a deterministic seed
|
||||
Useful for randomizing order while maintaining reproducibility
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -148,65 +194,54 @@ class ListShuffler:
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT")
|
||||
RETURN_NAMES = ("shuffled_list", "count")
|
||||
|
||||
FUNCTION = "shuffle_list"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def shuffle_list(self, input_list, seed):
|
||||
def shuffle_list(self, input_list: List, seed: int) -> Tuple[List, int]:
|
||||
if not input_list:
|
||||
return ([], 0)
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
shuffled = list(input_list)
|
||||
rng = random.Random(seed)
|
||||
rng.shuffle(shuffled)
|
||||
|
||||
random.Random(seed).shuffle(shuffled)
|
||||
return (shuffled, len(shuffled))
|
||||
|
||||
|
||||
class ListMerger:
|
||||
"""
|
||||
Node for merging multiple lists together
|
||||
Supports concatenation and interleaving modes
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"list_a": ("LIST",),
|
||||
"list_b": ("LIST",),
|
||||
"mode": (["concatenate", "interleave"],),
|
||||
"mode": (["concatenate", "interleave", "union"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT")
|
||||
RETURN_NAMES = ("merged_list", "count")
|
||||
|
||||
FUNCTION = "merge_lists"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def merge_lists(self, list_a, list_b, mode):
|
||||
def merge_lists(self, list_a: List, list_b: List, mode: str) -> Tuple[List, int]:
|
||||
if mode == "concatenate":
|
||||
result = list_a + list_b
|
||||
else: # interleave
|
||||
elif mode == "interleave":
|
||||
result = []
|
||||
max_len = max(len(list_a), len(list_b))
|
||||
for i in range(max_len):
|
||||
if i < len(list_a):
|
||||
result.append(list_a[i])
|
||||
if i < len(list_b):
|
||||
result.append(list_b[i])
|
||||
for i in range(max(len(list_a), len(list_b))):
|
||||
if i < len(list_a): result.append(list_a[i])
|
||||
if i < len(list_b): result.append(list_b[i])
|
||||
elif mode == "union":
|
||||
seen = set()
|
||||
result = []
|
||||
for item in list_a + list_b:
|
||||
s = str(item)
|
||||
if s not in seen:
|
||||
seen.add(s)
|
||||
result.append(item)
|
||||
|
||||
return (result, len(result))
|
||||
|
||||
|
||||
class ListDeduplicator:
|
||||
"""
|
||||
Node for removing duplicate items from a list
|
||||
Preserves original order
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -217,43 +252,41 @@ class ListDeduplicator:
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT", "INT")
|
||||
RETURN_NAMES = ("deduplicated_list", "count", "removed_count")
|
||||
|
||||
FUNCTION = "deduplicate"
|
||||
CATEGORY = "DebugPadawan/List"
|
||||
|
||||
def deduplicate(self, input_list):
|
||||
seen = []
|
||||
def deduplicate(self, input_list: List) -> Tuple[List, int, int]:
|
||||
seen = set()
|
||||
result = []
|
||||
for item in input_list:
|
||||
# Convert to string for comparison (handles non-hashable types)
|
||||
item_key = str(item)
|
||||
if item_key not in seen:
|
||||
seen.append(item_key)
|
||||
seen.add(item_key)
|
||||
result.append(item)
|
||||
|
||||
removed = len(input_list) - len(result)
|
||||
return (result, len(result), removed)
|
||||
return (result, len(result), len(input_list) - len(result))
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DebugPadawan_GetListItem": GetListItem,
|
||||
"DebugPadawan_ListCreate": ListCreate,
|
||||
"DebugPadawan_ListFilter": ListFilter,
|
||||
"DebugPadawan_ListSlicer": ListSlicer,
|
||||
"DebugPadawan_ListInfo": ListInfo,
|
||||
"DebugPadawan_RandomListSelector": RandomListSelector,
|
||||
"DebugPadawan_ListShuffler": ListShuffler,
|
||||
"DebugPadawan_ListMerger": ListMerger,
|
||||
"DebugPadawan_ListDeduplicator": ListDeduplicator,
|
||||
# Alias for backward compatibility
|
||||
"DP_GetListItem": GetListItem,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DebugPadawan_GetListItem": "Get List Item",
|
||||
"DebugPadawan_ListCreate": "List Create (Multi-Input)",
|
||||
"DebugPadawan_ListFilter": "List Filter",
|
||||
"DebugPadawan_ListSlicer": "List Slicer",
|
||||
"DebugPadawan_ListInfo": "List Info",
|
||||
"DebugPadawan_RandomListSelector": "Random List Selector",
|
||||
"DebugPadawan_ListShuffler": "List Shuffler",
|
||||
"DebugPadawan_ListMerger": "List Merger",
|
||||
"DebugPadawan_ListDeduplicator": "List Deduplicator",
|
||||
"DP_GetListItem": "Get List Item (Legacy)",
|
||||
}
|
||||
|
||||
+86
-39
@@ -1,25 +1,18 @@
|
||||
import random
|
||||
import math
|
||||
from typing import Tuple, List, Union
|
||||
|
||||
class BaseMathOperation:
|
||||
"""
|
||||
Base class for math operations to reduce duplication
|
||||
"""
|
||||
def _perform_calculation(self, a, b, operation, is_int=True):
|
||||
if operation == "add":
|
||||
res = a + b
|
||||
elif operation == "subtract":
|
||||
res = a - b
|
||||
elif operation == "multiply":
|
||||
res = a * b
|
||||
elif operation == "divide":
|
||||
res = a / b if b != 0 else 0
|
||||
elif operation == "modulo":
|
||||
res = a % b if b != 0 else 0
|
||||
elif operation == "power":
|
||||
res = math.pow(a, b)
|
||||
else:
|
||||
res = 0
|
||||
def _perform_calculation(self, a: float, b: float, operation: str, is_int: bool = True) -> Tuple[Union[int, float], Union[float, int]]:
|
||||
res = 0.0
|
||||
if operation == "add": res = a + b
|
||||
elif operation == "subtract": res = a - b
|
||||
elif operation == "multiply": res = a * b
|
||||
elif operation == "divide": res = a / b if b != 0 else 0.0
|
||||
elif operation == "modulo": res = a % b if b != 0 else 0.0
|
||||
elif operation == "power": res = math.pow(a, b)
|
||||
elif operation == "max": res = max(a, b)
|
||||
elif operation == "min": res = min(a, b)
|
||||
|
||||
if is_int:
|
||||
return (int(res), float(res))
|
||||
@@ -27,16 +20,13 @@ class BaseMathOperation:
|
||||
return (float(res), int(res))
|
||||
|
||||
class IntMathOperation(BaseMathOperation):
|
||||
"""
|
||||
Node for performing basic integer math operations
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"a": ("INT", {"default": 0, "step": 1}),
|
||||
"b": ("INT", {"default": 0, "step": 1}),
|
||||
"operation": (["add", "subtract", "multiply", "divide", "modulo", "power"],),
|
||||
"operation": (["add", "subtract", "multiply", "divide", "modulo", "power", "max", "min"],),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,20 +36,16 @@ class IntMathOperation(BaseMathOperation):
|
||||
CATEGORY = "DebugPadawan/Math"
|
||||
|
||||
def perform_math(self, a, b, operation):
|
||||
return self._perform_calculation(a, b, operation, is_int=True)
|
||||
|
||||
return self._perform_calculation(float(a), float(b), operation, is_int=True)
|
||||
|
||||
class FloatMathOperation(BaseMathOperation):
|
||||
"""
|
||||
Node for performing basic float math operations
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"a": ("FLOAT", {"default": 0.0, "step": 0.01}),
|
||||
"b": ("FLOAT", {"default": 0.0, "step": 0.01}),
|
||||
"operation": (["add", "subtract", "multiply", "divide", "power"],),
|
||||
"operation": (["add", "subtract", "multiply", "divide", "power", "max", "min"],),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +57,72 @@ class FloatMathOperation(BaseMathOperation):
|
||||
def perform_math(self, a, b, operation):
|
||||
return self._perform_calculation(a, b, operation, is_int=False)
|
||||
|
||||
class SingleNumberOp:
|
||||
"""
|
||||
Operations on a single number (floor, ceil, rounded, abs, sin, cos).
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("FLOAT", {"default": 0.0, "step": 0.001}),
|
||||
"operation": (["floor", "ceil", "round", "abs", "sin", "cos", "sqrt", "negate"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT", "INT")
|
||||
RETURN_NAMES = ("float_val", "int_val")
|
||||
FUNCTION = "op"
|
||||
CATEGORY = "DebugPadawan/Math"
|
||||
|
||||
def op(self, value: float, operation: str) -> Tuple[float, int]:
|
||||
res = 0.0
|
||||
if operation == "floor": res = float(math.floor(value))
|
||||
elif operation == "ceil": res = float(math.ceil(value))
|
||||
elif operation == "round": res = float(round(value))
|
||||
elif operation == "abs": res = abs(value)
|
||||
elif operation == "sin": res = math.sin(value)
|
||||
elif operation == "cos": res = math.cos(value)
|
||||
elif operation == "sqrt": res = math.sqrt(value) if value >= 0 else 0.0
|
||||
elif operation == "negate": res = -value
|
||||
|
||||
return (res, int(res))
|
||||
|
||||
class MathExpression:
|
||||
"""
|
||||
Evaluates simple mathematical expressions.
|
||||
BE CAREFUL: Uses eval(), so keep it strictly math only.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"expression": ("STRING", {"default": "a * (b + 10)"}),
|
||||
"a": ("FLOAT", {"default": 1.0}),
|
||||
"b": ("FLOAT", {"default": 1.0}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT", "INT")
|
||||
RETURN_NAMES = ("float_val", "int_val")
|
||||
FUNCTION = "eval_expr"
|
||||
CATEGORY = "DebugPadawan/Math"
|
||||
|
||||
def eval_expr(self, expression: str, a: float, b: float) -> Tuple[float, int]:
|
||||
# Basic sanitization
|
||||
safe_dict = {"a": a, "b": b, "math": math, "abs": abs, "round": round, "min": min, "max": max}
|
||||
try:
|
||||
# This is still slightly risky but okay for a local tool.
|
||||
# We should use a proper parser if this were production web.
|
||||
# But for ComfyUI, users usually have full local access anyway.
|
||||
result = eval(expression, {"__builtins__": {}}, safe_dict)
|
||||
f_res = float(result)
|
||||
return (f_res, int(f_res))
|
||||
except Exception as e:
|
||||
print(f"[MathExpression Error] {e}")
|
||||
return (0.0, 0)
|
||||
|
||||
class RandomGenerator:
|
||||
"""
|
||||
Node for generating random integers or floats based on a seed
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -93,25 +140,25 @@ class RandomGenerator:
|
||||
CATEGORY = "DebugPadawan/Math"
|
||||
|
||||
def generate(self, seed, min_val, max_val, mode):
|
||||
# Initialize the random generator with the seed
|
||||
rng = random.Random(seed)
|
||||
|
||||
if mode == "float":
|
||||
res = rng.uniform(min_val, max_val)
|
||||
return (float(res), int(res))
|
||||
else:
|
||||
# For int mode, ensure min and max are integers
|
||||
res = rng.randint(int(min_val), int(max_val))
|
||||
return (float(res), int(res))
|
||||
return (float(res), int(res))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DebugPadawan_IntMathOperation": IntMathOperation,
|
||||
"DebugPadawan_FloatMathOperation": FloatMathOperation,
|
||||
"DebugPadawan_SingleNumberOp": SingleNumberOp,
|
||||
"DebugPadawan_MathExpression": MathExpression,
|
||||
"DebugPadawan_RandomGenerator": RandomGenerator,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DebugPadawan_IntMathOperation": "Int Math Operation",
|
||||
"DebugPadawan_FloatMathOperation": "Float Math Operation",
|
||||
"DebugPadawan_RandomGenerator": "Random Generator",
|
||||
"DebugPadawan_IntMathOperation": "Integer Math",
|
||||
"DebugPadawan_FloatMathOperation": "Float Math",
|
||||
"DebugPadawan_SingleNumberOp": "Number Transform",
|
||||
"DebugPadawan_MathExpression": "Math Expression Solver",
|
||||
"DebugPadawan_RandomGenerator": "Random Number Gen",
|
||||
}
|
||||
|
||||
+166
-210
@@ -1,46 +1,44 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
class TextSplitter:
|
||||
"""
|
||||
Node for splitting text strings by a delimiter
|
||||
Node for splitting text strings by a delimiter.
|
||||
Supports simple strings or regular expressions.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "apple,banana,orange"
|
||||
}),
|
||||
"delimiter": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ","
|
||||
}),
|
||||
"text": ("STRING", {"multiline": True, "default": "apple,banana,orange"}),
|
||||
"delimiter": ("STRING", {"multiline": False, "default": ","}),
|
||||
"is_regex": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"strip_whitespace": ("BOOLEAN", {
|
||||
"default": True
|
||||
}),
|
||||
"remove_empty": ("BOOLEAN", {
|
||||
"default": True
|
||||
}),
|
||||
"strip_whitespace": ("BOOLEAN", {"default": True}),
|
||||
"remove_empty": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LIST", "INT")
|
||||
RETURN_NAMES = ("text_list", "count")
|
||||
|
||||
FUNCTION = "split_text"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def split_text(self, text, delimiter, strip_whitespace=True, remove_empty=True):
|
||||
"""
|
||||
Split text by delimiter and return list with count
|
||||
"""
|
||||
result = text.split(delimiter)
|
||||
def split_text(self, text: str, delimiter: str, is_regex: bool = False, strip_whitespace: bool = True, remove_empty: bool = True) -> Tuple[List[str], int]:
|
||||
if not text:
|
||||
return ([], 0)
|
||||
|
||||
if is_regex:
|
||||
try:
|
||||
result = re.split(delimiter, text)
|
||||
except re.error:
|
||||
# Fallback to simple split if regex is invalid
|
||||
result = text.split(delimiter)
|
||||
else:
|
||||
result = text.split(delimiter)
|
||||
|
||||
if strip_whitespace:
|
||||
result = [item.strip() for item in result]
|
||||
@@ -53,7 +51,7 @@ class TextSplitter:
|
||||
|
||||
class TextJoiner:
|
||||
"""
|
||||
Node for joining a list of strings with a delimiter
|
||||
Node for joining a list of strings with a delimiter.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -61,206 +59,184 @@ class TextJoiner:
|
||||
return {
|
||||
"required": {
|
||||
"text_list": ("LIST",),
|
||||
"delimiter": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ", "
|
||||
}),
|
||||
"delimiter": ("STRING", {"multiline": False, "default": ", "}),
|
||||
},
|
||||
"optional": {
|
||||
"prefix": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ""
|
||||
}),
|
||||
"suffix": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ""
|
||||
}),
|
||||
"prefix": ("STRING", {"multiline": False, "default": ""}),
|
||||
"suffix": ("STRING", {"multiline": False, "default": ""}),
|
||||
"skip_empty": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("joined_text",)
|
||||
|
||||
FUNCTION = "join_text"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def join_text(self, text_list, delimiter, prefix="", suffix=""):
|
||||
def join_text(self, text_list: List, delimiter: str, prefix: str = "", suffix: str = "", skip_empty: bool = True) -> Tuple[str]:
|
||||
if not text_list:
|
||||
return (prefix + suffix,)
|
||||
|
||||
str_list = [str(item) for item in text_list]
|
||||
if skip_empty:
|
||||
str_list = [s for s in str_list if s.strip()]
|
||||
|
||||
result = delimiter.join(str_list)
|
||||
result = prefix + result + suffix
|
||||
return (result,)
|
||||
|
||||
|
||||
class TextReplace:
|
||||
class TextTemplate:
|
||||
"""
|
||||
Node for replacing specific text in a string
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "A beautiful sunset over the ocean"
|
||||
}),
|
||||
"find": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": "sunset"
|
||||
}),
|
||||
"replace": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": "sunrise"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
|
||||
FUNCTION = "replace_text"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def replace_text(self, text, find, replace):
|
||||
if not find:
|
||||
return (text,)
|
||||
return (text.replace(find, replace),)
|
||||
|
||||
|
||||
class TextRegex:
|
||||
"""
|
||||
Node for searching and replacing text using regular expressions
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "My phone number is 123-456-7890"
|
||||
}),
|
||||
"pattern": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": r"\d{3}-\d{3}-\d{4}"
|
||||
}),
|
||||
"replace": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": "[REDACTED]"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "LIST", "BOOLEAN")
|
||||
RETURN_NAMES = ("text", "matches", "found")
|
||||
|
||||
FUNCTION = "regex_op"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def regex_op(self, text, pattern, replace):
|
||||
if not pattern:
|
||||
return (text, [], False)
|
||||
|
||||
matches = re.findall(pattern, text)
|
||||
result = re.sub(pattern, replace, text)
|
||||
|
||||
return (result, matches, len(matches) > 0)
|
||||
|
||||
|
||||
class TextCaseConverter:
|
||||
"""
|
||||
Node for converting text to different case styles
|
||||
Useful for formatting prompts, filenames, and display text
|
||||
Node for flexible string templating using Python's format method.
|
||||
Useful for constructing complex prompts or filenames.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "hello world"
|
||||
}),
|
||||
"case_mode": (["UPPER", "lower", "Title Case", "Sentence case", "snake_case", "kebab-case", "camelCase", "PascalCase"],),
|
||||
"template": ("STRING", {"multiline": True, "default": "A {subject} in the style of {artist}"}),
|
||||
"input_1": ("*", {"forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"input_2": ("*", {"forceInput": True}),
|
||||
"input_3": ("*", {"forceInput": True}),
|
||||
"input_4": ("*", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("formatted_text",)
|
||||
FUNCTION = "format_template"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def format_template(self, template: str, input_1=None, input_2=None, input_3=None, input_4=None) -> Tuple[str]:
|
||||
try:
|
||||
# We use a simple replacement if the user doesn't use {0}, {1} etc.
|
||||
# but rather named or positional arguments.
|
||||
# To be most flexible, we provide both positional and 'valX' names.
|
||||
kwargs = {
|
||||
"val1": input_1, "val2": input_2, "val3": input_3, "val4": input_4,
|
||||
"input_1": input_1, "input_2": input_2, "input_3": input_3, "input_4": input_4
|
||||
}
|
||||
result = template.format(input_1, input_2, input_3, input_4, **kwargs)
|
||||
return (result,)
|
||||
except Exception as e:
|
||||
return (f"Error: {str(e)}",)
|
||||
|
||||
|
||||
class TextCaseConverter:
|
||||
"""
|
||||
Comprehensive case converter for text.
|
||||
Correctly handles snake_case, camelCase, etc.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"multiline": True, "default": "hello world"}),
|
||||
"mode": (["UPPER", "lower", "Title Case", "Sentence case", "snake_case", "kebab-case", "camelCase", "PascalCase"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("converted_text",)
|
||||
|
||||
FUNCTION = "convert_case"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def convert_case(self, text, case_mode):
|
||||
"""
|
||||
Convert text to the specified case style
|
||||
"""
|
||||
if case_mode == "UPPER":
|
||||
def convert_case(self, text: str, mode: str) -> Tuple[str]:
|
||||
if not text:
|
||||
return ("",)
|
||||
|
||||
if mode == "UPPER":
|
||||
return (text.upper(),)
|
||||
elif case_mode == "lower":
|
||||
elif mode == "lower":
|
||||
return (text.lower(),)
|
||||
elif case_mode == "Title Case":
|
||||
elif mode == "Title Case":
|
||||
return (text.title(),)
|
||||
elif case_mode == "Sentence case":
|
||||
# Capitalize first letter of each sentence
|
||||
result = '. '.join(s.capitalize() for s in text.split('. '))
|
||||
return (result,)
|
||||
elif case_mode == "snake_case":
|
||||
# Convert to snake_case
|
||||
# First normalize spaces and existing separators
|
||||
normalized = text.lower().replace('-', ' ').replace('_', ' ')
|
||||
words = normalized.split()
|
||||
return ('_'.join(words),)
|
||||
elif case_mode == "kebab-case":
|
||||
# Convert to kebab-case
|
||||
normalized = text.lower().replace('_', ' ').replace('-', ' ')
|
||||
words = normalized.split()
|
||||
return ('-'.join(words),)
|
||||
elif case_mode == "camelCase":
|
||||
# Convert to camelCase
|
||||
normalized = text.replace('-', ' ').replace('_', ' ')
|
||||
words = normalized.split()
|
||||
if not words:
|
||||
return ("",)
|
||||
result = words[0].lower() + ''.join(w.capitalize() for w in words[1:])
|
||||
return (result,)
|
||||
elif case_mode == "PascalCase":
|
||||
# Convert to PascalCase
|
||||
normalized = text.replace('-', ' ').replace('_', ' ')
|
||||
words = normalized.split()
|
||||
result = ''.join(w.capitalize() for w in words)
|
||||
return (result,)
|
||||
else:
|
||||
elif mode == "Sentence case":
|
||||
return ('. '.join(s.strip().capitalize() for s in text.split('. ')),)
|
||||
|
||||
# For complex cases, first tokenize
|
||||
# Replace non-alphanumeric with spaces, then split
|
||||
words = re.sub(r'[^a-zA-Z0-9]', ' ', text).split()
|
||||
if not words:
|
||||
return (text,)
|
||||
|
||||
if mode == "snake_case":
|
||||
return ('_'.join(w.lower() for w in words),)
|
||||
elif mode == "kebab-case":
|
||||
return ('-'.join(w.lower() for w in words),)
|
||||
elif mode == "camelCase":
|
||||
return (words[0].lower() + ''.join(w.capitalize() for w in words[1:]),)
|
||||
elif mode == "PascalCase":
|
||||
return (''.join(w.capitalize() for w in words),)
|
||||
|
||||
return (text,)
|
||||
|
||||
class TextTrimmer:
|
||||
|
||||
class TextRegex:
|
||||
"""
|
||||
Node for trimming text with various options
|
||||
Advanced Regex node for search, replace, and extraction.
|
||||
"""
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": " hello world "
|
||||
}),
|
||||
"mode": (["both", "start", "end", "all_whitespace", "collapse_spaces"],),
|
||||
"text": ("STRING", {"multiline": True, "default": "My phone is 123-456-7890"}),
|
||||
"pattern": ("STRING", {"multiline": False, "default": r"(\d{3})-(\d{3}-\d{4})"}),
|
||||
"replace": ("STRING", {"multiline": False, "default": r"(\1) \2"}),
|
||||
},
|
||||
"optional": {
|
||||
"flags": (["None", "IGNORECASE", "MULTILINE", "DOTALL"], {"default": "None"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "LIST", "BOOLEAN")
|
||||
RETURN_NAMES = ("text", "matches", "found")
|
||||
FUNCTION = "regex_op"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def regex_op(self, text: str, pattern: str, replace: str = "", flags: str = "None") -> Tuple[str, List[str], bool]:
|
||||
if not pattern:
|
||||
return (text, [], False)
|
||||
|
||||
re_flags = 0
|
||||
if flags == "IGNORECASE": re_flags = re.IGNORECASE
|
||||
elif flags == "MULTILINE": re_flags = re.MULTILINE
|
||||
elif flags == "DOTALL": re_flags = re.DOTALL
|
||||
|
||||
try:
|
||||
compiled = re.compile(pattern, re_flags)
|
||||
matches = compiled.findall(text)
|
||||
# findall returns tuples if there are multiple groups, let's flatten or stringify
|
||||
str_matches = [str(m) for m in matches]
|
||||
|
||||
result = compiled.sub(replace, text)
|
||||
return (result, str_matches, len(matches) > 0)
|
||||
except re.error as e:
|
||||
return (f"Regex Error: {str(e)}", [], False)
|
||||
|
||||
|
||||
class TextTrimmer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"multiline": True, "default": " hello world "}),
|
||||
"mode": (["both", "start", "end", "all_whitespace", "collapse_spaces", "remove_newlines"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("trimmed_text",)
|
||||
|
||||
FUNCTION = "trim_text"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def trim_text(self, text, mode):
|
||||
"""
|
||||
Trim text based on the specified mode
|
||||
"""
|
||||
def trim_text(self, text: str, mode: str) -> Tuple[str]:
|
||||
if mode == "both":
|
||||
return (text.strip(),)
|
||||
elif mode == "start":
|
||||
@@ -268,70 +244,49 @@ class TextTrimmer:
|
||||
elif mode == "end":
|
||||
return (text.rstrip(),)
|
||||
elif mode == "all_whitespace":
|
||||
# Remove all whitespace characters
|
||||
return (''.join(text.split()),)
|
||||
elif mode == "collapse_spaces":
|
||||
# Collapse multiple spaces into single spaces
|
||||
return (' '.join(text.split()),)
|
||||
else:
|
||||
return (text,)
|
||||
elif mode == "remove_newlines":
|
||||
return (text.replace('\n', ' ').replace('\r', ' '),)
|
||||
return (text,)
|
||||
|
||||
|
||||
class TextPrefixSuffix:
|
||||
"""
|
||||
Node for adding prefix and/or suffix to text
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "beautiful"
|
||||
}),
|
||||
"text": ("STRING", {"multiline": True, "default": "beautiful"}),
|
||||
},
|
||||
"optional": {
|
||||
"prefix": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ""
|
||||
}),
|
||||
"suffix": ("STRING", {
|
||||
"multiline": False,
|
||||
"default": ""
|
||||
}),
|
||||
"prefix": ("STRING", {"multiline": False, "default": ""}),
|
||||
"suffix": ("STRING", {"multiline": False, "default": ""}),
|
||||
"add_space": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("result_text",)
|
||||
|
||||
FUNCTION = "add_prefix_suffix"
|
||||
CATEGORY = "DebugPadawan/Text"
|
||||
|
||||
def add_prefix_suffix(self, text, prefix="", suffix="", add_space=True):
|
||||
result = text
|
||||
|
||||
def add_prefix_suffix(self, text: str, prefix: str = "", suffix: str = "", add_space: bool = True) -> Tuple[str]:
|
||||
res = text
|
||||
if prefix:
|
||||
if add_space and text and not text.startswith(' '):
|
||||
result = prefix + ' ' + result
|
||||
else:
|
||||
result = prefix + result
|
||||
|
||||
connector = " " if add_space and not prefix.endswith(" ") and not text.startswith(" ") else ""
|
||||
res = f"{prefix}{connector}{res}"
|
||||
if suffix:
|
||||
if add_space and text and not text.endswith(' '):
|
||||
result = result + ' ' + suffix
|
||||
else:
|
||||
result = result + suffix
|
||||
|
||||
return (result,)
|
||||
connector = " " if add_space and not res.endswith(" ") and not suffix.startswith(" ") else ""
|
||||
res = f"{res}{connector}{suffix}"
|
||||
return (res,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DebugPadawan_TextSplitter": TextSplitter,
|
||||
"DebugPadawan_TextJoiner": TextJoiner,
|
||||
"DebugPadawan_TextReplace": TextReplace,
|
||||
"DebugPadawan_TextTemplate": TextTemplate,
|
||||
"DebugPadawan_TextReplace": TextRegex, # Merged replace into Regex or kept separate
|
||||
"DebugPadawan_TextRegex": TextRegex,
|
||||
"DebugPadawan_TextCaseConverter": TextCaseConverter,
|
||||
"DebugPadawan_TextTrimmer": TextTrimmer,
|
||||
@@ -341,8 +296,9 @@ NODE_CLASS_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DebugPadawan_TextSplitter": "Text Splitter",
|
||||
"DebugPadawan_TextJoiner": "Text Joiner",
|
||||
"DebugPadawan_TextReplace": "Text Replace",
|
||||
"DebugPadawan_TextRegex": "Text Regex (Search & Replace)",
|
||||
"DebugPadawan_TextTemplate": "Text Template (Format)",
|
||||
"DebugPadawan_TextReplace": "Text Replace (Legacy)",
|
||||
"DebugPadawan_TextRegex": "Text Regex (Pro)",
|
||||
"DebugPadawan_TextCaseConverter": "Text Case Converter",
|
||||
"DebugPadawan_TextTrimmer": "Text Trimmer",
|
||||
"DebugPadawan_TextPrefixSuffix": "Text Prefix Suffix",
|
||||
|
||||
Reference in New Issue
Block a user