This commit is contained in:
AWWalker
2026-03-18 16:20:23 -04:00
parent 1c43f81177
commit 5a8d041865
64 changed files with 683 additions and 683 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# http://editorconfig.org
# http://editorconfig.org
root = true
+1 -1
View File
@@ -1,2 +1,2 @@
# Auto detect text files and perform LF normalization
# Auto detect text files and perform LF normalization
* text=auto
+19 -19
View File
@@ -1,17 +1,17 @@
# Byte-compiled / optimized / DLL files
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# OSX useful to ignore
# OSX useful to ignore
*.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
# Thumbnails
._*
# Files that might appear in the root of a volume
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
@@ -20,17 +20,17 @@ __pycache__/
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# C extensions
# C extensions
*.so
# Distribution / packaging
# Distribution / packaging
.Python
env/
venv/
@@ -49,17 +49,17 @@ var/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
@@ -71,30 +71,30 @@ coverage.xml
.hypothesis/
.pytest_cache/
# Translations
# Translations
*.mo
*.pot
# Django stuff:
# Django stuff:
*.log
# Sphinx documentation
# Sphinx documentation
docs/_build/
# IntelliJ Idea
# IntelliJ Idea
.idea
*.iml
*.ipr
*.iws
# PyBuilder
# PyBuilder
target/
# Cookiecutter
# Cookiecutter
output/
python_boilerplate/
cookiecutter-pypackage-env/
# vscode settings
# vscode settings
.history/
*.code-workspace
+5 -5
View File
@@ -4,7 +4,7 @@ import os
import sys
from .pysssss import init, get_ext_dir
# High-Compatibility ANSI Colors
# High-Compatibility ANSI Colors
MAGENTA_BRIGHT = "\033[95m"
BLUE_BRIGHT = "\033[94m"
CYAN_BRIGHT = "\033[96m"
@@ -13,8 +13,8 @@ CYAN = "\033[1;36m"
MAGENTA = "\033[1;35m"
RESET = "\033[0m"
# 5-Line Gradient ASCII Header: EmAySee (Block Style)
# Each line is wrapped individually to force the color shift
# 5-Line Gradient ASCII Header: EmAySee (Block Style)
# Each line is wrapped individually to force the color shift
banner = f"""
{MAGENTA_BRIGHT} ███████╗███╗ ███╗ █████╗ ██╗ ██╗ ██████╗███████╗███████╗{RESET}
{MAGENTA_BRIGHT} ██╔════╝████╗ ████║██╔══██╗╚██╗ ██╔╝██╔════╝██╔════╝██╔════╝{RESET}
@@ -49,14 +49,14 @@ if init():
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
# Success Checkmark
# Success Checkmark
print(f" {GREEN}[✓]{RESET} {module_name}")
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
except Exception as e:
# Failure Cross
# Failure Cross
print(f" \033[1;31m[✗]\033[0m {module_name} : Error -> {str(e)}")
print(f"{MAGENTA}----------------------------------------------------------{RESET}")
BIN
View File
Binary file not shown.
+10 -10
View File
@@ -8,38 +8,38 @@ class EmAySee_AnyPassthrough:
def INPUT_TYPES(s):
return {
"required": {
# Input connection that accepts any data type (*).
# You can rename this input visually in the UI.
# Input connection that accepts any data type (*).
# You can rename this input visually in the UI.
"input_any": ("*",),
}
}
# The node outputs the same type as the input (any type)
# The node outputs the same type as the input (any type)
RETURN_TYPES = ("*",)
# You can rename the output connection visually in the UI as well.
# You can rename the output connection visually in the UI as well.
RETURN_NAMES = ("output_any",)
# Category for the ComfyUI menu
# Category for the ComfyUI menu
CATEGORY = "EmAySee_Utils"
# Title displayed on the node
# Title displayed on the node
TITLE = "EmAySee Any Passthrough"
# The method that will be executed when the node runs
# The method that will be executed when the node runs
FUNCTION = "pass_through_any"
def pass_through_any(self, input_any):
"""
Simply returns the input data of any type.
"""
# The node's logic is just to return the input data.
# The node's logic is just to return the input data.
return (input_any,)
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_AnyPassthrough": EmAySee_AnyPassthrough
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_AnyPassthrough": "EmAySee Any Passthrough"
}
+1 -1
View File
@@ -18,7 +18,7 @@ class EmAySee_CalculateAspectRatio:
def EmAySee_calculate_dimensions(self, image, output_height):
batch_size, original_height, original_width, channels = image.shape
# Calculate the new width based on the aspect ratio
# Calculate the new width based on the aspect ratio
ratio = original_width / original_height
output_width = int(output_height * ratio)
+3 -3
View File
@@ -17,10 +17,10 @@ class EmAySee_CheckboxFloatNode:
FUNCTION = "checkbox_to_float"
def checkbox_to_float(self, checkbox_input):
if checkbox_input: # If checkbox_input is True (checked)
if checkbox_input: # If checkbox_input is True (checked)
return (1.0,)
else: # If checkbox_input is False (unchecked)
return (0.0,) # Or you could return (0.0,) if you prefer 0 when unchecked
else: # If checkbox_input is False (unchecked)
return (0.0,) # Or you could return (0.0,) if you prefer 0 when unchecked
NODE_CLASS_MAPPINGS = {
"EmAySee_CheckboxFloatNode": EmAySee_CheckboxFloatNode
+8 -8
View File
@@ -35,14 +35,14 @@ class EmAySee_ConditionalResize:
if method == "lanczos":
s = samples.clone()
# Lanczos kernel implementation usually requires custom handling or specific libraries in standard Torch.
# Using bicubic as a fallback for standard torch interpolate if lanczos isn't explicitly defined in context,
# but standard ComfyUI `common_upscale` handles this.
# To keep this dependency-free and simple torch:
# We will use bicubic for lanczos slot here or implement standard resize.
# Actually, simpler to map standard torch modes.
# Let's use bicubic for 'lanczos' to avoid external dependencies,
# or simple bicubic with antialias=True if supported.
# Lanczos kernel implementation usually requires custom handling or specific libraries in standard Torch.
# Using bicubic as a fallback for standard torch interpolate if lanczos isn't explicitly defined in context,
# but standard ComfyUI `common_upscale` handles this.
# To keep this dependency-free and simple torch:
# We will use bicubic for lanczos slot here or implement standard resize.
# Actually, simpler to map standard torch modes.
# Let's use bicubic for 'lanczos' to avoid external dependencies,
# or simple bicubic with antialias=True if supported.
mode = "bicubic"
else:
mode = method
+7 -7
View File
@@ -21,13 +21,13 @@ class EmAySee_ContextManager:
CATEGORY = "EmAySee/Automation"
def manage_context(self, file_path, history_count, mode, force_reset, new_generation=""):
# Force absolute path to avoid "Ghost" file issues in ComfyUI temp folders
# Force absolute path to avoid "Ghost" file issues in ComfyUI temp folders
abs_path = os.path.abspath(file_path)
if force_reset and os.path.exists(abs_path):
os.remove(abs_path)
# Helper to read blocks
# Helper to read blocks
def get_blocks(path):
if not os.path.exists(path) or os.path.getsize(path) == 0:
return []
@@ -39,11 +39,11 @@ class EmAySee_ContextManager:
blocks = get_blocks(abs_path)
if mode == "Read_Context":
# SEEDING: If file is empty, use the input wire as the current state
# SEEDING: If file is empty, use the input wire as the current state
if not blocks:
if new_generation and new_generation.strip():
seed = new_generation.strip()
# Write immediately so the Append node sees it later in the same cycle
# Write immediately so the Append node sees it later in the same cycle
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(seed + "\n\n")
f.flush()
@@ -56,17 +56,17 @@ class EmAySee_ContextManager:
if mode == "Append_and_Prune":
if new_generation and new_generation.strip():
# Avoid doubling up if Read_Context already seeded it
# Avoid doubling up if Read_Context already seeded it
if not blocks or blocks[-1] != new_generation.strip():
blocks.append(new_generation.strip())
pruned = blocks[-history_count:]
# ATOMIC WRITE
# ATOMIC WRITE
with open(abs_path, 'w', encoding='utf-8') as f:
f.write("\n\n".join(pruned))
f.flush()
os.fsync(f.fileno()) # Forces Windows/Linux to write to disk NOW
os.fsync(f.fileno()) # Forces Windows/Linux to write to disk NOW
return (new_generation if new_generation else "",)
+6 -6
View File
@@ -16,7 +16,7 @@ def get_blocks(path):
return []
with open(path, 'r', encoding='utf-8') as f:
data = f.read()
# V5 RE-DESIGNED REGEX: Looks for PROMPT and the LAST occurrence of STATE in a chunk
# V5 RE-DESIGNED REGEX: Looks for PROMPT and the LAST occurrence of STATE in a chunk
found = re.findall(r"(PROMPT:.*?STATE:.*?)(?=PROMPT:|\Z)", data, re.IGNORECASE | re.DOTALL)
return [b.strip() for b in found if b.strip()]
@@ -70,12 +70,12 @@ class EmAySee_ContextWriter:
text = new_generation.strip()
# --- V5 REPAIR LOGIC ---
# 1. Ensure PROMPT header
# --- V5 REPAIR LOGIC ---
# 1. Ensure PROMPT header
if not re.search(r"^PROMPT:", text, re.IGNORECASE):
text = "PROMPT: " + text
# 2. Check for STATE header (case insensitive)
# 2. Check for STATE header (case insensitive)
if not re.search(r"STATE:", text, re.IGNORECASE):
print("--- [V5 WRITER] Repairing Missing STATE ---")
text = text + "\n\nSTATE: [Repaired Placeholder]"
@@ -83,10 +83,10 @@ class EmAySee_ContextWriter:
abs_path = os.path.join(get_comfy_root(), file_name)
blocks = get_blocks(abs_path)
# Deduplication check
# Deduplication check
if not (blocks and blocks[-1][:100] == text[:100]):
blocks.append(text)
# Keep only the requested history depth
# Keep only the requested history depth
atomic_write(abs_path, blocks[-history_count:])
return (text,)
+2 -2
View File
@@ -21,7 +21,7 @@ class EmAySee_ContextManager_V2:
CATEGORY = "EmAySee_Automation"
def manage_context(self, file_path, history_count, mode, force_reset, new_generation=""):
# ABSOLUTE PATH LOGGING
# ABSOLUTE PATH LOGGING
abs_path = os.path.abspath(file_path)
print(f"--- [EmAySee V2] DEBUG: Working Directory is {os.getcwd()} ---")
print(f"--- [EmAySee V2] DEBUG: Looking for file at {abs_path} ---")
@@ -42,7 +42,7 @@ class EmAySee_ContextManager_V2:
with open(path, 'r', encoding='utf-8') as f:
data = f.read()
# This regex is strict. If your file doesn't have "PROMPT:" it will return nothing.
# This regex is strict. If your file doesn't have "PROMPT:" it will return nothing.
found = re.findall(r"(PROMPT:.*?STATE:.*?)(?=PROMPT:|\Z)", data, re.DOTALL)
print(f"--- [EmAySee V2] DEBUG: Found {len(found)} valid PROMPT/STATE blocks. ---")
return [b.strip() for b in found if b.strip()]
+6 -6
View File
@@ -16,28 +16,28 @@ class EmAySee_ContextWriter_V5:
CATEGORY = "EmAySee_Automation/Context"
def write_context(self, file_path, text_to_append, max_history):
# 1. Truncation Guard
# 1. Truncation Guard
if "[REASONING TRUNCATED]" in text_to_append:
raise ValueError(f"CRITICAL ERROR: LLM Output was truncated. Workflow halted to prevent history corruption.")
# 2. Path Validation
# 2. Path Validation
if not os.path.exists(os.path.dirname(file_path)):
os.makedirs(os.path.dirname(file_path), mode=0o777, exist_ok=True)
# 3. Read existing to manage history length
# 3. Read existing to manage history length
history = []
if os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
history = [block for block in content.split("---") if "STATE:" in block]
# 4. Append New Entry
# 4. Append New Entry
history.append(text_to_append)
# 5. Keep only the last N entries
# 5. Keep only the last N entries
history = history[-max_history:]
# 6. Rewrite file
# 6. Rewrite file
with open(file_path, "w", encoding="utf-8") as f:
f.write("---".join(history))
+1 -1
View File
@@ -121,7 +121,7 @@ class EmAySee_DeepReasoningConnector:
if stop_on_error: raise e
return (f"Error: {str(e)}", "", "ERROR")
# Parsing results
# Parsing results
think_pattern = r'<(?:think|thought)>(.*?)</(?:think|thought)>'
think_match = re.search(think_pattern, full_content, flags=re.DOTALL | re.IGNORECASE)
+3 -3
View File
@@ -7,7 +7,7 @@ class EmAySee_DynamicStringSelectorNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"string_input_1": ("STRING", {"default": "Initial Input"}), # Added initial input
"string_input_1": ("STRING", {"default": "Initial Input"}), # Added initial input
"select_index": ("INT", {"default": 1, "min": 1, "max": 1, "step": 1, "display": "slider", "label": "Select Input"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}, "optional": {}}
@@ -20,7 +20,7 @@ class EmAySee_DynamicStringSelectorNode:
@classmethod
def IS_CHANGED(s, *args):
return float("nan") # Never cache, so the node updates every time
return float("nan") # Never cache, so the node updates every time
@classmethod
def VALIDATE_INPUTS(s, select_index, seed, **kwargs):
@@ -31,7 +31,7 @@ class EmAySee_DynamicStringSelectorNode:
def EmAySee_function(self, select_index, seed, **kwargs):
if not kwargs:
return ("",) # No inputs connected
return ("",) # No inputs connected
random.seed(seed) #seed the random generator
return (list(kwargs.values())[select_index - 1],)
+8 -8
View File
@@ -16,34 +16,34 @@ class EphemeralPreview:
RETURN_TYPES = ()
FUNCTION = "preview_no_cache"
OUTPUT_NODE = True
CATEGORY = "EmAySee_Tools" # Added per your preference
CATEGORY = "EmAySee_Tools" # Added per your preference
def preview_no_cache(self, images):
results = []
# We use a fixed filename to encourage the browser to reuse the memory slot
# slightly changing the query param (subfolder) to force a refresh but try to keep it lean.
# We use a fixed filename to encourage the browser to reuse the memory slot
# slightly changing the query param (subfolder) to force a refresh but try to keep it lean.
for i, image in enumerate(images):
i_np = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i_np, 0, 255).astype(np.uint8))
# Save to a fixed filename in temp to avoid disk bloat
# Save to a fixed filename in temp to avoid disk bloat
filename = f"ephemeral_monitor_{i}.png"
file = os.path.join(self.output_dir, filename)
img.save(file, optimize=True, compress_level=4)
# We append a random int to the subfolder only to trigger the UI update,
# but the file on disk remains constant.
# We append a random int to the subfolder only to trigger the UI update,
# but the file on disk remains constant.
results.append({
"filename": filename,
"subfolder": "",
"type": self.type
})
# This return structure mimics a PreviewImage node
# This return structure mimics a PreviewImage node
return {"ui": {"images": results}}
# Register the node
# Register the node
NODE_CLASS_MAPPINGS = {
"EmAySee_EphemeralPreview": EphemeralPreview
}
+2 -2
View File
@@ -15,8 +15,8 @@ class EmAySee_GetModelPath:
FUNCTION = "EmAySee_get_path"
def EmAySee_get_path(self, model):
# A model object in ComfyUI typically has the file path in a specific attribute
# We assume the first path is the one you need.
# A model object in ComfyUI typically has the file path in a specific attribute
# We assume the first path is the one you need.
model_path = model.model_filenames[0]
return (model_path,)
+23 -23
View File
@@ -11,27 +11,27 @@ class EmAySee_HostPinger:
def INPUT_TYPES(s):
return {
"required": {
"host": ("STRING", {"default": "google.com"}), # Hostname or IP address to ping
"host": ("STRING", {"default": "google.com"}), # Hostname or IP address to ping
}
}
RETURN_TYPES = ("INT",) # Output is a single integer (1 or 0)
RETURN_NAMES = ("is_up",) # Name for the output
CATEGORY = "EmAySee_Network" # Category in the ComfyUI menu
TITLE = "EmAySee Host Pinger" # Title displayed on the node
RETURN_TYPES = ("INT",) # Output is a single integer (1 or 0)
RETURN_NAMES = ("is_up",) # Name for the output
CATEGORY = "EmAySee_Network" # Category in the ComfyUI menu
TITLE = "EmAySee Host Pinger" # Title displayed on the node
FUNCTION = "EmAySee_check_host" # The method that will be executed
FUNCTION = "EmAySee_check_host" # The method that will be executed
def EmAySee_check_host(self, host):
"""
Executes a system ping command to check if the host is reachable.
Returns 1 if successful, 0 otherwise.
"""
# Determine the correct ping command based on the operating system
# Determine the correct ping command based on the operating system
param = '-n' if platform.system().lower() == 'windows' else '-c'
count = '1' # Number of ping requests
count = '1' # Number of ping requests
timeout_param = '-w' if platform.system().lower() == 'windows' else '-W'
timeout_value = '1000' if platform.system().lower() == 'windows' else '1' # Timeout in ms (Windows) or seconds (Linux/macOS)
timeout_value = '1000' if platform.system().lower() == 'windows' else '1' # Timeout in ms (Windows) or seconds (Linux/macOS)
command = [
'ping',
@@ -40,28 +40,28 @@ class EmAySee_HostPinger:
host
]
is_up = 0 # Default to 0 (down)
is_up = 0 # Default to 0 (down)
try:
# Execute the ping command
# capture_output=True captures stdout and stderr
# text=True decodes stdout/stderr as text
# timeout sets a maximum time to wait for the command to complete
# Execute the ping command
# capture_output=True captures stdout and stderr
# text=True decodes stdout/stderr as text
# timeout sets a maximum time to wait for the command to complete
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=2, # Set a reasonable timeout for the subprocess itself
check=False # Do not raise an exception for non-zero exit codes (like host unreachable)
timeout=2, # Set a reasonable timeout for the subprocess itself
check=False # Do not raise an exception for non-zero exit codes (like host unreachable)
)
# Check the return code
# A return code of 0 usually indicates success (host is reachable)
# Check the return code
# A return code of 0 usually indicates success (host is reachable)
if result.returncode == 0:
is_up = 1
else:
# You can optionally print stderr for debugging if the ping fails
# print(f"Ping command failed for {host}: {result.stderr}")
# You can optionally print stderr for debugging if the ping fails
# print(f"Ping command failed for {host}: {result.stderr}")
is_up = 0
except FileNotFoundError:
@@ -74,14 +74,14 @@ class EmAySee_HostPinger:
print(f"An unexpected error occurred while pinging {host}: {e}")
is_up = 0
return (is_up,) # Return the result as a tuple
return (is_up,) # Return the result as a tuple
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_HostPinger": EmAySee_HostPinger
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_HostPinger": "EmAySee Host Pinger"
}
+9 -9
View File
@@ -8,37 +8,37 @@ class EmAySee_ImagePassthrough:
def INPUT_TYPES(s):
return {
"required": {
# Input image connection. You can rename this input visually in the UI.
# Input image connection. You can rename this input visually in the UI.
"input_image": ("IMAGE",),
}
}
# The node outputs an image
# The node outputs an image
RETURN_TYPES = ("IMAGE",)
# You can rename the output connection visually in the UI as well.
# You can rename the output connection visually in the UI as well.
RETURN_NAMES = ("output_image",)
# Category for the ComfyUI menu
# Category for the ComfyUI menu
CATEGORY = "EmAySee_Utils"
# Title displayed on the node
# Title displayed on the node
TITLE = "EmAySee Image Passthrough"
# The method that will be executed when the node runs
# The method that will be executed when the node runs
FUNCTION = "pass_through"
def pass_through(self, input_image):
"""
Simply returns the input image.
"""
# The node's logic is just to return the input image.
# The node's logic is just to return the input image.
return (input_image,)
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_ImagePassthrough": EmAySee_ImagePassthrough
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_ImagePassthrough": "EmAySee Image Passthrough"
}
+15 -15
View File
@@ -6,7 +6,7 @@ import time
class EmAySee_ImagePreviewPassthrough:
def __init__(self):
# Get the temp directory from ComfyUI
# Get the temp directory from ComfyUI
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
@@ -24,39 +24,39 @@ class EmAySee_ImagePreviewPassthrough:
CATEGORY = "EmAySee/Utils"
def preview_and_passthrough(self, image):
# 1. --- Prepare images for preview ---
# This logic is adapted from ComfyUI's PreviewImage node
# 1. --- Prepare images for preview ---
# This logic is adapted from ComfyUI's PreviewImage node
# Convert tensor to PIL images
# Convert tensor to PIL images
images = []
for i in range(image.shape[0]):
img_tensor = image[i]
img = (img_tensor * 255.0).cpu().numpy().astype(np.uint8)
images.append(Image.fromarray(img))
# Save images to temp folder and get filenames
# Save images to temp folder and get filenames
preview_images = []
for img in images:
# Generate a unique filename based on current time
# Generate a unique filename based on current time
filename = f"emaysee_preview_{time.time_ns()}.png"
filepath = os.path.join(self.output_dir, filename)
# Save the image
img.save(filepath, format='PNG', compress_level=4) # Use low compression for speed
# Save the image
img.save(filepath, format='PNG', compress_level=4) # Use low compression for speed
# Add to preview list for the UI
# Add to preview list for the UI
preview_images.append({
"filename": filename,
"subfolder": "", # We are using the temp folder directly
"subfolder": "", # We are using the temp folder directly
"type": self.type
})
# 2. --- Return results ---
# This dictionary structure tells ComfyUI what to display in the UI
# and what to pass to the output port.
# 2. --- Return results ---
# This dictionary structure tells ComfyUI what to display in the UI
# and what to pass to the output port.
return {
"ui": { "images": preview_images }, # This triggers the inline preview
"result": (image,) # This is the passthrough output
"ui": { "images": preview_images }, # This triggers the inline preview
"result": (image,) # This is the passthrough output
}
NODE_CLASS_MAPPINGS = {
+4 -4
View File
@@ -15,7 +15,7 @@ class EmAySee_IntegerStringSelectorNode:
"min": 1,
"max": 4,
"step": 1,
"display": "slider", # Or "number"
"display": "slider", # Or "number"
"label": "Select Input (1-4)"
}),
},
@@ -37,13 +37,13 @@ class EmAySee_IntegerStringSelectorNode:
elif select_index == 4:
return (string_input_4,)
else:
return ("",) # Should not happen given input constraints, but as a failsafe
return ("",) # Should not happen given input constraints, but as a failsafe
NODE_CLASS_MAPPINGS = { # Moved to after the class definition
NODE_CLASS_MAPPINGS = { # Moved to after the class definition
"EmAySee_IntegerStringSelectorNode": EmAySee_IntegerStringSelectorNode
}
NODE_DISPLAY_NAME_MAPPINGS = { # Moved to after the class definition
NODE_DISPLAY_NAME_MAPPINGS = { # Moved to after the class definition
"EmAySee_IntegerStringSelectorNode": "EmAySee Integer String Selector"
}
+9 -9
View File
@@ -19,10 +19,10 @@ class EmAySee_GreaterThanNode:
}
}
RETURN_TYPES = ("INT",) # The node will return an integer (0 or 1)
RETURN_NAMES = ("RESULT",) # The output port will be named 'RESULT'
FUNCTION = "compare_floats" # The name of the function that executes the node's logic
CATEGORY = "Logic" # The category where the node will appear in ComfyUI
RETURN_TYPES = ("INT",) # The node will return an integer (0 or 1)
RETURN_NAMES = ("RESULT",) # The output port will be named 'RESULT'
FUNCTION = "compare_floats" # The name of the function that executes the node's logic
CATEGORY = "Logic" # The category where the node will appear in ComfyUI
def compare_floats(self, A, B):
"""
@@ -30,17 +30,17 @@ class EmAySee_GreaterThanNode:
Returns 1 if A is greater than B, otherwise returns 0.
"""
if A > B:
return (1,) # Return as a tuple, as ComfyUI expects
return (1,) # Return as a tuple, as ComfyUI expects
else:
return (0,) # Return as a tuple
return (0,) # Return as a tuple
# A dictionary of all custom nodes available in this file.
# The key is the node class name, and the value is the class itself.
# A dictionary of all custom nodes available in this file.
# The key is the node class name, and the value is the class itself.
NODE_CLASS_MAPPINGS = {
"EmAySee_GreaterThanNode": EmAySee_GreaterThanNode
}
# A dictionary that provides more user-friendly names for the nodes in the UI.
# A dictionary that provides more user-friendly names for the nodes in the UI.
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_GreaterThanNode": "EmAySee Greater Than (Float)"
}
+6 -6
View File
@@ -15,15 +15,15 @@ class EmAySee_LLMOutputCleaner:
CATEGORY = "EmAySee/Utils"
def clean_text(self, text):
# 1. Remove <think>...</think> blocks (DeepSeek/Reasoning models)
# flags=re.DOTALL ensures the dot matches newlines inside the think block
# 1. Remove <think>...</think> blocks (DeepSeek/Reasoning models)
# flags=re.DOTALL ensures the dot matches newlines inside the think block
cleaned = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
# 2. Remove common "Here is the thinking process:" preambles if they exist
# (Optional, but helps with some chatty models)
# cleaned = re.sub(r'(?i)here[\'s\s]+the\s+thinking.*?:', '', cleaned)
# 2. Remove common "Here is the thinking process:" preambles if they exist
# (Optional, but helps with some chatty models)
# cleaned = re.sub(r'(?i)here[\'s\s]+the\s+thinking.*?:', '', cleaned)
# 3. Clean up extra whitespace left behind
# 3. Clean up extra whitespace left behind
cleaned = cleaned.strip()
return (cleaned,)
+30 -30
View File
@@ -2,11 +2,11 @@ import json
import os
from safetensors import safe_open
# Node class mapping and display name (how it appears in ComfyUI)
# Node class mapping and display name (how it appears in ComfyUI)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
"""
A ComfyUI custom node to extract and display training metadata
from a .safetensors LoRA file.
@@ -17,19 +17,19 @@ class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
def __init__(self):
pass
# Define the return types of the node's outputs
# This node will output a single STRING
# Define the return types of the node's outputs
# This node will output a single STRING
RETURN_TYPES = ("STRING",)
# Define the names of the node's outputs
# Define the names of the node's outputs
RETURN_NAMES = ("metadata_json_string",)
# Define the category where the node will appear in ComfyUI
# Define the category where the node will appear in ComfyUI
FUNCTION = "extract_metadata"
CATEGORY = "Mac Custom Nodes/LoRA Tools" # You can change this category if you prefer
CATEGORY = "Mac Custom Nodes/LoRA Tools" # You can change this category if you prefer
# Define the input types of the node
# This node requires a file path to a .safetensors file
# Define the input types of the node
# This node requires a file path to a .safetensors file
@classmethod
def INPUT_TYPES(s):
return {
@@ -37,10 +37,10 @@ class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
"lora_filepath": ("STRING", {"multiline": False, "default": ""}),
},
"optional": {
# Optional input for a file upload, if you prefer to drag-and-drop
# Note: ComfyUI's file input might not directly give a full path easily
# Manual string path is often more reliable for existing files.
"lora_upload": ("LORA", {"forceInput": True}), # This allows drag-and-drop, but you'd still need to get the path
# Optional input for a file upload, if you prefer to drag-and-drop
# Note: ComfyUI's file input might not directly give a full path easily
# Manual string path is often more reliable for existing files.
"lora_upload": ("LORA", {"forceInput": True}), # This allows drag-and-drop, but you'd still need to get the path
}
}
@@ -59,7 +59,7 @@ class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
if not lora_filepath:
return ("Error: LoRA filepath cannot be empty.",)
# Ensure the file exists
# Ensure the file exists
if not os.path.exists(lora_filepath):
return (f"Error: File not found at '{lora_filepath}'. Please check the path.",)
if not os.path.isfile(lora_filepath):
@@ -70,58 +70,58 @@ class EmAySee_LoRAMetadataExtractor: # Prepend EmAySee_ to the class name
metadata = f.metadata()
output_data = {}
# Attempt to get the full Kohya_ss config if nested under __metadata__ (newer Kohya)
# Attempt to get the full Kohya_ss config if nested under __metadata__ (newer Kohya)
if "__metadata__" in metadata:
try:
output_data = json.loads(metadata["__metadata__"])
# Add a flag to indicate it's the full config
# Add a flag to indicate it's the full config
output_data["_metadata_source"] = "Full Kohya_ss Config (__metadata__)"
except json.JSONDecodeError:
output_data["_error"] = "Failed to parse __metadata__ as JSON."
output_data["_raw_metadata"] = metadata["__metadata__"]
output_data["_metadata_source"] = "Raw __metadata__ (Parse Error)"
else:
# If not, extract individual ss_ prefixed keys and their values
# If not, extract individual ss_ prefixed keys and their values
extracted_params = {}
for key, value in metadata.items():
# First, attempt general type conversion (int, float, bool, None)
# First, attempt general type conversion (int, float, bool, None)
if isinstance(value, str):
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
elif value.lower() == "none": # Handle "None" as Python None
elif value.lower() == "none": # Handle "None" as Python None
value = None
else:
try:
if "." in value: # Likely a float
if "." in value: # Likely a float
value = float(value)
else: # Try integer
else: # Try integer
value = int(value)
except ValueError:
pass # Keep as string if not a simple number/boolean
pass # Keep as string if not a simple number/boolean
# Then, specifically parse known nested JSON strings
# Then, specifically parse known nested JSON strings
if key in ["ss_tag_frequency", "ss_bucket_info", "ss_datasets"]:
if isinstance(value, str): # Only try to parse if it's still a string
if isinstance(value, str): # Only try to parse if it's still a string
try:
value = json.loads(value)
except json.JSONDecodeError:
# If parsing fails, keep it as a string but note the error
# If parsing fails, keep it as a string but note the error
value = f"ERROR_PARSING_JSON: {value}"
# Store the processed value
# Store the processed value
extracted_params[key] = value
output_data = extracted_params
output_data["_metadata_source"] = "Extracted ss_ Prefixed Keys"
# Return the data as a pretty-printed JSON string
# Return the data as a pretty-printed JSON string
return (json.dumps(output_data, indent=2),)
except Exception as e:
return (f"An unexpected error occurred during metadata extraction: {e}",)
# Register the node
NODE_CLASS_MAPPINGS["EmAySee_LoRAMetadataExtractor"] = EmAySee_LoRAMetadataExtractor # Prepend EmAySee_
NODE_DISPLAY_NAME_MAPPINGS["EmAySee_LoRA Metadata Extractor"] = "EmAySee_LoRA Metadata Extractor" # Prepend EmAySee_
# Register the node
NODE_CLASS_MAPPINGS["EmAySee_LoRAMetadataExtractor"] = EmAySee_LoRAMetadataExtractor # Prepend EmAySee_
NODE_DISPLAY_NAME_MAPPINGS["EmAySee_LoRA Metadata Extractor"] = "EmAySee_LoRA Metadata Extractor" # Prepend EmAySee_
+32 -32
View File
@@ -2,10 +2,10 @@ import json
import os
from safetensors import safe_open
# Node class mapping and display name (how it appears in ComfyUI)
# These mappings are defined here for direct node loading.
# Node class mapping and display name (how it appears in ComfyUI)
# These mappings are defined here for direct node loading.
class EmAySee_LoRAMetadataExtractorTagDrop: # Prepend EmAySee_ to the class name
class EmAySee_LoRAMetadataExtractorTagDrop: # Prepend EmAySee_ to the class name
"""
A ComfyUI custom node to extract and display training metadata
from a .safetensors LoRA file.
@@ -17,31 +17,31 @@ class EmAySee_LoRAMetadataExtractorTagDrop: # Prepend EmAySee_ to the class nam
def __init__(self):
pass
# Define the return types of the node's outputs
# This node will output a single STRING
# Define the return types of the node's outputs
# This node will output a single STRING
RETURN_TYPES = ("STRING",)
# Define the names of the node's outputs
# Define the names of the node's outputs
RETURN_NAMES = ("metadata_json_string",)
# Define the category where the node will appear in ComfyUI
# Define the category where the node will appear in ComfyUI
FUNCTION = "extract_metadata"
CATEGORY = "EmAySee/LoRA Tools" # Updated category for consistency with your prefix
CATEGORY = "EmAySee/LoRA Tools" # Updated category for consistency with your prefix
# Define the input types of the node
# This node requires a file path to a .safetensors file
# Define the input types of the node
# This node requires a file path to a .safetensors file
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"lora_filepath": ("STRING", {"multiline": False, "default": ""}),
"exclude_tag_frequency": ("BOOLEAN", {"default": False}), # New toggle input
"exclude_tag_frequency": ("BOOLEAN", {"default": False}), # New toggle input
},
"optional": {
# Optional input for a file upload, if you prefer to drag-and-drop
# Note: ComfyUI's file input might not directly give a full path easily
# Manual string path is often more reliable for existing files.
"lora_upload": ("LORA", {"forceInput": True}), # This allows drag-and-drop, but you'd still need to get the path
# Optional input for a file upload, if you prefer to drag-and-drop
# Note: ComfyUI's file input might not directly give a full path easily
# Manual string path is often more reliable for existing files.
"lora_upload": ("LORA", {"forceInput": True}), # This allows drag-and-drop, but you'd still need to get the path
}
}
@@ -61,7 +61,7 @@ class EmAySee_LoRAMetadataExtractorTagDrop: # Prepend EmAySee_ to the class nam
if not lora_filepath:
return ("Error: LoRA filepath cannot be empty.",)
# Ensure the file exists
# Ensure the file exists
if not os.path.exists(lora_filepath):
return (f"Error: File not found at '{lora_filepath}'. Please check the path.",)
if not os.path.isfile(lora_filepath):
@@ -72,69 +72,69 @@ class EmAySee_LoRAMetadataExtractorTagDrop: # Prepend EmAySee_ to the class nam
metadata = f.metadata()
output_data = {}
# Attempt to get the full Kohya_ss config if nested under __metadata__ (newer Kohya)
# Attempt to get the full Kohya_ss config if nested under __metadata__ (newer Kohya)
if "__metadata__" in metadata:
try:
output_data = json.loads(metadata["__metadata__"])
# Add a flag to indicate it's the full config
# Add a flag to indicate it's the full config
output_data["_metadata_source"] = "Full Kohya_ss Config (__metadata__)"
except json.JSONDecodeError:
output_data["_error"] = "Failed to parse __metadata__ as JSON."
output_data["_raw_metadata"] = metadata["__metadata__"]
output_data["_metadata_source"] = "Raw __metadata__ (Parse Error)"
else:
# If not, extract individual ss_ prefixed keys and their values
# If not, extract individual ss_ prefixed keys and their values
extracted_params = {}
for key, value in metadata.items():
# First, attempt general type conversion (int, float, bool, None)
# First, attempt general type conversion (int, float, bool, None)
if isinstance(value, str):
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
elif value.lower() == "none": # Handle "None" as Python None
elif value.lower() == "none": # Handle "None" as Python None
value = None
else:
try:
if "." in value: # Likely a float
if "." in value: # Likely a float
value = float(value)
else: # Try integer
else: # Try integer
value = int(value)
except ValueError:
pass # Keep as string if not a simple number/boolean
pass # Keep as string if not a simple number/boolean
# Then, specifically parse known nested JSON strings
# Then, specifically parse known nested JSON strings
if key in ["ss_tag_frequency", "ss_bucket_info", "ss_datasets"]:
if isinstance(value, str): # Only try to parse if it's still a string
if isinstance(value, str): # Only try to parse if it's still a string
try:
value = json.loads(value)
except json.JSONDecodeError:
# If parsing fails, keep it as a string but note the error
# If parsing fails, keep it as a string but note the error
value = f"ERROR_PARSING_JSON: {value}"
# Store the processed value
# Store the processed value
extracted_params[key] = value
output_data = extracted_params
output_data["_metadata_source"] = "Extracted ss_ Prefixed Keys"
# Apply the toggle: remove ss_tag_frequency if exclude_tag_frequency is True
# Apply the toggle: remove ss_tag_frequency if exclude_tag_frequency is True
if exclude_tag_frequency:
if "ss_tag_frequency" in output_data:
del output_data["ss_tag_frequency"]
# If the full config was loaded, check for ss_tag_frequency within it
# If the full config was loaded, check for ss_tag_frequency within it
if "_metadata_source" in output_data and output_data["_metadata_source"] == "Full Kohya_ss Config (__metadata__)":
if "ss_tag_frequency" in output_data:
del output_data["ss_tag_frequency"]
# Return the data as a pretty-printed JSON string
# Return the data as a pretty-printed JSON string
return (json.dumps(output_data, indent=2),)
except Exception as e:
return (f"An unexpected error occurred during metadata extraction: {e}",)
# Register the node
# Register the node
NODE_CLASS_MAPPINGS = {
"EmAySee_LoRAMetadataExtractorTagDrop": EmAySee_LoRAMetadataExtractorTagDrop,
}
+44 -44
View File
@@ -1,8 +1,8 @@
import os
import folder_paths # ComfyUI utility to get model paths
import folder_paths # ComfyUI utility to get model paths
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
class EmAySee_LoraFilePicker:
"""
A ComfyUI custom node to pick LORA files from configured paths.
@@ -10,7 +10,7 @@ class EmAySee_LoraFilePicker:
the selected filename and its full file path.
"""
def __init__(self):
# Constructor for the node. No special initialization needed for this node.
# Constructor for the node. No special initialization needed for this node.
pass
@classmethod
@@ -19,38 +19,38 @@ class EmAySee_LoraFilePicker:
Defines the input types (widgets and connections) for the node.
This method is a class method and must return a dictionary.
"""
# Get all LORA files from ComfyUI's configured LORA paths.
# This will return a list of relative filenames (e.g., "my_lora.safetensors",
# "subdir/another_lora.pt"). The 'return_full_paths=True' argument is removed
# for compatibility with older ComfyUI versions.
# Get all LORA files from ComfyUI's configured LORA paths.
# This will return a list of relative filenames (e.g., "my_lora.safetensors",
# "subdir/another_lora.pt"). The 'return_full_paths=True' argument is removed
# for compatibility with older ComfyUI versions.
lora_files = folder_paths.get_filename_list("loras")
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
lora_files.insert(0, "None")
return {
"required": {
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
"lora_name": (lora_files,),
},
}
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the names for the output ports. These names will appear on the node.
# Define the names for the output ports. These names will appear on the node.
RETURN_NAMES = ("filename", "full_filepath",)
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
FUNCTION = "pick_lora_file"
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
CATEGORY = "EmAySee/File Utilities"
def pick_lora_file(self, lora_name):
@@ -59,57 +59,57 @@ class EmAySee_LoraFilePicker:
It takes the selected LORA filename as input and returns the filename
and its corresponding full file path.
"""
# If the "None" option is selected in the dropdown, return empty strings.
# If the "None" option is selected in the dropdown, return empty strings.
if lora_name == "None":
return ("", "")
full_lora_path = None
# Get all configured base directories for LORAs.
# folder_paths.get_folder_paths("loras") returns a list of absolute paths
# to the directories where LORAs are stored.
# Get all configured base directories for LORAs.
# folder_paths.get_folder_paths("loras") returns a list of absolute paths
# to the directories where LORAs are stored.
lora_folders = folder_paths.get_folder_paths("loras")
# Iterate through each LORA folder to find the selected LORA file.
# Iterate through each LORA folder to find the selected LORA file.
for folder in lora_folders:
# Construct a potential full path by joining the base folder and the selected LORA name.
# Construct a potential full path by joining the base folder and the selected LORA name.
potential_path = os.path.join(folder, lora_name)
# Verify if this constructed path actually exists and points to a file.
# Verify if this constructed path actually exists and points to a file.
if os.path.exists(potential_path) and os.path.isfile(potential_path):
full_lora_path = potential_path
break
# Also check if the lora_name itself contains subdirectories (e.g., "subdir/lora.safetensors")
# and the folder is a base path.
# This handles cases where the dropdown might show "subdir/lora.safetensors" directly.
# Also check if the lora_name itself contains subdirectories (e.g., "subdir/lora.safetensors")
# and the folder is a base path.
# This handles cases where the dropdown might show "subdir/lora.safetensors" directly.
if os.path.exists(os.path.join(folder, lora_name)) and os.path.isfile(os.path.join(folder, lora_name)):
full_lora_path = os.path.join(folder, lora_name)
break
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
if full_lora_path is None:
print(f"Warning: LORA file '{lora_name}' not found in known ComfyUI LORA paths.")
return ("", "")
# Return the selected filename and its successfully determined full path.
# Return the selected filename and its successfully determined full path.
return (lora_name, full_lora_path)
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
NODE_CLASS_MAPPINGS = {
"EmAySee_LoraFilePicker": EmAySee_LoraFilePicker,
}
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_LoraFilePicker": "EmAySee Lora File Picker",
}
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
+5 -5
View File
@@ -9,7 +9,7 @@ class EmAySee_LoraLoaderFromInput:
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
# This input will connect to the output of the EmAySee Lora Name Selector
# This input will connect to the output of the EmAySee Lora Name Selector
"lora_filename": ("LORA",),
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
@@ -22,16 +22,16 @@ class EmAySee_LoraLoaderFromInput:
def EmAySee_load_lora(self, model, clip, lora_filename, strength_model, strength_clip):
# 1. Find the full path of the Lora file using the filename from input
# 1. Find the full path of the Lora file using the filename from input
lora_path = folder_paths.get_full_path("loras", lora_filename)
# 2. Check if the file exists before proceeding
# 2. Check if the file exists before proceeding
if lora_path is None:
# We print an error but return the original model/clip to prevent the workflow from crashing
# We print an error but return the original model/clip to prevent the workflow from crashing
print(f"[EmAySee_LoraLoaderFromInput] Lora file not found: {lora_filename}. Skipping Lora application.")
return (model, clip)
# 3. Load and apply the Lora using ComfyUI's standard utility function
# 3. Load and apply the Lora using ComfyUI's standard utility function
model_lora, clip_lora = comfy.sd.load_lora(model, clip, lora_path, strength_model, strength_clip)
return (model_lora, clip_lora)
+3 -3
View File
@@ -101,7 +101,7 @@ class EmAySee_LoraStacker20:
"optional": {}
}
# Create inputs for 20 Loras with individual strength controls
# Create inputs for 20 Loras with individual strength controls
for i in range(1, 21):
inputs["optional"][f"lora_{i}"] = ("LORA_OBJECT",)
inputs["required"][f"strength_model_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
@@ -117,13 +117,13 @@ class EmAySee_LoraStacker20:
current_model = model
current_clip = clip
# Iterate through all 20 possible slots
# Iterate through all 20 possible slots
for i in range(1, 21):
lora_obj = kwargs.get(f"lora_{i}", None)
str_model = kwargs.get(f"strength_model_{i}", 1.0)
str_clip = kwargs.get(f"strength_clip_{i}", 1.0)
# Only apply if a Lora Object is connected to the slot
# Only apply if a Lora Object is connected to the slot
if lora_obj is not None:
if str_model == 0 and str_clip == 0:
continue
+5 -5
View File
@@ -7,11 +7,11 @@ class EmAySee_LoraSelector:
"""
@classmethod
def INPUT_TYPES(s):
# Define the controls as part of the INPUT_TYPES but specify that they should
# be rendered as internal widgets (input_is_widget=True)
# Define the controls as part of the INPUT_TYPES but specify that they should
# be rendered as internal widgets (input_is_widget=True)
return {
"required": {
# This input is used solely for the internal widget rendering.
# This input is used solely for the internal widget rendering.
"lora_name": ("LORA", {"default": "NONE", "input_is_widget": True, "widget_name": "lora_name_select"}),
"strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "input_is_widget": True, "widget_name": "strength_model_select"}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "input_is_widget": True, "widget_name": "strength_clip_select"}),
@@ -19,14 +19,14 @@ class EmAySee_LoraSelector:
"optional": {},
}
# The output now includes the filename and the two strength values as separate outputs
# The output now includes the filename and the two strength values as separate outputs
RETURN_TYPES = ("LORA", "FLOAT", "FLOAT",)
RETURN_NAMES = ("lora_filename", "strength_model", "strength_clip",)
CATEGORY = "EmAySee/Loaders"
FUNCTION = "EmAySee_select_lora"
def EmAySee_select_lora(self, lora_name, strength_model, strength_clip):
# The node receives values from its internal widgets and passes them out.
# The node receives values from its internal widgets and passes them out.
return (lora_name, strength_model, strength_clip,)
NODE_CLASS_MAPPINGS = {
+45 -45
View File
@@ -1,8 +1,8 @@
import os
import folder_paths # ComfyUI utility to get model paths
import folder_paths # ComfyUI utility to get model paths
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
class EmAySee_LoraFilePicker:
"""
A ComfyUI custom node to pick LORA files from configured paths.
@@ -10,7 +10,7 @@ class EmAySee_LoraFilePicker:
the selected filename and its full file path.
"""
def __init__(self):
# Constructor for the node. No special initialization needed for this node.
# Constructor for the node. No special initialization needed for this node.
pass
@classmethod
@@ -19,37 +19,37 @@ class EmAySee_LoraFilePicker:
Defines the input types (widgets and connections) for the node.
This method is a class method and must return a dictionary.
"""
# Get all LORA files from ComfyUI's configured LORA paths.
# folder_paths.get_filename_list("loras") returns a list of relative filenames
# (e.g., "my_lora.safetensors", "subdir/another_lora.pt").
# Get all LORA files from ComfyUI's configured LORA paths.
# folder_paths.get_filename_list("loras") returns a list of relative filenames
# (e.g., "my_lora.safetensors", "subdir/another_lora.pt").
lora_files = folder_paths.get_filename_list("loras")
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
lora_files.insert(0, "None")
return {
"required": {
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
"lora_name": (lora_files,),
},
}
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the names for the output ports. These names will appear on the node.
# Define the names for the output ports. These names will appear on the node.
RETURN_NAMES = ("filename", "full_filepath",)
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
FUNCTION = "pick_lora_file"
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
CATEGORY = "EmAySee/File Utilities"
def pick_lora_file(self, lora_name):
@@ -58,60 +58,60 @@ class EmAySee_LoraFilePicker:
It takes the selected LORA filename as input and returns the filename
and its corresponding full file path.
"""
# If the "None" option is selected in the dropdown, return empty strings.
# If the "None" option is selected in the dropdown, return empty strings.
if lora_name == "None":
return ("", "")
full_lora_path = None
# Attempt to find the full path of the selected LORA file.
# folder_paths.get_filename_list("loras", return_full_paths=True)
# returns a list of absolute paths for all LORA files that ComfyUI has discovered.
# Attempt to find the full path of the selected LORA file.
# folder_paths.get_filename_list("loras", return_full_paths=True)
# returns a list of absolute paths for all LORA files that ComfyUI has discovered.
for lora_path in folder_paths.get_filename_list("loras", return_full_paths=True):
# Check if the base name of the current lora_path matches the selected lora_name,
# or if the full lora_path ends with the selected lora_name (this handles
# cases where lora_name might include subdirectories, e.g., "my_folder/my_lora.safetensors").
# Check if the base name of the current lora_path matches the selected lora_name,
# or if the full lora_path ends with the selected lora_name (this handles
# cases where lora_name might include subdirectories, e.g., "my_folder/my_lora.safetensors").
if os.path.basename(lora_path) == lora_name or lora_path.endswith(lora_name):
full_lora_path = lora_path
break
# Fallback mechanism: If the LORA was not found by direct matching (e.g., due to
# inconsistencies in how names are reported vs. stored, or new files not yet indexed).
# Fallback mechanism: If the LORA was not found by direct matching (e.g., due to
# inconsistencies in how names are reported vs. stored, or new files not yet indexed).
if full_lora_path is None:
# Get all configured base directories for LORAs.
# Get all configured base directories for LORAs.
lora_folders = folder_paths.get_folder_paths("loras")
for folder in lora_folders:
# Construct a potential full path by joining the base folder and the selected LORA name.
# Construct a potential full path by joining the base folder and the selected LORA name.
potential_path = os.path.join(folder, lora_name)
# Verify if this constructed path actually exists and points to a file.
# Verify if this constructed path actually exists and points to a file.
if os.path.exists(potential_path) and os.path.isfile(potential_path):
full_lora_path = potential_path
break
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
if full_lora_path is None:
print(f"Warning: LORA file '{lora_name}' not found in known ComfyUI LORA paths.")
return ("", "")
# Return the selected filename and its successfully determined full path.
# Return the selected filename and its successfully determined full path.
return (lora_name, full_lora_path)
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
NODE_CLASS_MAPPINGS = {
"EmAySee_LoraFilePicker": EmAySee_LoraFilePicker,
}
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_LoraFilePicker": "EmAySee Lora File Picker",
}
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
+21 -21
View File
@@ -1,6 +1,6 @@
import json
# Node class mapping and display name (how it appears in ComfyUI)
# Node class mapping and display name (how it appears in ComfyUI)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -13,17 +13,17 @@ class EmAySee_MetadataFormatter:
def __init__(self):
pass
# Define the return types of the node's outputs
# Define the return types of the node's outputs
RETURN_TYPES = ("STRING",)
# Define the names of the node's outputs
# Define the names of the node's outputs
RETURN_NAMES = ("formatted_metadata_string",)
# Define the category where the node will appear in ComfyUI
# Define the category where the node will appear in ComfyUI
FUNCTION = "format_metadata"
CATEGORY = "Mac Custom Nodes/LoRA Tools" # Consistent with your other node
CATEGORY = "Mac Custom Nodes/LoRA Tools" # Consistent with your other node
# Define the input types of the node
# Define the input types of the node
@classmethod
def INPUT_TYPES(s):
return {
@@ -38,39 +38,39 @@ class EmAySee_MetadataFormatter:
a readable line-by-line string.
"""
try:
# Parse the input JSON string
# Parse the input JSON string
metadata = json.loads(metadata_json_string)
except json.JSONDecodeError as e:
return (f"Error: Invalid JSON input for metadata: {e}",)
formatted_lines = []
# Add a header for clarity
# Add a header for clarity
formatted_lines.append("--- LoRA Training Parameters ---")
# Sort keys for consistent output order
# Sort keys for consistent output order
sorted_keys = sorted(metadata.keys())
for key in sorted_keys:
value = metadata[key]
display_key = key.replace("ss_", "") # Remove ss_ prefix for display
display_key = key.replace("ss_", "") # Remove ss_ prefix for display
# Skip internal metadata source flag
# Skip internal metadata source flag
if key == "_metadata_source":
continue
# Handle specific nested JSON fields for better readability
# Handle specific nested JSON fields for better readability
if key == "ss_tag_frequency":
formatted_lines.append(f"\n{display_key.replace('_', ' ').title()}:")
# Parse the nested tag frequency JSON string
if isinstance(value, dict): # Should already be parsed by Extractor node
# Parse the nested tag frequency JSON string
if isinstance(value, dict): # Should already be parsed by Extractor node
for sub_key, sub_value in value.items():
formatted_lines.append(f" - {sub_key}: {sub_value}")
else:
formatted_lines.append(f" (Raw data, could not parse: {value})")
elif key == "ss_bucket_info":
formatted_lines.append(f"\n{display_key.replace('_', ' ').title()}:")
if isinstance(value, dict): # Should already be parsed by Extractor node
if isinstance(value, dict): # Should already be parsed by Extractor node
if "buckets" in value and isinstance(value["buckets"], dict):
formatted_lines.append(" Buckets:")
for bucket_id, bucket_data in value["buckets"].items():
@@ -83,11 +83,11 @@ class EmAySee_MetadataFormatter:
formatted_lines.append(f" (Raw data, could not parse: {value})")
elif key == "ss_datasets":
formatted_lines.append(f"\n{display_key.replace('_', ' ').title()}:")
if isinstance(value, list): # Should already be parsed by Extractor node
if isinstance(value, list): # Should already be parsed by Extractor node
for i, dataset_entry in enumerate(value):
formatted_lines.append(f" Dataset Entry {i}:")
for ds_key, ds_val in dataset_entry.items():
if isinstance(ds_val, dict): # For nested dicts like tag_frequency within datasets
if isinstance(ds_val, dict): # For nested dicts like tag_frequency within datasets
formatted_lines.append(f" - {ds_key.replace('_', ' ').title()}:")
for sub_key, sub_val in ds_val.items():
formatted_lines.append(f" - {sub_key}: {sub_val}")
@@ -96,11 +96,11 @@ class EmAySee_MetadataFormatter:
else:
formatted_lines.append(f" (Raw data, could not parse: {value})")
else:
# Format other parameters
# Attempt to make values more readable (e.g., float to scientific notation if very small)
# Format other parameters
# Attempt to make values more readable (e.g., float to scientific notation if very small)
display_value = value
if isinstance(value, float) and value < 0.001 and value != 0.0:
display_value = f"{value:.1e}" # Scientific notation for small floats
display_value = f"{value:.1e}" # Scientific notation for small floats
formatted_lines.append(f"{display_key.replace('_', ' ').title()}: {display_value}")
@@ -108,6 +108,6 @@ class EmAySee_MetadataFormatter:
return ("\n".join(formatted_lines),)
# Register the node
# Register the node
NODE_CLASS_MAPPINGS["EmAySee_MetadataFormatter"] = EmAySee_MetadataFormatter
NODE_DISPLAY_NAME_MAPPINGS["EmAySee_LoRA Metadata Formatter"] = "EmAySee_LoRA Metadata Formatter"
+7 -7
View File
@@ -10,10 +10,10 @@ class EmAySee_ProbabilityStringSelectorNode:
"string_input_1": ("STRING", {"default": "Option 1 - Less Likely"}),
"string_input_2": ("STRING", {"default": "Option 2 - More Likely"}),
"probability_option_1": ("FLOAT", {
"default": 0.1, # Default probability of 10% for option 1
"min": 0.0, # Minimum probability is 0%
"max": 1.0, # Maximum probability is 100%
"step": 0.01, # Probability step of 1%
"default": 0.1, # Default probability of 10% for option 1
"min": 0.0, # Minimum probability is 0%
"max": 1.0, # Maximum probability is 100%
"step": 0.01, # Probability step of 1%
"label": "Probability of Option 1 (0.0-1.0)"
}),
},
@@ -26,13 +26,13 @@ class EmAySee_ProbabilityStringSelectorNode:
FUNCTION = "EmAySee_function"
def EmAySee_function(self, string_input_1, string_input_2, probability_option_1):
# Generate a random float between 0.0 and 1.0
# Generate a random float between 0.0 and 1.0
random_value = random.random()
if random_value < probability_option_1:
return (string_input_1,) # Select string_input_1 if random_value is less than the probability
return (string_input_1,) # Select string_input_1 if random_value is less than the probability
else:
return (string_input_2,) # Otherwise, select string_input_2
return (string_input_2,) # Otherwise, select string_input_2
NODE_CLASS_MAPPINGS = {
"EmAySee_ProbabilityStringSelectorNode": EmAySee_ProbabilityStringSelectorNode
+9 -9
View File
@@ -4,7 +4,7 @@ import torch.nn.functional as F
class EmAySee_QwenPixelAligner:
@classmethod
def INPUT_TYPES(s):
# Generate the list of resolutions from 784 to 2240 in steps of 112
# Generate the list of resolutions from 784 to 2240 in steps of 112
resolutions = [str(x) for x in range(784, 2241, 112)]
return {
@@ -25,33 +25,33 @@ class EmAySee_QwenPixelAligner:
target_edge = int(longest_edge)
B, H, W, C = image.shape
# Calculate aspect ratio and new dimensions
# Calculate aspect ratio and new dimensions
current_longest = max(H, W)
# Scale factor to match the longest edge target
# Scale factor to match the longest edge target
scale_factor = target_edge / current_longest
target_h = H * scale_factor
target_w = W * scale_factor
# Snap dimensions to the nearest alignment multiple
# Snap dimensions to the nearest alignment multiple
new_height = int(round(target_h / align_val) * align_val)
new_width = int(round(target_w / align_val) * align_val)
# Ensure dimensions are at least one alignment block
# Ensure dimensions are at least one alignment block
new_height = max(align_val, new_height)
new_width = max(align_val, new_width)
# Permute image to [B, C, H, W] for interpolation
# Permute image to [B, C, H, W] for interpolation
img_permuted = image.permute(0, 3, 1, 2)
# Resize using bicubic interpolation
# Resize using bicubic interpolation
resized_img = F.interpolate(img_permuted, size=(new_height, new_width), mode="bicubic", align_corners=False)
# Permute back to [B, H, W, C]
# Permute back to [B, H, W, C]
final_image = resized_img.permute(0, 2, 3, 1)
# Create resolution string
# Create resolution string
res_text = f"{new_width}x{new_height}"
return (final_image, new_width, new_height, res_text)
+9 -9
View File
@@ -1,5 +1,5 @@
import random
import time # Import the time module
import time # Import the time module
class EmAySee_RandomIntFromList:
"""
@@ -21,10 +21,10 @@ class EmAySee_RandomIntFromList:
}),
"seed": ("INT", {
"default": 0,
"min": 0, # Minimum seed value
"step": 1, # Increment/decrement step
"display": "number" # Optional, for better UI
# Can add "forceInput": True to *require* a connection.
"min": 0, # Minimum seed value
"step": 1, # Increment/decrement step
"display": "number" # Optional, for better UI
# Can add "forceInput": True to *require* a connection.
}),
},
"optional": {
@@ -50,12 +50,12 @@ class EmAySee_RandomIntFromList:
if not numbers:
return (0,)
# Seed handling:
# Seed handling:
if use_system_time:
# Use system time * number of milliseconds for a more unique seed each run
seed = int(time.time() * 1000) # Use milliseconds for more variation
# Use system time * number of milliseconds for a more unique seed each run
seed = int(time.time() * 1000) # Use milliseconds for more variation
random.seed(seed) # Set the seed *before* calling random.choice
random.seed(seed) # Set the seed *before* calling random.choice
random_int = random.choice(numbers)
return (random_int,)
+9 -9
View File
@@ -1,5 +1,5 @@
import random
import time # Import the time module
import time # Import the time module
class EmAySee_RandomIntFromList:
"""
@@ -21,10 +21,10 @@ class EmAySee_RandomIntFromList:
}),
"seed": ("INT", {
"default": 0,
"min": 0, # Minimum seed value
"step": 1, # Increment/decrement step
"display": "number" # Optional, for better UI
# Can add "forceInput": True to *require* a connection.
"min": 0, # Minimum seed value
"step": 1, # Increment/decrement step
"display": "number" # Optional, for better UI
# Can add "forceInput": True to *require* a connection.
}),
},
"optional": {
@@ -50,12 +50,12 @@ class EmAySee_RandomIntFromList:
if not numbers:
return (0,)
# Seed handling:
# Seed handling:
if use_system_time:
# Use system time * number of milliseconds for a more unique seed each run
seed = int(time.time() * 1000) # Use milliseconds for more variation
# Use system time * number of milliseconds for a more unique seed each run
seed = int(time.time() * 1000) # Use milliseconds for more variation
random.seed(seed) # Set the seed *before* calling random.choice
random.seed(seed) # Set the seed *before* calling random.choice
random_int = random.choice(numbers)
return (random_int,)
+1 -1
View File
@@ -32,7 +32,7 @@ class EmAySee_RandomIntegerFromListNode:
return (selected_integer,)
else:
print("EmAySee Random Integer From List: No valid integers found in input string. Returning 0.")
return (0,) # Or you could return (None,) if you prefer None when no valid integers
return (0,) # Or you could return (None,) if you prefer None when no valid integers
NODE_CLASS_MAPPINGS = {
"EmAySee_RandomIntegerFromListNode": EmAySee_RandomIntegerFromListNode
@@ -8,7 +8,7 @@ class EmAySee_RandomIntegerFromTogglesNode_PremadeLabels:
def INPUT_TYPES(s):
return {"required": {
"node_description": ("STRING", {"multiline": True, "default": "Select options below to choose from randomly."}),
"option_1": ("TOGGLE", {"default": False, "label": "Option 1: Label for Option 1"}), # Example labels
"option_1": ("TOGGLE", {"default": False, "label": "Option 1: Label for Option 1"}), # Example labels
"option_2": ("BOOLEAN", {"default": False, "label": "Option 2: Label for Option 2"}),
"option_3": ("BOOLEAN", {"default": False, "label": "Option 3: Label for Option 3"}),
"option_4": ("BOOLEAN", {"default": False, "label": "Option 4: Label for Option 4"}),
@@ -36,10 +36,10 @@ class EmAySee_RandomIntegerFromTogglesNode_PremadeLabels:
FUNCTION = "get_random_integer_from_toggles"
def get_random_integer_from_toggles(self, node_description, **kwargs): # Only node_description and kwargs now
def get_random_integer_from_toggles(self, node_description, **kwargs): # Only node_description and kwargs now
selected_integers = []
for i in range(1, 21):
toggle_name = f"option_{i}" # Toggle input names are now option_1, option_2,...
toggle_name = f"option_{i}" # Toggle input names are now option_1, option_2,...
if kwargs.get(toggle_name, False):
selected_integers.append(i)
+5 -5
View File
@@ -18,7 +18,7 @@ class EmAySee_RandomLoraLoader:
}
}
# Dynamically create 10 LoRA slots with activation toggles
# Dynamically create 10 LoRA slots with activation toggles
for i in range(1, 11):
inputs["required"][f"lora_name_{i}"] = (lora_list,)
inputs["required"][f"lora_active_{i}"] = ("BOOLEAN", {"default": True})
@@ -32,7 +32,7 @@ class EmAySee_RandomLoraLoader:
def load_random_lora(self, model, clip, strength_model, strength_clip, seed, **kwargs):
lora_options = []
# Collect all active LoRAs from the inputs
# Collect all active LoRAs from the inputs
for i in range(1, 11):
lora_name = kwargs.get(f"lora_name_{i}")
is_active = kwargs.get(f"lora_active_{i}")
@@ -42,17 +42,17 @@ class EmAySee_RandomLoraLoader:
selected_lora_name = "None"
selected_lora_index = 0
# If no valid loras are active or strength is 0, pass through the original model/clip
# If no valid loras are active or strength is 0, pass through the original model/clip
if not lora_options or (strength_model == 0 and strength_clip == 0):
return (model, clip, selected_lora_name, selected_lora_index)
# Use the provided seed for reproducibility
# Use the provided seed for reproducibility
random.seed(seed)
selected_lora = random.choice(lora_options)
selected_lora_name = selected_lora["name"]
selected_lora_index = selected_lora["index"]
# --- LoRA Loading Logic (adapted from the default LoraLoader) ---
# --- LoRA Loading Logic (adapted from the default LoraLoader) ---
lora_path = folder_paths.get_full_path("loras", selected_lora_name)
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
+5 -5
View File
@@ -1,7 +1,7 @@
import comfy.sd
import torch
import numpy as np
import random # Import the random module
import random # Import the random module
class EmAySee_RandomStringSelectorNode:
@classmethod
@@ -14,18 +14,18 @@ class EmAySee_RandomStringSelectorNode:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("RANDOM_SELECTED_STRING",)
CATEGORY = "EmAySee_Utils" # Or a category you prefer
CATEGORY = "EmAySee_Utils" # Or a category you prefer
FUNCTION = "EmAySee_function"
def EmAySee_function(self, string_input_1, string_input_2):
# Generate a random number (0 or 1)
# Generate a random number (0 or 1)
random_choice = random.randint(0, 1)
if random_choice == 0:
return (string_input_1,) # Return the first input if random_choice is 0
return (string_input_1,) # Return the first input if random_choice is 0
else:
return (string_input_2,) # Return the second input if random_choice is 1
return (string_input_2,) # Return the second input if random_choice is 1
NODE_CLASS_MAPPINGS = {
"EmAySee_RandomStringSelectorNode": EmAySee_RandomStringSelectorNode
+5 -5
View File
@@ -1,7 +1,7 @@
import comfy.sd
import torch
import numpy as np
import random # Import the random module
import random # Import the random module
class EmAySee_RandomStringSelectorNode:
@classmethod
@@ -14,18 +14,18 @@ class EmAySee_RandomStringSelectorNode:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("RANDOM_SELECTED_STRING",)
CATEGORY = "EmAySee_Utils" # Or a category you prefer
CATEGORY = "EmAySee_Utils" # Or a category you prefer
FUNCTION = "EmAySee_function"
def EmAySee_function(self, string_input_1, string_input_2):
# Generate a random number (0 or 1)
# Generate a random number (0 or 1)
random_choice = random.randint(0, 1)
if random_choice == 0:
return (string_input_1,) # Return the first input if random_choice is 0
return (string_input_1,) # Return the first input if random_choice is 0
else:
return (string_input_2,) # Return the second input if random_choice is 1
return (string_input_2,) # Return the second input if random_choice is 1
NODE_CLASS_MAPPINGS = {
"EmAySee_RandomStringSelectorNode": EmAySee_RandomStringSelectorNode
@@ -1,7 +1,7 @@
import comfy.sd
import torch
import numpy as np
import random # Import the random module
import random # Import the random module
class EmAySee_RandomStringSelectorNodeFourChoice:
@classmethod
@@ -16,22 +16,22 @@ class EmAySee_RandomStringSelectorNodeFourChoice:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("RANDOM_SELECTED_STRING",)
CATEGORY = "EmAySee_Utils" # Or a category you prefer
CATEGORY = "EmAySee_Utils" # Or a category you prefer
FUNCTION = "EmAySee_function"
def EmAySee_function(self, string_input_1, string_input_2, string_input_3, string_input_4):
# Generate a random number (0 , 1 , 2 , 3)
# Generate a random number (0 , 1 , 2 , 3)
random_choice = random.randint(0, 3)
print(random_choice)
if random_choice == 0:
return (string_input_1,) # Return the first input if random_choice is 0
return (string_input_1,) # Return the first input if random_choice is 0
if random_choice == 1:
return (string_input_2,) # Return the first input if random_choice is 1
return (string_input_2,) # Return the first input if random_choice is 1
if random_choice == 2:
return (string_input_3,) # Return the first input if random_choice is 2
return (string_input_3,) # Return the first input if random_choice is 2
else:
return (string_input_4,) # Return the second input if random_choice is 3
return (string_input_4,) # Return the second input if random_choice is 3
NODE_CLASS_MAPPINGS = {
"EmAySee_RandomStringSelectorNodeFourChoice": EmAySee_RandomStringSelectorNodeFourChoice
@@ -1,7 +1,7 @@
import comfy.sd
import torch
import numpy as np
import random # Import the random module
import random # Import the random module
class EmAySee_RandomStringSelectorNodeThreeChoice:
@classmethod
@@ -15,20 +15,20 @@ class EmAySee_RandomStringSelectorNodeThreeChoice:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("RANDOM_SELECTED_STRING",)
CATEGORY = "EmAySee_Utils" # Or a category you prefer
CATEGORY = "EmAySee_Utils" # Or a category you prefer
FUNCTION = "EmAySee_function"
def EmAySee_function(self, string_input_1, string_input_2, string_input_3):
# Generate a random number (0 , 1 , 2 )
# Generate a random number (0 , 1 , 2 )
random_choice = random.randint(0, 2)
if random_choice == 0:
return (string_input_1,) # Return the first input if random_choice is 0
return (string_input_1,) # Return the first input if random_choice is 0
if random_choice == 1:
return (string_input_2,) # Return the first input if random_choice is 1
return (string_input_2,) # Return the first input if random_choice is 1
else:
return (string_input_3,) # Return the first input if random_choice is 2
return (string_input_3,) # Return the first input if random_choice is 2
NODE_CLASS_MAPPINGS = {
+20 -20
View File
@@ -7,50 +7,50 @@ class EmAySee_RemoveDuplicatesFromString:
def INPUT_TYPES(s):
return {
"required": {
"input_string": ("STRING", {"multiline": True, "default": "apple, banana, apple, orange, banana, grape"}), # Comma-separated list of words
"input_string": ("STRING", {"multiline": True, "default": "apple, banana, apple, orange, banana, grape"}), # Comma-separated list of words
}
}
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("unique_list_string",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Duplicates From String" # Title displayed on the node
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("unique_list_string",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Duplicates From String" # Title displayed on the node
FUNCTION = "EmAySee_process_string" # The method that will be executed
FUNCTION = "EmAySee_process_string" # The method that will be executed
def EmAySee_process_string(self, input_string):
"""
Splits the input string, removes duplicates, and rejoins.
"""
if not input_string:
return ("",) # Return an empty string if input is empty
return ("",) # Return an empty string if input is empty
# 1. Split the string by comma
# strip() removes leading/trailing whitespace from each word
# 1. Split the string by comma
# strip() removes leading/trailing whitespace from each word
words = [word.strip() for word in input_string.split(',')]
# 2. Use a set to automatically handle uniqueness
# Sets do not preserve order, but for a simple list of words,
# this is often acceptable. If order is crucial, more complex
# logic would be needed (e.g., iterating and adding to a new list
# only if not already present).
# 2. Use a set to automatically handle uniqueness
# Sets do not preserve order, but for a simple list of words,
# this is often acceptable. If order is crucial, more complex
# logic would be needed (e.g., iterating and adding to a new list
# only if not already present).
unique_words_set = set(words)
# 3. Convert the set back to a list (optional, but useful for joining)
# Sorting here makes the output consistent, otherwise order is arbitrary.
# 3. Convert the set back to a list (optional, but useful for joining)
# Sorting here makes the output consistent, otherwise order is arbitrary.
unique_words_list = sorted(list(unique_words_set))
# 4. Join the unique words back into a comma-separated string
# 4. Join the unique words back into a comma-separated string
output_string = ", ".join(unique_words_list)
return (output_string,) # Return the result as a tuple
return (output_string,) # Return the result as a tuple
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_RemoveDuplicatesFromString": EmAySee_RemoveDuplicatesFromString
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_RemoveDuplicatesFromString": "EmAySee Remove Duplicates From String"
}
+18 -18
View File
@@ -8,17 +8,17 @@ class EmAySee_RemoveDuplicatesFromStringV2:
def INPUT_TYPES(s):
return {
"required": {
"input_string": ("STRING", {"multiline": True, "default": "apple, banana, apple, orange, banana, grape"}), # Comma-separated list of words
"sort_output": ("BOOLEAN", {"default": True}), # New: Toggle to sort the unique words alphabetically
"input_string": ("STRING", {"multiline": True, "default": "apple, banana, apple, orange, banana, grape"}), # Comma-separated list of words
"sort_output": ("BOOLEAN", {"default": True}), # New: Toggle to sort the unique words alphabetically
}
}
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("unique_list_string",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Duplicates From String" # Title displayed on the node
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("unique_list_string",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Duplicates From String" # Title displayed on the node
FUNCTION = "EmAySee_process_string" # The method that will be executed
FUNCTION = "EmAySee_process_string" # The method that will be executed
def EmAySee_process_string(self, input_string, sort_output):
"""
@@ -26,31 +26,31 @@ class EmAySee_RemoveDuplicatesFromStringV2:
Can optionally sort the unique words.
"""
if not input_string:
return ("",) # Return an empty string if input is empty
return ("",) # Return an empty string if input is empty
# 1. Split the string by comma and strip whitespace from each word
# 1. Split the string by comma and strip whitespace from each word
words = [word.strip() for word in input_string.split(',')]
# 2. Remove duplicates while preserving original order (for Python 3.7+ dicts are insertion-ordered)
# This method uses dict.fromkeys() to create a dict where keys are unique words
# and then converts the keys back to a list, preserving their first-seen order.
# 2. Remove duplicates while preserving original order (for Python 3.7+ dicts are insertion-ordered)
# This method uses dict.fromkeys() to create a dict where keys are unique words
# and then converts the keys back to a list, preserving their first-seen order.
unique_words_list = list(dict.fromkeys(words))
# 3. If sorting is requested, sort the list alphabetically
# 3. If sorting is requested, sort the list alphabetically
if sort_output:
unique_words_list.sort() # Sorts in-place alphabetically
unique_words_list.sort() # Sorts in-place alphabetically
# 4. Join the unique words back into a comma-separated string
# 4. Join the unique words back into a comma-separated string
output_string = ", ".join(unique_words_list)
return (output_string,) # Return the result as a tuple
return (output_string,) # Return the result as a tuple
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_RemoveDuplicatesFromStringV2": EmAySee_RemoveDuplicatesFromStringV2
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_RemoveDuplicatesFromStringV2": "EmAySee Remove Duplicates From StringV2"
}
+18 -18
View File
@@ -8,47 +8,47 @@ class EmAySee_RemoveWordsFromText:
def INPUT_TYPES(s):
return {
"required": {
"main_text": ("STRING", {"multiline": True, "default": "A dog, a cat, and a bird."}), # The main text to be parsed
"words_to_remove": ("STRING", {"multiline": True, "default": "dog, cat, and"}), # Comma-separated list of words to remove
"main_text": ("STRING", {"multiline": True, "default": "A dog, a cat, and a bird."}), # The main text to be parsed
"words_to_remove": ("STRING", {"multiline": True, "default": "dog, cat, and"}), # Comma-separated list of words to remove
}
}
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("cleaned_text",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Words From Text" # Title displayed on the node
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("cleaned_text",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Remove Words From Text" # Title displayed on the node
FUNCTION = "EmAySee_process_text" # The method that will be executed
FUNCTION = "EmAySee_process_text" # The method that will be executed
def EmAySee_process_text(self, main_text, words_to_remove):
"""
Removes each word from the 'words_to_remove' list from the 'main_text' string.
"""
if not main_text or not words_to_remove:
return (main_text,) # Return the original text if either input is empty
return (main_text,) # Return the original text if either input is empty
# 1. Split the string of words to remove by comma, and strip whitespace.
# We escape special characters for use in a regex pattern.
# 1. Split the string of words to remove by comma, and strip whitespace.
# We escape special characters for use in a regex pattern.
words_list = [re.escape(word.strip()) for word in words_to_remove.split(',')]
# 2. Join the words with a pipe '|' to create a regex OR pattern.
# \b ensures we match whole words only, avoiding partial matches.
# For example, we don't want to remove "cat" from "caterpillar".
# 2. Join the words with a pipe '|' to create a regex OR pattern.
# \b ensures we match whole words only, avoiding partial matches.
# For example, we don't want to remove "cat" from "caterpillar".
pattern = r"\b(" + "|".join(words_list) + r")\b"
# 3. Use re.sub to find and replace all matching words with an empty string.
# We also replace multiple spaces that might be left behind.
# 3. Use re.sub to find and replace all matching words with an empty string.
# We also replace multiple spaces that might be left behind.
cleaned_text = re.sub(pattern, "", main_text, flags=re.IGNORECASE)
cleaned_text = re.sub(r'\s{2,}', ' ', cleaned_text).strip()
return (cleaned_text,) # Return the cleaned string as a tuple
return (cleaned_text,) # Return the cleaned string as a tuple
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_RemoveWordsFromText": EmAySee_RemoveWordsFromText
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_RemoveWordsFromText": "EmAySee Remove Words From Text"
}
+6 -6
View File
@@ -30,20 +30,20 @@ class EmAySee_RepaintKSampler:
FUNCTION = "EmAySee_function"
def EmAySee_function(self, model, positive, negative, latent_image, mask, sampler_name, scheduler, steps, cfg, denoise):
# Invert the mask because in ComfyUI, white usually means "keep" and black means "change" for inpainting
# Invert the mask because in ComfyUI, white usually means "keep" and black means "change" for inpainting
inverted_mask = 1.0 - mask.unsqueeze(1)
# Apply the mask to the latent image
# Apply the mask to the latent image
masked_latent = latent_image["samples"] * inverted_mask
# Create an empty latent image for the masked area (this will be filled by the sampler)
# Create an empty latent image for the masked area (this will be filled by the sampler)
initial_noise = torch.randn_like(latent_image["samples"]) * denoise
# Combine the masked original and the initial noise for the masked area
# Where the mask is white (1.0), we take the noise; where it's black (0.0), we take the original latent
# Combine the masked original and the initial noise for the masked area
# Where the mask is white (1.0), we take the noise; where it's black (0.0), we take the original latent
combined_latent = masked_latent + initial_noise * mask.unsqueeze(1)
# Perform the K-sampling with the combined latent as the starting point
# Perform the K-sampling with the combined latent as the starting point
sampler = ComfyUISampler(model)
samples = sampler.sample(noise=combined_latent,
positive=positive,
+10 -10
View File
@@ -22,7 +22,7 @@ class EmAySee_SaveImage:
"images": ("IMAGE",),
"subfolder": ("STRING", {"default": ""}),
"name_postfix": ("STRING", {"default": "image_"}),
"timestamp_format": (["none", "YYYYMMDDHHMMSS"], {"default": "YYYYMMDDHHMMSS"}), # Removed .f option
"timestamp_format": (["none", "YYYYMMDDHHMMSS"], {"default": "YYYYMMDDHHMMSS"}), # Removed .f option
"name_prefix": ("STRING", {"default": ""}),
"separator": ("STRING", {"default": "_"}),
},
@@ -48,31 +48,31 @@ class EmAySee_SaveImage:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# --- Filename Construction ---
# --- Filename Construction ---
filename_parts = []
# Add name prefix (if provided)
# Add name prefix (if provided)
if name_prefix:
filename_parts.append(name_prefix)
# Add timestamp (if selected)
# Add timestamp (if selected)
if timestamp_format != "none":
if timestamp_format == "YYYYMMDDHHMMSS":
timestamp = time.strftime("%Y%m%d%H%M%S") # Keep only YYYYMMDDHHMMSS
# No else needed, as the only other option is "none"
timestamp = time.strftime("%Y%m%d%H%M%S") # Keep only YYYYMMDDHHMMSS
# No else needed, as the only other option is "none"
filename_parts.append(timestamp)
# Add the base filename postfix
# Add the base filename postfix
filename_parts.append(name_postfix)
# Join the parts with the separator
# Join the parts with the separator
file_name = separator.join(filename_parts)
file = f"{file_name}" # comfyui counter
file = f"{file_name}" # comfyui counter
file_path = os.path.join(full_output_folder, f"{file}.png")
filepaths.append(file_path)
# --- Metadata Handling ---
# --- Metadata Handling ---
if extra_pnginfo is not None:
pnginfo = PngImagePlugin.PngInfo()
for k, v in extra_pnginfo.items():
+21 -21
View File
@@ -1,5 +1,5 @@
import os
import folder_paths # ComfyUI utility to get paths
import folder_paths # ComfyUI utility to get paths
class EmAySee_SaveTextToFile:
"""
@@ -9,48 +9,48 @@ class EmAySee_SaveTextToFile:
def INPUT_TYPES(s):
return {
"required": {
"text_to_save": ("STRING", {"multiline": True}), # The text content to save
"filename": ("STRING", {"default": "output_text"}), # The desired filename (without extension)
"subfolder": ("STRING", {"default": ""}), # Optional subfolder within the output directory
"text_to_save": ("STRING", {"multiline": True}), # The text content to save
"filename": ("STRING", {"default": "output_text"}), # The desired filename (without extension)
"subfolder": ("STRING", {"default": ""}), # Optional subfolder within the output directory
}
}
# Define RETURN_TYPES as an empty tuple since this node has no direct outputs
# This is required by ComfyUI even for nodes without outputs.
# Define RETURN_TYPES as an empty tuple since this node has no direct outputs
# This is required by ComfyUI even for nodes without outputs.
RETURN_TYPES = ()
# RETURN_NAMES = () # RETURN_NAMES is not strictly necessary if RETURN_TYPES is empty
# RETURN_NAMES = () # RETURN_NAMES is not strictly necessary if RETURN_TYPES is empty
CATEGORY = "EmAySee_Utils" # Category in the ComfyUI menu
TITLE = "EmAySee Save Text to File" # Title displayed on the node
CATEGORY = "EmAySee_Utils" # Category in the ComfyUI menu
TITLE = "EmAySee Save Text to File" # Title displayed on the node
FUNCTION = "EmAySee_save_text" # The method that will be executed
FUNCTION = "EmAySee_save_text" # The method that will be executed
def EmAySee_save_text(self, text_to_save, filename, subfolder):
"""
Saves the text content to a file in the specified subfolder.
"""
# Get the base output directory for ComfyUI
output_dir = folder_paths.get_output_paths(None)[0] # Get the first output path
# Get the base output directory for ComfyUI
output_dir = folder_paths.get_output_paths(None)[0] # Get the first output path
# Construct the full directory path
# Construct the full directory path
if subfolder:
save_dir = os.path.join(output_dir, subfolder)
else:
save_dir = output_dir
# Ensure the directory exists, create if necessary
# Ensure the directory exists, create if necessary
os.makedirs(save_dir, exist_ok=True)
# Construct the full file path with .txt extension
# Sanitize filename to remove potentially problematic characters
# Construct the full file path with .txt extension
# Sanitize filename to remove potentially problematic characters
safe_filename = "".join(c for c in filename if c.isalnum() or c in (' ', '_', '-')).rstrip()
if not safe_filename:
safe_filename = "output_text" # Fallback if filename becomes empty after sanitization
safe_filename = "output_text" # Fallback if filename becomes empty after sanitization
file_path = os.path.join(save_dir, f"{safe_filename}.txt")
try:
# Save the text content to the file
# Save the text content to the file
with open(file_path, "w", encoding="utf-8") as f:
f.write(text_to_save)
@@ -61,15 +61,15 @@ class EmAySee_SaveTextToFile:
except Exception as e:
print(f"An unexpected error occurred while saving text: {e}")
# This node doesn't return any specific output values, so return an empty tuple.
# This node doesn't return any specific output values, so return an empty tuple.
return ()
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_SaveTextToFile": EmAySee_SaveTextToFile
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_SaveTextToFile": "EmAySee Save Text to File"
}
+3 -3
View File
@@ -101,17 +101,17 @@ class EmAySee_SelectiveModelUnloader:
gc.collect()
comfy.model_management.soft_empty_cache()
# Replicating the "Free model and node cache" button backend logic:
# Replicating the "Free model and node cache" button backend logic:
if not exclude_checkpoints:
comfy.model_management.unload_all_models()
if hasattr(comfy.model_management, "loaded_models"):
comfy.model_management.loaded_models.clear()
# Clear the execution cache (Node Cache)
# Clear the execution cache (Node Cache)
if hasattr(comfy.model_management, "node_cache"):
comfy.model_management.node_cache.clear()
# Brute force memory cleanup
# Brute force memory cleanup
try:
comfy.model_management.cleanup_models(keep_free_mem=0)
except:
+4 -4
View File
@@ -16,7 +16,7 @@ class EmAySee_StringPoseSelectorNode:
"min": 1,
"max": 5,
"step": 1,
"display": "slider", # Or "number"
"display": "slider", # Or "number"
"label": "Select Input (1-5)"
}),
},
@@ -40,13 +40,13 @@ class EmAySee_StringPoseSelectorNode:
elif select_index == 5:
return (string_input_5,)
else:
return ("",) # Should not happen given input constraints, but as a failsafe
return ("",) # Should not happen given input constraints, but as a failsafe
NODE_CLASS_MAPPINGS = { # Moved to after the class definition
NODE_CLASS_MAPPINGS = { # Moved to after the class definition
"EmAySee_StringPoseSelectorNode": EmAySee_StringPoseSelectorNode
}
NODE_DISPLAY_NAME_MAPPINGS = { # Moved to after the class definition
NODE_DISPLAY_NAME_MAPPINGS = { # Moved to after the class definition
"EmAySee_StringPoseSelectorNode": "EmAySee Pose Option Selector"
}
+2 -2
View File
@@ -17,8 +17,8 @@ class EmAySee_StringTupleInputNode:
FUNCTION = "create_string_tuple"
def create_string_tuple(self, string_list):
lines = string_list.strip().split('\n') # Split multiline string into lines
string_tuple = tuple(line.strip() for line in lines) # Create a tuple of strings, stripping whitespace from each line
lines = string_list.strip().split('\n') # Split multiline string into lines
string_tuple = tuple(line.strip() for line in lines) # Create a tuple of strings, stripping whitespace from each line
return (string_tuple,)
NODE_CLASS_MAPPINGS = {
@@ -35,7 +35,7 @@ class EmAySee_SubmitToOobaboogaAPIWithKeyThinker:
"Authorization": f"Bearer {api_key}"
}
# Construct the payload for OpenAI-compatible API
# Construct the payload for OpenAI-compatible API
data = {
"messages": [
{"role": "system", "content": system_prompt},
@@ -45,7 +45,7 @@ class EmAySee_SubmitToOobaboogaAPIWithKeyThinker:
"temperature": temperature,
"top_p": top_p,
"seed": seed,
"mode": "instruct", # Helpful for some Ooba configs
"mode": "instruct", # Helpful for some Ooba configs
"stream": False
}
@@ -55,20 +55,20 @@ class EmAySee_SubmitToOobaboogaAPIWithKeyThinker:
result_json = response.json()
# Extract content from OpenAI format
# Extract content from OpenAI format
if "choices" in result_json and len(result_json["choices"]) > 0:
raw_content = result_json["choices"][0]["message"]["content"]
else:
raw_content = ""
print(f"EmAySee_SubmitToOobaboogaAPIWithKeyThinker: No content in response: {result_json}")
# --- Logic to separate <think> content from clean text ---
# --- Logic to separate <think> content from clean text ---
# 1. Extract thinking content
# 1. Extract thinking content
think_match = re.search(r'<think>(.*?)</think>', raw_content, flags=re.DOTALL)
thinking_content = think_match.group(1).strip() if think_match else ""
# 2. Remove thinking tags to get clean text
# 2. Remove thinking tags to get clean text
clean_text = re.sub(r'<think>.*?</think>', '', raw_content, flags=re.DOTALL).strip()
return (clean_text, thinking_content)
+24 -24
View File
@@ -2,7 +2,7 @@ import requests
import json
import time
class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with EmAySee_
class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with EmAySee_
def __init__(self):
pass
@@ -12,23 +12,23 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with
"required": {
"apikey_input": ("STRING", {"default": "Enter your api key here"}),
"text_input": ("STRING", {"default": "Enter your prompt for text-generation-webui here"}),
"api_endpoint": ("STRING", ["http://10.0.0.71:5000/v1/completions","http://10.0.0.76:5000/v1/completions"],{"default": "http://10.0.0.71:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"min_p": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"api_endpoint": ("STRING", ["http://10.0.0.71:5000/v1/completions","http://10.0.0.76:5000/v1/completions"],{"default": "http://10.0.0.71:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"min_p": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"max_new_tokens": (
["25", "50", "75", "100", "125", "150", "175", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800"], # Dropdown options
{"default": "200"} # Default, adjust as needed
["25", "50", "75", "100", "125", "150", "175", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800"], # Dropdown options
{"default": "200"} # Default, adjust as needed
),
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1, "label": "Delay (seconds)"}),
"show_api_response": ("BOOLEAN", {"default": False, "label": "Show API Response in Console"}),
},
"optional": {
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
}
}
@@ -37,28 +37,28 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with
OUTPUT_NODE = True
FUNCTION = "submitToOobabooga"
def submitToOobabooga(self, apikey_input, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
def submitToOobabooga(self, apikey_input, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
"""
Submits a request to the text-generation-webui (oobabooga) /v1/completions API for non-streaming text generation.
Waits for the full response before returning.
"""
payload = {
"prompt": text_input,
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"temperature": temperature,
"top_p": top_p,
"top_k": top_k,
"min_p": top_k,
"typical_p": typical_p,
"repetition_penalty": repetition_penalty,
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
}
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {apikey_input}' # <-- ADD THIS LINE
'Authorization': f'Bearer {apikey_input}' # <-- ADD THIS LINE
}
try:
time.sleep(delay_seconds)
@@ -76,12 +76,12 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with
print("Oobabooga API Response Content:")
print(api_response)
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
if isinstance(api_response, dict) and 'choices' in api_response and api_response['choices']:
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['text']
elif isinstance(api_response, str): # Fallback if API returns plain text string
elif isinstance(api_response, str): # Fallback if API returns plain text string
api_response_text = api_response
else:
error_message = f"Unexpected Oobabooga API response format for /v1/completions. JSON: {api_response}"
@@ -98,9 +98,9 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with
NODE_CLASS_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPIWithKey_test": EmAySee_SubmitToOobaboogaAPIWithKey_test # Class mapping with alias
"EmAySee_SubmitToOobaboogaAPIWithKey_test": EmAySee_SubmitToOobaboogaAPIWithKey_test # Class mapping with alias
}
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPIWithKey_test": "EmAySee Submit to Oobabooga API Node With API Key_test" # Display name with alias
"EmAySee_SubmitToOobaboogaAPIWithKey_test": "EmAySee Submit to Oobabooga API Node With API Key_test" # Display name with alias
}
+23 -23
View File
@@ -2,7 +2,7 @@ import requests
import json
import time
class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAySee_
class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAySee_
def __init__(self):
pass
@@ -12,22 +12,22 @@ class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAy
"required": {
"apikey_input": ("STRING", {"default": "Enter your api key here"}),
"text_input": ("STRING", {"default": "Enter your prompt for text-generation-webui here"}),
"api_endpoint": ("STRING", {"default": "http://10.0.0.57:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"api_endpoint": ("STRING", {"default": "http://10.0.0.57:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"max_new_tokens": (
["25", "50", "75", "100", "125", "150", "175", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800", "1024", "2048", "4096"], # Dropdown options
{"default": "300"} # Default, adjust as needed
["25", "50", "75", "100", "125", "150", "175", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800", "1024", "2048", "4096"], # Dropdown options
{"default": "300"} # Default, adjust as needed
),
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1, "label": "Delay (seconds)"}),
"show_api_response": ("BOOLEAN", {"default": False, "label": "Show API Response in Console"}),
},
"optional": {
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
}
}
@@ -36,27 +36,27 @@ class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAy
OUTPUT_NODE = True
FUNCTION = "submitToOobabooga"
def submitToOobabooga(self, apikey_input, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
def submitToOobabooga(self, apikey_input, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
"""
Submits a request to the text-generation-webui (oobabooga) /v1/completions API for non-streaming text generation.
Waits for the full response before returning.
"""
payload = {
"prompt": text_input,
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"temperature": temperature,
"top_p": top_p,
"top_k": top_k,
"typical_p": typical_p,
"repetition_penalty": repetition_penalty,
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
}
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {apikey_input}' # <-- ADD THIS LINE
'Authorization': f'Bearer {apikey_input}' # <-- ADD THIS LINE
}
try:
time.sleep(delay_seconds)
@@ -74,12 +74,12 @@ class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAy
print("Oobabooga API Response Content:")
print(api_response)
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
if isinstance(api_response, dict) and 'choices' in api_response and api_response['choices']:
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['text']
elif isinstance(api_response, str): # Fallback if API returns plain text string
elif isinstance(api_response, str): # Fallback if API returns plain text string
api_response_text = api_response
else:
error_message = f"Unexpected Oobabooga API response format for /v1/completions. JSON: {api_response}"
@@ -96,9 +96,9 @@ class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAy
NODE_CLASS_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPIWithKey": EmAySee_SubmitToOobaboogaAPIWithKey # Class mapping with alias
"EmAySee_SubmitToOobaboogaAPIWithKey": EmAySee_SubmitToOobaboogaAPIWithKey # Class mapping with alias
}
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPIWithKey": "EmAySee Submit to Oobabooga API Node With API Key" # Display name with alias
"EmAySee_SubmitToOobaboogaAPIWithKey": "EmAySee Submit to Oobabooga API Node With API Key" # Display name with alias
}
+22 -22
View File
@@ -2,7 +2,7 @@ import requests
import json
import time
class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
def __init__(self):
pass
@@ -11,22 +11,22 @@ class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
return {
"required": {
"text_input": ("STRING", {"default": "Enter your prompt for text-generation-webui here"}),
"api_endpoint": ("STRING", {"default": "http://127.0.0.1:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"api_endpoint": ("STRING", {"default": "http://127.0.0.1:5000/v1/completions"}), # Default oobabooga COMPletions API endpoint - Non-streaming
"temperature": ("FLOAT", {"default": 0.7, "min": 0.01, "max": 2.0, "step": 0.01}), # Common oobabooga default
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}), # 0 means disabled in oobabooga
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), # Common oobabooga default
"repetition_penalty": ("FLOAT", {"default": 1.1, "min": 0.0, "max": 2.0, "step": 0.01}), # Common oobabooga default
"max_new_tokens": (
["50", "100", "150", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800"], # Dropdown options
{"default": "200"} # Default, adjust as needed
["50", "100", "150", "200", "250", "300", "350", "400", "450", "500", "550", "600", "650", "700", "750", "800"], # Dropdown options
{"default": "200"} # Default, adjust as needed
),
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"seed": ("INT", {"default": -1, "label": "Seed (-1 for random)"}), # Added Seed - common in completions API
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1, "label": "Delay (seconds)"}),
"show_api_response": ("BOOLEAN", {"default": False, "label": "Show API Response in Console"}),
},
"optional": {
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
# You can add 'stopping_strings' or other completion-specific parameters here later if needed
}
}
@@ -35,22 +35,22 @@ class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
OUTPUT_NODE = True
FUNCTION = "submitToOobabooga"
def submitToOobabooga(self, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
def submitToOobabooga(self, api_endpoint, text_input, temperature, top_p, top_k, typical_p, repetition_penalty, max_new_tokens, seed, delay_seconds, show_api_response, **kwargs): # Removed history_, added seed
"""
Submits a request to the text-generation-webui (oobabooga) /v1/completions API for non-streaming text generation.
Waits for the full response before returning.
"""
payload = {
"prompt": text_input,
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"max_tokens": int(max_new_tokens), # Use 'max_tokens' for /v1/completions
"temperature": temperature,
"top_p": top_p,
"top_k": top_k,
"typical_p": typical_p,
"repetition_penalty": repetition_penalty,
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
"seed": seed, # Added seed to payload
"stream": False # **Ensuring non-streaming: explicitly set stream to False**
# Add other oobabooga /v1/completions parameters here as needed, consult API docs
}
headers = {'Content-Type': 'application/json'}
@@ -71,12 +71,12 @@ class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
print("Oobabooga API Response Content:")
print(api_response)
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
# Response parsing for /v1/completions endpoint - VERIFY AGAINST DOCS!
if isinstance(api_response, dict) and 'choices' in api_response and api_response['choices']:
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['choices'][0]['text'] # Expecting text directly under 'choices'-> 0 -> 'text' for /v1/completions
elif isinstance(api_response, dict) and 'text' in api_response: # Fallback if API just returns text directly
api_response_text = api_response['text']
elif isinstance(api_response, str): # Fallback if API returns plain text string
elif isinstance(api_response, str): # Fallback if API returns plain text string
api_response_text = api_response
else:
error_message = f"Unexpected Oobabooga API response format for /v1/completions. JSON: {api_response}"
@@ -93,9 +93,9 @@ class EmAySee_SubmitToOobaboogaAPI: # Class name now prefixed with EmAySee_
NODE_CLASS_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPI": EmAySee_SubmitToOobaboogaAPI # Class mapping with alias
"EmAySee_SubmitToOobaboogaAPI": EmAySee_SubmitToOobaboogaAPI # Class mapping with alias
}
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_SubmitToOobaboogaAPI": "EmAySee Submit to Oobabooga API Node" # Display name with alias
"EmAySee_SubmitToOobaboogaAPI": "EmAySee Submit to Oobabooga API Node" # Display name with alias
}
@@ -26,7 +26,7 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse:
"dry_base": ("FLOAT", {"default": 1.75, "min": 1.0, "max": 5.0, "step": 0.05}),
"dry_allowed_length": ("INT", {"default": 2, "min": 0, "max": 20}),
"enable_thinking": ("BOOLEAN", {"default": True}),
# New Parameter: reasoning_effort
# New Parameter: reasoning_effort
"reasoning_effort": (["low", "medium", "high"], {"default": "medium"}),
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.1}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}),
@@ -71,7 +71,7 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse:
"dry_base": dry_base,
"dry_allowed_length": dry_allowed_length,
"enable_thinking": enable_thinking,
"reasoning_effort": reasoning_effort, # Added here
"reasoning_effort": reasoning_effort, # Added here
"guidance_scale": guidance_scale,
"negative_prompt": negative_prompt,
"instruction_template": instruction_template,
@@ -95,11 +95,11 @@ class EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse:
raw_content = ""
print(f"EmAySee_SubmitToOobaboogaAPIWithKey: No content in response: {result_json}")
# 1. Extract thinking content
# 1. Extract thinking content
think_match = re.search(r'<think>(.*?)</think>', raw_content, flags=re.DOTALL)
thinking_content = think_match.group(1).strip() if think_match else ""
# 2. Remove thinking tags to get clean text
# 2. Remove thinking tags to get clean text
clean_text = re.sub(r'<think>.*?</think>', '', raw_content, flags=re.DOTALL).strip()
return (clean_text, thinking_content)
+2 -2
View File
@@ -19,9 +19,9 @@ class EmAySee_ToggleIntNode:
FUNCTION = "toggle_to_integer"
def toggle_to_integer(self, toggle_input, int_if_on, int_if_off):
if toggle_input: # If toggle_input is True (toggle is ON)
if toggle_input: # If toggle_input is True (toggle is ON)
return (int_if_on,)
else: # If toggle_input is False (toggle is OFF)
else: # If toggle_input is False (toggle is OFF)
return (int_if_off,)
NODE_CLASS_MAPPINGS = {
+22 -22
View File
@@ -8,26 +8,26 @@ class EmAySee_VarTextReplacer:
def INPUT_TYPES(s):
return {
"required": {
"main_text": ("STRING", {"default": "This is %var1%, that is %var2%, and here is %var3%. We also have %var4%, %var5%, %var6%, %var7%, %var8%, %var9%, and finally %var10%.", "multiline": True}), # Main text with placeholders
"var1": ("STRING", {"default": "Variable 1"}), # Input variable for %var1%
"var2": ("STRING", {"default": "Variable 2"}), # Input variable for %var2%
"var3": ("STRING", {"default": "Variable 3"}), # Input variable for %var3%
"var4": ("STRING", {"default": "Variable 4"}), # Input variable for %var4%
"var5": ("STRING", {"default": "Variable 5"}), # Input variable for %var5%
"var6": ("STRING", {"default": "Variable 6"}), # Input variable for %var6%
"var7": ("STRING", {"default": "Variable 7"}), # Input variable for %var7%
"var8": ("STRING", {"default": "Variable 8"}), # Input variable for %var8%
"var9": ("STRING", {"default": "Variable 9"}), # Input variable for %var9%
"var10": ("STRING", {"default": "Variable 10"}), # Input variable for %var10%
"main_text": ("STRING", {"default": "This is %var1%, that is %var2%, and here is %var3%. We also have %var4%, %var5%, %var6%, %var7%, %var8%, %var9%, and finally %var10%.", "multiline": True}), # Main text with placeholders
"var1": ("STRING", {"default": "Variable 1"}), # Input variable for %var1%
"var2": ("STRING", {"default": "Variable 2"}), # Input variable for %var2%
"var3": ("STRING", {"default": "Variable 3"}), # Input variable for %var3%
"var4": ("STRING", {"default": "Variable 4"}), # Input variable for %var4%
"var5": ("STRING", {"default": "Variable 5"}), # Input variable for %var5%
"var6": ("STRING", {"default": "Variable 6"}), # Input variable for %var6%
"var7": ("STRING", {"default": "Variable 7"}), # Input variable for %var7%
"var8": ("STRING", {"default": "Variable 8"}), # Input variable for %var8%
"var9": ("STRING", {"default": "Variable 9"}), # Input variable for %var9%
"var10": ("STRING", {"default": "Variable 10"}), # Input variable for %var10%
}
}
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("replaced_text",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Var Text Replacer" # Title displayed on the node
RETURN_TYPES = ("STRING",) # Output is a single string
RETURN_NAMES = ("replaced_text",) # Name for the output
CATEGORY = "EmAySee_Text" # Category in the ComfyUI menu
TITLE = "EmAySee Var Text Replacer" # Title displayed on the node
FUNCTION = "EmAySee_replace_placeholders" # The method that will be executed
FUNCTION = "EmAySee_replace_placeholders" # The method that will be executed
def EmAySee_replace_placeholders(self, main_text, var1, var2, var3, var4, var5, var6, var7, var8, var9, var10):
"""
@@ -35,7 +35,7 @@ class EmAySee_VarTextReplacer:
"""
output_text = main_text
# Replace the specific variables we defined in INPUT_TYPES
# Replace the specific variables we defined in INPUT_TYPES
output_text = output_text.replace("%var1%", str(var1))
output_text = output_text.replace("%var2%", str(var2))
output_text = output_text.replace("%var3%", str(var3))
@@ -47,17 +47,17 @@ class EmAySee_VarTextReplacer:
output_text = output_text.replace("%var9%", str(var9))
output_text = output_text.replace("%var10%", str(var10))
# Note: Using str() just in case a non-string input is ever connected,
# though INPUT_TYPES specifies STRING.
# Note: Using str() just in case a non-string input is ever connected,
# though INPUT_TYPES specifies STRING.
return (output_text,) # Return the result as a tuple
return (output_text,) # Return the result as a tuple
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_VarTextReplacer": EmAySee_VarTextReplacer
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_VarTextReplacer": "EmAySee Var Text Replacer"
}
+4 -4
View File
@@ -14,17 +14,17 @@ class EmAySee_StringSelectorNode:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("SELECTED_STRING",)
CATEGORY = "EmAySee_Utils" # Or a category you prefer
CATEGORY = "EmAySee_Utils" # Or a category you prefer
FUNCTION = "EmAySee_function" # <-- IMPORTANT: Back to string for FUNCTION
FUNCTION = "EmAySee_function" # <-- IMPORTANT: Back to string for FUNCTION
def EmAySee_function(self, string_input_1, string_input_2, select_input): # Ensure defined AFTER FUNCTION is set
def EmAySee_function(self, string_input_1, string_input_2, select_input): # Ensure defined AFTER FUNCTION is set
if select_input == "string_input_1":
return (string_input_1,)
elif select_input == "string_input_2":
return (string_input_2,)
else:
return ("",) # Should not happen, but default to empty string
return ("",) # Should not happen, but default to empty string
NODE_CLASS_MAPPINGS = {
"EmAySee_VeryUniqueStringSelectorNode": EmAySee_StringSelectorNode
+4 -4
View File
@@ -36,8 +36,8 @@ class EmAySee_VisualMachine_SaveImageAndText_V2:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# 1. Handle Naming
# Uses the prefix as the base name; extension replacement happens here
# 1. Handle Naming
# Uses the prefix as the base name; extension replacement happens here
file_base = f"{filename_prefix}"
image_file = f"{file_base}.png"
log_file = f"{file_base}.txt"
@@ -45,10 +45,10 @@ class EmAySee_VisualMachine_SaveImageAndText_V2:
image_path = os.path.join(full_output_folder, image_file)
log_path = os.path.join(full_output_folder, log_file)
# 2. Save the Image
# 2. Save the Image
img.save(image_path, pnginfo=None, compress_level=4)
# 3. Save the Companion Text File
# 3. Save the Companion Text File
entry = (
f"--- SPECTRE V2 METADATA ---\n"
f"FILENAME: {image_file}\n"
@@ -21,18 +21,18 @@ class EmAySee_VAECompatibleAspectRatioCalculator:
FUNCTION = "EmAySee_calculate_dimensions"
def EmAySee_calculate_dimensions(self, image, output_height):
# We'll use the first image in the batch to determine dimensions.
# We'll use the first image in the batch to determine dimensions.
batch_size, original_height, original_width, channels = image.shape
# Calculate the ideal width based on the original aspect ratio.
# Calculate the ideal width based on the original aspect ratio.
ratio = original_width / original_height
# Calculate the new width based on the desired height and aspect ratio.
# This new width must be a multiple of 64 to be VAE-compatible.
# Calculate the new width based on the desired height and aspect ratio.
# This new width must be a multiple of 64 to be VAE-compatible.
vae_stride = 64
calculated_width = output_height * ratio
# Round the width to the nearest multiple of the VAE stride.
# Round the width to the nearest multiple of the VAE stride.
output_width = int(round(calculated_width / vae_stride) * vae_stride)
return (output_width, output_height,)
+7 -7
View File
@@ -17,24 +17,24 @@ class EmAySee_WD14TagFilter:
FUNCTION = "EmAySee_filter_tags"
def EmAySee_filter_tags(self, wd14_tags_input, tags_to_remove):
# 1. Normalize the input WD14 tags string (replace newlines with commas, split)
# This assumes the WD14 output is a string of tags separated by commas, spaces, or newlines.
# 1. Normalize the input WD14 tags string (replace newlines with commas, split)
# This assumes the WD14 output is a string of tags separated by commas, spaces, or newlines.
input_tags = re.split(r'[,\s\n]+', wd14_tags_input.lower().strip())
input_tags = [tag.strip() for tag in input_tags if tag.strip()]
# 2. Normalize the tags to remove string
# Split the string in the text box by commas or newlines
# 2. Normalize the tags to remove string
# Split the string in the text box by commas or newlines
remove_list_raw = re.split(r'[,\n]+', tags_to_remove.lower().strip())
tags_to_remove_set = {tag.strip() for tag in remove_list_raw if tag.strip()}
# 3. Perform the filtering
# 3. Perform the filtering
filtered_tags = []
for tag in input_tags:
# Only keep the tag if it is NOT in the set of tags to remove
# Only keep the tag if it is NOT in the set of tags to remove
if tag not in tags_to_remove_set:
filtered_tags.append(tag)
# 4. Join the remaining tags back into a single string
# 4. Join the remaining tags back into a single string
filtered_tags_output = ", ".join(filtered_tags)
return (filtered_tags_output,)
+45 -45
View File
@@ -1,8 +1,8 @@
import os
import folder_paths # ComfyUI utility to get model paths
import folder_paths # ComfyUI utility to get model paths
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
# Define the main class for your custom node.
# The class name will be used in NODE_CLASS_MAPPINGS.
class EmAySee_LoraFilePicker:
"""
A ComfyUI custom node to pick LORA files from configured paths.
@@ -10,7 +10,7 @@ class EmAySee_LoraFilePicker:
the selected filename and its full file path.
"""
def __init__(self):
# Constructor for the node. No special initialization needed for this node.
# Constructor for the node. No special initialization needed for this node.
pass
@classmethod
@@ -19,37 +19,37 @@ class EmAySee_LoraFilePicker:
Defines the input types (widgets and connections) for the node.
This method is a class method and must return a dictionary.
"""
# Get all LORA files from ComfyUI's configured LORA paths.
# folder_paths.get_filename_list("loras") returns a list of relative filenames
# (e.g., "my_lora.safetensors", "subdir/another_lora.pt").
# Get all LORA files from ComfyUI's configured LORA paths.
# folder_paths.get_filename_list("loras") returns a list of relative filenames
# (e.g., "my_lora.safetensors", "subdir/another_lora.pt").
lora_files = folder_paths.get_filename_list("loras")
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
# Add a "None" option at the beginning of the list.
# This allows the user to explicitly select no LORA.
lora_files.insert(0, "None")
return {
"required": {
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
# "lora_name" is the name of the input port that will appear on the node.
# (lora_files,) creates a dropdown widget in the ComfyUI interface,
# populated with the list of LORA files.
"lora_name": (lora_files,),
},
}
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the types of data that this node will output.
# RETURN_TYPES must be a tuple of ComfyUI data types (e.g., "STRING", "IMAGE", "MODEL").
RETURN_TYPES = ("STRING", "STRING",) # Outputs two strings.
# Define the names for the output ports. These names will appear on the node.
# Define the names for the output ports. These names will appear on the node.
RETURN_NAMES = ("filename", "full_filepath",)
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
# Specifies the Python function within this class that ComfyUI should call
# when the node is executed in a workflow.
FUNCTION = "pick_lora_file"
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
# Defines the category under which this node will appear in the ComfyUI "Add Node" menu.
# Using "EmAySee/" as a prefix helps organize your custom nodes.
CATEGORY = "EmAySee/File Utilities"
def pick_lora_file(self, lora_name):
@@ -58,60 +58,60 @@ class EmAySee_LoraFilePicker:
It takes the selected LORA filename as input and returns the filename
and its corresponding full file path.
"""
# If the "None" option is selected in the dropdown, return empty strings.
# If the "None" option is selected in the dropdown, return empty strings.
if lora_name == "None":
return ("", "")
full_lora_path = None
# Attempt to find the full path of the selected LORA file.
# folder_paths.get_filename_list("loras", return_full_paths=True)
# returns a list of absolute paths for all LORA files that ComfyUI has discovered.
# Attempt to find the full path of the selected LORA file.
# folder_paths.get_filename_list("loras", return_full_paths=True)
# returns a list of absolute paths for all LORA files that ComfyUI has discovered.
for lora_path in folder_paths.get_filename_list("loras", return_full_paths=True):
# Check if the base name of the current lora_path matches the selected lora_name,
# or if the full lora_path ends with the selected lora_name (this handles
# cases where lora_name might include subdirectories, e.g., "my_folder/my_lora.safetensors").
# Check if the base name of the current lora_path matches the selected lora_name,
# or if the full lora_path ends with the selected lora_name (this handles
# cases where lora_name might include subdirectories, e.g., "my_folder/my_lora.safetensors").
if os.path.basename(lora_path) == lora_name or lora_path.endswith(lora_name):
full_lora_path = lora_path
break
# Fallback mechanism: If the LORA was not found by direct matching (e.g., due to
# inconsistencies in how names are reported vs. stored, or new files not yet indexed).
# Fallback mechanism: If the LORA was not found by direct matching (e.g., due to
# inconsistencies in how names are reported vs. stored, or new files not yet indexed).
if full_lora_path is None:
# Get all configured base directories for LORAs.
# Get all configured base directories for LORAs.
lora_folders = folder_paths.get_folder_paths("loras")
for folder in lora_folders:
# Construct a potential full path by joining the base folder and the selected LORA name.
# Construct a potential full path by joining the base folder and the selected LORA name.
potential_path = os.path.join(folder, lora_name)
# Verify if this constructed path actually exists and points to a file.
# Verify if this constructed path actually exists and points to a file.
if os.path.exists(potential_path) and os.path.isfile(potential_path):
full_lora_path = potential_path
break
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
# If after all attempts, the file still cannot be located, print a warning
# and return empty strings to ensure the workflow doesn't crash.
if full_lora_path is None:
print(f"Warning: LORA file '{lora_name}' not found in known ComfyUI LORA paths.")
return ("", "")
# Return the selected filename and its successfully determined full path.
# Return the selected filename and its successfully determined full path.
return (lora_name, full_lora_path)
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
# NODE_CLASS_MAPPINGS is a mandatory dictionary that ComfyUI uses to discover
# and register your custom nodes. The keys are internal identifiers, and the
# values are the Python classes that define your nodes.
NODE_CLASS_MAPPINGS = {
"EmAySee_LoraFilePicker": EmAySee_LoraFilePicker,
}
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
# NODE_DISPLAY_NAME_MAPPINGS is an optional dictionary that provides
# more user-friendly names for your nodes in the ComfyUI interface.
# This name will appear in the "Add Node" menu.
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_LoraFilePicker": "EmAySee Lora File Picker",
}
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
# WEB_DIRECTORY can be defined if your node has custom web components (JavaScript/CSS).
# For this node, it's not strictly necessary, but it's a common practice to include it
# if you plan to extend the node with a custom UI later.
# WEB_DIRECTORY = "./web" # Uncomment and create a 'web' folder if needed for JS/CSS
+13 -13
View File
@@ -1,4 +1,4 @@
import torch # Often needed for ComfyUI nodes
import torch # Often needed for ComfyUI nodes
class EmAySee_MultiplierNode:
"""
@@ -9,36 +9,36 @@ class EmAySee_MultiplierNode:
def INPUT_TYPES(s):
return {
"required": {
"multiplier_slider": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.01}), # Slider input
"integer_field_1": ("INT", {"default": 10, "min": -10000, "max": 10000}), # First integer input field
"integer_field_2": ("INT", {"default": 20, "min": -10000, "max": 10000}), # Second integer input field
"multiplier_slider": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.01}), # Slider input
"integer_field_1": ("INT", {"default": 10, "min": -10000, "max": 10000}), # First integer input field
"integer_field_2": ("INT", {"default": 20, "min": -10000, "max": 10000}), # Second integer input field
}
}
RETURN_TYPES = ("INT", "INT",) # Two integer outputs
RETURN_NAMES = ("multiplied_output_1", "multiplied_output_2",) # Names for the outputs
CATEGORY = "EmAySee_Utils" # Category in the ComfyUI menu
TITLE = "EmAySee Multiplier Node" # Title displayed on the node
RETURN_TYPES = ("INT", "INT",) # Two integer outputs
RETURN_NAMES = ("multiplied_output_1", "multiplied_output_2",) # Names for the outputs
CATEGORY = "EmAySee_Utils" # Category in the ComfyUI menu
TITLE = "EmAySee Multiplier Node" # Title displayed on the node
FUNCTION = "EmAySee_calculate_multiplication" # The method that will be executed
FUNCTION = "EmAySee_calculate_multiplication" # The method that will be executed
def EmAySee_calculate_multiplication(self, multiplier_slider, integer_field_1, integer_field_2):
"""
Calculates the multiplication of each integer field by the slider value.
"""
# Perform the multiplication and cast to integer for the output
# Perform the multiplication and cast to integer for the output
output_1 = int(integer_field_1 * multiplier_slider)
output_2 = int(integer_field_2 * multiplier_slider)
# Return the results as a tuple
# Return the results as a tuple
return (output_1, output_2,)
# Mapping of node class name to the class
# Mapping of node class name to the class
NODE_CLASS_MAPPINGS = {
"EmAySee_MultiplierNode": EmAySee_MultiplierNode
}
# Mapping of node class name to the display name in the UI
# Mapping of node class name to the display name in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"EmAySee_MultiplierNode": "EmAySee Multiplier Node"
}