YAU - 4
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
# http://editorconfig.org
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
+19
-19
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "",)
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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],)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_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 = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user