SPECTRE v4.1.0: Full collection update including VRAM management and Qwen optimization
This commit is contained in:
+50
-9
@@ -4,22 +4,63 @@ import os
|
||||
import sys
|
||||
from .pysssss import init, get_ext_dir
|
||||
|
||||
# High-Compatibility ANSI Colors
|
||||
MAGENTA_BRIGHT = "\033[95m"
|
||||
BLUE_BRIGHT = "\033[94m"
|
||||
CYAN_BRIGHT = "\033[96m"
|
||||
GREEN = "\033[1;32m"
|
||||
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
|
||||
banner = f"""
|
||||
{MAGENTA_BRIGHT} ███████╗███╗ ███╗ █████╗ ██╗ ██╗ ██████╗███████╗███████╗{RESET}
|
||||
{MAGENTA_BRIGHT} ██╔════╝████╗ ████║██╔══██╗╚██╗ ██╔╝██╔════╝██╔════╝██╔════╝{RESET}
|
||||
{BLUE_BRIGHT} █████╗ ██╔████╔██║███████║ ╚████╔╝ ╚█████╗ █████╗ █████╗ {RESET}
|
||||
{CYAN_BRIGHT} ██╔══╝ ██║╚██╔╝██║██╔══██║ ╚██╔╝ ╚═══██╗██╔══╝ ██╔══╝ {RESET}
|
||||
{CYAN_BRIGHT} ███████╗██║ ╚═╝ ██║██║ ██║ ██║ ██████╔╝███████╗███████╗{RESET}
|
||||
{CYAN_BRIGHT} ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝{RESET}"""
|
||||
|
||||
print(banner)
|
||||
print(f"{MAGENTA}----------------------------------------------------------{RESET}")
|
||||
print(f"{GREEN}Initializing SPECTRE v5.0 | EmAySee Custom Nodes Active{RESET}")
|
||||
print(f"{MAGENTA}----------------------------------------------------------{RESET}")
|
||||
print(f"{CYAN}Starting Diagnostic System Check...{RESET}")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
if init():
|
||||
py = get_ext_dir("py")
|
||||
files = glob.glob(os.path.join(py, "*.py"), recursive=False)
|
||||
|
||||
for file in files:
|
||||
name = os.path.splitext(file)[0]
|
||||
spec = importlib.util.spec_from_file_location(name, file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
|
||||
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
||||
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)
|
||||
module_name = os.path.splitext(os.path.basename(file))[0]
|
||||
if module_name == "__init__":
|
||||
continue
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
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
|
||||
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
|
||||
print(f" \033[1;31m[✗]\033[0m {module_name} : Error -> {str(e)}")
|
||||
|
||||
print(f"{MAGENTA}----------------------------------------------------------{RESET}")
|
||||
print(f"{GREEN}SPECTRE Online: {len(NODE_CLASS_MAPPINGS)} Nodes Mapped.{RESET}\n")
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
class EmAySee_AdvancedOobaboogaConnector:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"api_url": ("STRING", {"default": "http://10.0.0.71:5000/v1/chat/completions"}),
|
||||
"api_key": ("STRING", {"default": "supersecretkey"}),
|
||||
"prompt": ("STRING", {"multiline": True, "default": "Enter prompt here"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}),
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff, "control_after_generate": True}),
|
||||
"max_tokens": ("INT", {"default": 512, "min": 1, "max": 32768}),
|
||||
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
|
||||
"top_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"min_p": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_k": ("INT", {"default": 0, "min": 0, "max": 200}),
|
||||
"repetition_penalty": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.01}),
|
||||
"reasoning_effort": (["low", "medium", "high"], {"default": "medium"}),
|
||||
"auto_continue": ("BOOLEAN", {"default": False}),
|
||||
"max_continues": ("INT", {"default": 3, "min": 1, "max": 10}),
|
||||
"stop_on_error": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"best_of": ("INT", {"default": 1, "min": 1, "max": 10}),
|
||||
"echo": ("BOOLEAN", {"default": False}),
|
||||
"frequency_penalty": ("FLOAT", {"default": 0.0, "min": -2.0, "max": 2.0, "step": 0.01}),
|
||||
"presence_penalty": ("FLOAT", {"default": 0.0, "min": -2.0, "max": 2.0, "step": 0.01}),
|
||||
"n": ("INT", {"default": 1, "min": 1, "max": 10}),
|
||||
"suffix": ("STRING", {"default": ""}),
|
||||
"user": ("STRING", {"default": ""}),
|
||||
"preset": ("STRING", {"default": ""}),
|
||||
"dynatemp_low": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
|
||||
"dynatemp_high": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01}),
|
||||
"dynatemp_exponent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"smoothing_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"smoothing_curve": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}),
|
||||
"typical_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"xtc_threshold": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"xtc_probability": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"epsilon_cutoff": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"eta_cutoff": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"tfs": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_a": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_n_sigma": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"dry_multiplier": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.01}),
|
||||
"dry_allowed_length": ("INT", {"default": 2, "min": 0, "max": 20}),
|
||||
"dry_base": ("FLOAT", {"default": 1.75, "min": 1.0, "max": 5.0, "step": 0.01}),
|
||||
"encoder_repetition_penalty": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.01}),
|
||||
"no_repeat_ngram_size": ("INT", {"default": 0, "min": 0, "max": 20}),
|
||||
"repetition_penalty_range": ("INT", {"default": 1024, "min": 0, "max": 8192}),
|
||||
"penalty_alpha": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||
"guidance_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 20.0, "step": 0.1}),
|
||||
"mirostat_mode": ("INT", {"default": 0, "min": 0, "max": 2}),
|
||||
"mirostat_tau": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 20.0, "step": 0.1}),
|
||||
"mirostat_eta": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"prompt_lookup_num_tokens": ("INT", {"default": 0, "min": 0, "max": 20}),
|
||||
"max_tokens_second": ("INT", {"default": 0, "min": 0, "max": 32768}),
|
||||
"do_sample": ("BOOLEAN", {"default": True}),
|
||||
"dynamic_temperature": ("BOOLEAN", {"default": False}),
|
||||
"temperature_last": ("BOOLEAN", {"default": False}),
|
||||
"auto_max_new_tokens": ("BOOLEAN", {"default": False}),
|
||||
"ban_eos_token": ("BOOLEAN", {"default": False}),
|
||||
"add_bos_token": ("BOOLEAN", {"default": True}),
|
||||
"enable_thinking": ("BOOLEAN", {"default": True}),
|
||||
"skip_special_tokens": ("BOOLEAN", {"default": True}),
|
||||
"static_cache": ("BOOLEAN", {"default": False}),
|
||||
"truncation_length": ("INT", {"default": 0, "min": 0, "max": 131072}),
|
||||
"custom_token_bans": ("STRING", {"default": ""}),
|
||||
"negative_prompt": ("STRING", {"default": ""}),
|
||||
"dry_sequence_breakers": ("STRING", {"default": '"\\n", ":", "\\"", "*"'}),
|
||||
"grammar_string": ("STRING", {"multiline": True, "default": ""}),
|
||||
"sampler_priority": ("STRING", {"multiline": True, "default": ""}),
|
||||
"stop_sequences": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("answer", "thinking", "full_raw")
|
||||
FUNCTION = "execute_request"
|
||||
CATEGORY = "EmAySee/LLM"
|
||||
|
||||
def execute_request(self, api_url, api_key, prompt, system_prompt, seed, max_tokens,
|
||||
temperature, top_p, min_p, top_k, repetition_penalty,
|
||||
reasoning_effort, auto_continue, max_continues, stop_on_error, **kwargs):
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
schema_defaults = {
|
||||
"best_of": 1, "echo": False, "frequency_penalty": 0.0, "presence_penalty": 0.0,
|
||||
"n": 1, "suffix": "", "user": "", "preset": "", "dynatemp_low": 1.0,
|
||||
"dynatemp_high": 1.0, "dynatemp_exponent": 1.0, "smoothing_factor": 0.0,
|
||||
"smoothing_curve": 1.0, "typical_p": 1.0, "xtc_threshold": 0.1,
|
||||
"xtc_probability": 0.0, "epsilon_cutoff": 0.0, "eta_cutoff": 0.0,
|
||||
"tfs": 1.0, "top_a": 0.0, "top_n_sigma": 0.0, "dry_multiplier": 0.0,
|
||||
"dry_allowed_length": 2, "dry_base": 1.75, "encoder_repetition_penalty": 1.0,
|
||||
"no_repeat_ngram_size": 0, "repetition_penalty_range": 1024, "penalty_alpha": 0.0,
|
||||
"guidance_scale": 1.0, "mirostat_mode": 0, "mirostat_tau": 5.0, "mirostat_eta": 0.1,
|
||||
"prompt_lookup_num_tokens": 0, "max_tokens_second": 0, "do_sample": True,
|
||||
"dynamic_temperature": False, "temperature_last": False, "auto_max_new_tokens": False,
|
||||
"ban_eos_token": False, "add_bos_token": True, "enable_thinking": True,
|
||||
"skip_special_tokens": True, "static_cache": False, "truncation_length": 0,
|
||||
"custom_token_bans": "", "negative_prompt": "", "grammar_string": "",
|
||||
"dry_sequence_breakers": '"\\n", ":", "\\"", "*"', "sampler_priority": "",
|
||||
"stop_sequences": ""
|
||||
}
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
full_content = ""
|
||||
current_continue = 0
|
||||
last_json_response = {}
|
||||
|
||||
while current_continue <= max_continues:
|
||||
payload = {
|
||||
"messages": messages,
|
||||
"seed": seed,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"min_p": min_p,
|
||||
"top_k": top_k,
|
||||
"repetition_penalty": repetition_penalty,
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
for key, default_val in schema_defaults.items():
|
||||
current_val = kwargs.get(key, default_val)
|
||||
if current_val != default_val:
|
||||
if key == "sampler_priority" and current_val.strip():
|
||||
payload[key] = [s.strip() for s in current_val.split(",") if s.strip()]
|
||||
elif key == "stop_sequences" and current_val.strip():
|
||||
payload["stop"] = [s.strip() for s in current_val.split(",") if s.strip()]
|
||||
else:
|
||||
payload[key] = current_val
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=payload, timeout=600)
|
||||
if response.status_code != 200:
|
||||
if stop_on_error:
|
||||
raise Exception(f"Backend Error {response.status_code}: {response.text}")
|
||||
return (f"Error {response.status_code}", "", response.text)
|
||||
|
||||
result_json = response.json()
|
||||
last_json_response = result_json
|
||||
|
||||
if "choices" in result_json and len(result_json["choices"]) > 0:
|
||||
choice = result_json["choices"][0]
|
||||
content = choice["message"]["content"]
|
||||
finish_reason = choice.get("finish_reason", "")
|
||||
|
||||
full_content += content
|
||||
|
||||
if not auto_continue or (finish_reason != "length" and finish_reason != "maxlen"):
|
||||
break
|
||||
|
||||
messages.append({"role": "assistant", "content": content})
|
||||
current_continue += 1
|
||||
else:
|
||||
if stop_on_error:
|
||||
raise Exception("No content in response")
|
||||
return ("No content in response", "", str(result_json))
|
||||
|
||||
except Exception as e:
|
||||
if stop_on_error:
|
||||
raise e
|
||||
return (f"Connection Error: {str(e)}", "", "ERROR")
|
||||
|
||||
think_pattern = r'<(?:think|thought)>(.*?)</(?:think|thought)>'
|
||||
think_match = re.search(think_pattern, full_content, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
if think_match:
|
||||
thinking = think_match.group(1).strip()
|
||||
answer = re.sub(think_pattern, '', full_content, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
else:
|
||||
if "<think>" in full_content.lower():
|
||||
start_idx = full_content.lower().find("<think>") + 7
|
||||
thinking = full_content[start_idx:].strip()
|
||||
answer = "[REASONING TRUNCATED]"
|
||||
else:
|
||||
thinking = ""
|
||||
answer = full_content.strip()
|
||||
|
||||
return (answer, thinking, json.dumps(last_json_response, indent=2))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_AdvancedOobaboogaConnector": EmAySee_AdvancedOobaboogaConnector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_AdvancedOobaboogaConnector": "EmAySee Advanced Oobabooga Connector"
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
class EmAySee_BodyAttributes_Float:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
]
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("FLOAT", {"default": 0.0, "min": -3.0, "max": 6.0, "step": 0.1})
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("FLOAT",) * 17
|
||||
RETURN_NAMES = (
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
)
|
||||
FUNCTION = "get_values"
|
||||
CATEGORY = "EmAySee/Morph"
|
||||
|
||||
def get_values(self, **kwargs):
|
||||
labels = [
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
]
|
||||
results = [kwargs.get(label, 0.0) for label in labels]
|
||||
return tuple(results)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_BodyAttributes_Float": EmAySee_BodyAttributes_Float
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_BodyAttributes_Float": "EmAySee Body Attributes Float"
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
class EmAySee_BodyAttributes_TextOut:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
]
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("STRING", {"default": "", "multiline": True})
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",) * 17
|
||||
RETURN_NAMES = (
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
)
|
||||
FUNCTION = "get_text"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def get_text(self, **kwargs):
|
||||
labels = [
|
||||
"01 Breast Size", "02 Breast Sag", "03 Areola Size", "04 Puffies",
|
||||
"05 Belly Fat", "06 Waist Size", "07 Butt Size", "08 Thigh Size",
|
||||
"09 Thigh Gap", "10 Body Type", "11 Pregnant", "12 Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05"
|
||||
]
|
||||
results = [kwargs.get(label, "") for label in labels]
|
||||
return tuple(results)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_BodyAttributes_TextOut": EmAySee_BodyAttributes_TextOut
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_BodyAttributes_TextOut": "EmAySee Body Attributes Text Out"
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
import torch
|
||||
import gc
|
||||
import comfy.model_management
|
||||
|
||||
class EmAySee_CacheCleaner:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"anything": ("*", {}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("*",)
|
||||
RETURN_NAMES = ("output",)
|
||||
FUNCTION = "clean_cache"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def clean_cache(self, anything):
|
||||
comfy.model_management.unload_all_models()
|
||||
comfy.model_management.soft_empty_cache()
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
return (anything,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_CacheCleaner": EmAySee_CacheCleaner
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_CacheCleaner": "EmAySee Cache Cleaner"
|
||||
}
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
class EmAySee_ConditionalResize:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"trigger_threshold": ("INT", {"default": 1024, "min": 0, "max": 16384, "step": 8}),
|
||||
"target_resolution": ("INT", {"default": 1024, "min": 0, "max": 16384, "step": 8}),
|
||||
"method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "resize_if_needed"
|
||||
CATEGORY = "EmAySee/Image"
|
||||
|
||||
def resize_if_needed(self, image, trigger_threshold, target_resolution, method):
|
||||
_, h, w, _ = image.shape
|
||||
longest_side = max(h, w)
|
||||
|
||||
if longest_side >= trigger_threshold:
|
||||
return (image,)
|
||||
|
||||
scale = target_resolution / longest_side
|
||||
new_h = int(h * scale)
|
||||
new_w = int(w * scale)
|
||||
|
||||
samples = image.movedim(-1, 1)
|
||||
|
||||
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.
|
||||
mode = "bicubic"
|
||||
else:
|
||||
mode = method
|
||||
|
||||
if method == "nearest-exact":
|
||||
mode = "nearest"
|
||||
|
||||
s = F.interpolate(samples, size=(new_h, new_w), mode=mode, align_corners=False if mode not in ["nearest", "area"] else None)
|
||||
s = s.movedim(1, -1)
|
||||
|
||||
return (s,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_ConditionalResize": EmAySee_ConditionalResize
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_ConditionalResize": "EmAySee Conditional Resize"
|
||||
}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
class EmAySee_ContextManager:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"default": "prompt.txt"}),
|
||||
"history_count": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}),
|
||||
"mode": (["Read_Context", "Append_and_Prune"],),
|
||||
"force_reset": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"new_generation": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "manage_context"
|
||||
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
|
||||
abs_path = os.path.abspath(file_path)
|
||||
|
||||
if force_reset and os.path.exists(abs_path):
|
||||
os.remove(abs_path)
|
||||
|
||||
# Helper to read blocks
|
||||
def get_blocks(path):
|
||||
if not os.path.exists(path) or os.path.getsize(path) == 0:
|
||||
return []
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = f.read()
|
||||
found = re.findall(r"(PROMPT:.*?STATE:.*?)(?=PROMPT:|\Z)", data, re.DOTALL)
|
||||
return [b.strip() for b in found if b.strip()]
|
||||
|
||||
blocks = get_blocks(abs_path)
|
||||
|
||||
if mode == "Read_Context":
|
||||
# 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
|
||||
with open(abs_path, 'w', encoding='utf-8') as f:
|
||||
f.write(seed + "\n\n")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
return (seed,)
|
||||
return ("",)
|
||||
|
||||
recent_blocks = blocks[-history_count:]
|
||||
return ("\n\n".join(recent_blocks),)
|
||||
|
||||
if mode == "Append_and_Prune":
|
||||
if new_generation and new_generation.strip():
|
||||
# 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
|
||||
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
|
||||
|
||||
return (new_generation if new_generation else "",)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_ContextManager": EmAySee_ContextManager}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_ContextManager": "EmAySee Context Manager"}
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
def get_comfy_root():
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
def atomic_write(path, blocks):
|
||||
directory = os.path.dirname(path)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory, mode=0o777, exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write("\n\n".join(blocks))
|
||||
|
||||
def get_blocks(path):
|
||||
if not os.path.exists(path) or os.path.getsize(path) == 0:
|
||||
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
|
||||
found = re.findall(r"(PROMPT:.*?STATE:.*?)(?=PROMPT:|\Z)", data, re.IGNORECASE | re.DOTALL)
|
||||
return [b.strip() for b in found if b.strip()]
|
||||
|
||||
class EmAySee_ContextReader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_name": ("STRING", {"default": "user/default/emaysee_prompts/prompt_v2.txt"}),
|
||||
"history_count": ("INT", {"default": 2, "min": 1, "max": 10}),
|
||||
"force_reset_file": ("BOOLEAN", {"default": False}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {"seed_text": ("STRING", {"forceInput": True})}
|
||||
}
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("history_text", "status")
|
||||
FUNCTION = "read_context"
|
||||
CATEGORY = "EmAySee_Automation/V5"
|
||||
|
||||
def read_context(self, file_name, history_count, force_reset_file, seed, seed_text=""):
|
||||
abs_path = os.path.join(get_comfy_root(), file_name)
|
||||
if force_reset_file and os.path.exists(abs_path):
|
||||
os.remove(abs_path)
|
||||
blocks = get_blocks(abs_path)
|
||||
if not blocks:
|
||||
if seed_text:
|
||||
atomic_write(abs_path, [seed_text.strip()])
|
||||
return (seed_text.strip(), "SEEDED")
|
||||
return ("", "EMPTY")
|
||||
return ("\n\n".join(blocks[-history_count:]), f"READ {len(blocks[-history_count:])}")
|
||||
|
||||
class EmAySee_ContextWriter:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_name": ("STRING", {"default": "user/default/emaysee_prompts/prompt_v2.txt"}),
|
||||
"history_count": ("INT", {"default": 2, "min": 1, "max": 10}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {"new_generation": ("STRING", {"forceInput": True})}
|
||||
}
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "write_context"
|
||||
CATEGORY = "EmAySee_Automation/V5"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def write_context(self, file_name, history_count, seed, new_generation=""):
|
||||
if not new_generation or not new_generation.strip(): return ("",)
|
||||
|
||||
text = new_generation.strip()
|
||||
|
||||
# --- 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)
|
||||
if not re.search(r"STATE:", text, re.IGNORECASE):
|
||||
print("--- [V5 WRITER] Repairing Missing STATE ---")
|
||||
text = text + "\n\nSTATE: [Repaired Placeholder]"
|
||||
|
||||
abs_path = os.path.join(get_comfy_root(), file_name)
|
||||
blocks = get_blocks(abs_path)
|
||||
|
||||
# Deduplication check
|
||||
if not (blocks and blocks[-1][:100] == text[:100]):
|
||||
blocks.append(text)
|
||||
# Keep only the requested history depth
|
||||
atomic_write(abs_path, blocks[-history_count:])
|
||||
|
||||
return (text,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_ContextReader": EmAySee_ContextReader, "EmAySee_ContextWriter": EmAySee_ContextWriter}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_ContextReader": "EmAySee Context Reader V5", "EmAySee_ContextWriter": "EmAySee Context Writer V5"}
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
class EmAySee_ContextManager_V2:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"default": "prompt_v2.txt"}),
|
||||
"history_count": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}),
|
||||
"mode": (["Read_Context", "Append_and_Prune"],),
|
||||
"force_reset": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"new_generation": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "manage_context"
|
||||
CATEGORY = "EmAySee_Automation"
|
||||
|
||||
def manage_context(self, file_path, history_count, mode, force_reset, new_generation=""):
|
||||
# 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} ---")
|
||||
|
||||
if force_reset:
|
||||
if os.path.exists(abs_path):
|
||||
os.remove(abs_path)
|
||||
print(f"--- [EmAySee V2] RESET: DELETED {abs_path} ---")
|
||||
|
||||
def get_blocks(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"--- [EmAySee V2] DEBUG: File does NOT exist on disk. ---")
|
||||
return []
|
||||
if os.path.getsize(path) == 0:
|
||||
print(f"--- [EmAySee V2] DEBUG: File exists but is EMPTY (0 bytes). ---")
|
||||
return []
|
||||
|
||||
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.
|
||||
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()]
|
||||
|
||||
blocks = get_blocks(abs_path)
|
||||
|
||||
if mode == "Read_Context":
|
||||
if not blocks:
|
||||
if new_generation and new_generation.strip():
|
||||
seed = new_generation.strip()
|
||||
print(f"--- [EmAySee V2] SEEDING: Writing input to {abs_path} ---")
|
||||
with open(abs_path, 'w', encoding='utf-8') as f:
|
||||
f.write(seed + "\n\n")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
return (seed,)
|
||||
else:
|
||||
print(f"--- [EmAySee V2] CRITICAL: No file and no seed input! ---")
|
||||
return ("",)
|
||||
|
||||
recent = blocks[-history_count:]
|
||||
print(f"--- [EmAySee V2] SUCCESS: Passing {len(recent)} blocks to LLM. ---")
|
||||
return ("\n\n".join(recent),)
|
||||
|
||||
if mode == "Append_and_Prune":
|
||||
if new_generation and new_generation.strip():
|
||||
if not blocks or blocks[-1] != new_generation.strip():
|
||||
blocks.append(new_generation.strip())
|
||||
|
||||
pruned = blocks[-history_count:]
|
||||
with open(abs_path, 'w', encoding='utf-8') as f:
|
||||
f.write("\n\n".join(pruned))
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
return (new_generation if new_generation else "",)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_ContextManager_V2": EmAySee_ContextManager_V2}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_ContextManager_V2": "EmAySee Context Manager V2"}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
|
||||
class EmAySee_ContextWriter_V5:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"default": "/full/path/to/prompt_v2.txt"}),
|
||||
"text_to_append": ("STRING", {"forceInput": True}),
|
||||
"max_history": ("INT", {"default": 4, "min": 1, "max": 20}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "write_context"
|
||||
CATEGORY = "EmAySee_Automation/Context"
|
||||
|
||||
def write_context(self, file_path, text_to_append, max_history):
|
||||
# 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
|
||||
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
|
||||
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
|
||||
history.append(text_to_append)
|
||||
|
||||
# 5. Keep only the last N entries
|
||||
history = history[-max_history:]
|
||||
|
||||
# 6. Rewrite file
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write("---".join(history))
|
||||
|
||||
return (text_to_append,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_ContextWriter_V5": EmAySee_ContextWriter_V5}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_ContextWriter_V5": "EmAySee Context Writer V5"}
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
class EmAySee_DeepReasoningConnector:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "Enter prompt here"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}),
|
||||
"api_url": ("STRING", {"default": "http://10.0.0.71:5000/v1/chat/completions"}),
|
||||
"api_key": ("STRING", {"default": "supersecretkey"}),
|
||||
"model_name": ("STRING", {"default": "default"}),
|
||||
"max_tokens_per_call": ("INT", {"default": 4096, "min": 1, "max": 8192, "step": 64}),
|
||||
"truncation_length": ("INT", {"default": 32768, "min": 0, "max": 131072, "step": 512}),
|
||||
"temperature": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"top_p": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"min_p": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_k": ("INT", {"default": 20, "min": 0, "max": 200}),
|
||||
"repetition_penalty": ("FLOAT", {"default": 1.15, "min": 1.0, "max": 2.0, "step": 0.01}),
|
||||
"repetition_penalty_range": ("INT", {"default": 2048, "min": 0, "max": 8192, "step": 64}),
|
||||
"reasoning_effort": (["low", "medium", "high"], {"default": "low"}),
|
||||
"xtc_threshold": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"xtc_probability": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"dry_multiplier": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"dry_base": ("FLOAT", {"default": 1.75, "min": 1.0, "max": 5.0, "step": 0.05}),
|
||||
"dry_allowed_length": ("INT", {"default": 1, "min": 0, "max": 20}),
|
||||
"auto_continue": ("BOOLEAN", {"default": True}),
|
||||
"max_continues": ("INT", {"default": 3, "min": 1, "max": 10}),
|
||||
"stop_on_error": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("answer", "thinking", "full_raw")
|
||||
FUNCTION = "execute_request"
|
||||
CATEGORY = "EmAySee/LLM"
|
||||
|
||||
def execute_request(self, prompt, system_prompt, api_url, api_key, model_name,
|
||||
max_tokens_per_call, truncation_length, temperature, top_p, min_p, top_k,
|
||||
repetition_penalty, repetition_penalty_range, reasoning_effort,
|
||||
xtc_threshold, xtc_probability, dry_multiplier, dry_base,
|
||||
dry_allowed_length, auto_continue, max_continues, stop_on_error):
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
full_content = ""
|
||||
current_continue = 0
|
||||
|
||||
while current_continue <= max_continues:
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens_per_call,
|
||||
"truncation_length": truncation_length,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"min_p": min_p,
|
||||
"top_k": top_k,
|
||||
"repetition_penalty": repetition_penalty,
|
||||
"repetition_penalty_range": repetition_penalty_range,
|
||||
"xtc_threshold": xtc_threshold,
|
||||
"xtc_probability": xtc_probability,
|
||||
"dry_multiplier": dry_multiplier,
|
||||
"dry_base": dry_base,
|
||||
"dry_allowed_length": dry_allowed_length,
|
||||
"enable_thinking": True,
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"do_sample": True,
|
||||
"add_bos_token": False,
|
||||
"skip_special_tokens": True,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=payload, timeout=600)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_detail = response.text
|
||||
print(f"!!! EmAySee Deep Reasoning Error [{response.status_code}] !!!")
|
||||
print(f"Backend Response: {error_detail}")
|
||||
if stop_on_error:
|
||||
raise Exception(f"Backend Error {response.status_code}: {error_detail}")
|
||||
return (f"Error {response.status_code}", "", error_detail)
|
||||
|
||||
result_json = response.json()
|
||||
|
||||
if "choices" in result_json and len(result_json["choices"]) > 0:
|
||||
choice = result_json["choices"][0]
|
||||
content = choice["message"]["content"]
|
||||
finish_reason = choice.get("finish_reason", "")
|
||||
|
||||
full_content += content
|
||||
|
||||
usage = result_json.get("usage", {})
|
||||
print(f"[EmAySee LLM] Chunk {current_continue + 1} | Context: {usage.get('total_tokens', '??')}/{truncation_length} | Finish: {finish_reason}")
|
||||
|
||||
if not auto_continue or (finish_reason != "length" and finish_reason != "maxlen"):
|
||||
break
|
||||
|
||||
messages.append({"role": "assistant", "content": content})
|
||||
current_continue += 1
|
||||
else:
|
||||
if stop_on_error: raise Exception("No content in response")
|
||||
return ("No content", "", str(result_json))
|
||||
|
||||
except Exception as e:
|
||||
if stop_on_error: raise e
|
||||
return (f"Error: {str(e)}", "", "ERROR")
|
||||
|
||||
# Parsing results
|
||||
think_pattern = r'<(?:think|thought)>(.*?)</(?:think|thought)>'
|
||||
think_match = re.search(think_pattern, full_content, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
if think_match:
|
||||
thinking = think_match.group(1).strip()
|
||||
answer = re.sub(think_pattern, '', full_content, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
else:
|
||||
if "<think>" in full_content.lower():
|
||||
start_idx = full_content.lower().find("<think>") + 7
|
||||
thinking = full_content[start_idx:].strip()
|
||||
answer = "[REASONING DID NOT FINISH]"
|
||||
else:
|
||||
thinking = ""
|
||||
answer = full_content.strip()
|
||||
|
||||
return (answer, thinking, full_content)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_DeepReasoningConnector": EmAySee_DeepReasoningConnector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_DeepReasoningConnector": "EmAySee Deep Reasoning Connector"
|
||||
}
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
class EmAySee_DeepReasoningConnectorWithSeed:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "Enter prompt here"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}),
|
||||
"api_url": ("STRING", {"default": "http://10.0.0.71:5000/v1/chat/completions"}),
|
||||
"api_key": ("STRING", {"default": "supersecretkey"}),
|
||||
"model_name": ("STRING", {"default": "default"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
|
||||
"max_tokens_per_call": ("INT", {"default": 4096, "min": 1, "max": 8192, "step": 64}),
|
||||
"truncation_length": ("INT", {"default": 32768, "min": 0, "max": 131072, "step": 512}),
|
||||
"temperature": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"top_p": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"min_p": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_k": ("INT", {"default": 20, "min": 0, "max": 200}),
|
||||
"repetition_penalty": ("FLOAT", {"default": 1.15, "min": 1.0, "max": 2.0, "step": 0.01}),
|
||||
"repetition_penalty_range": ("INT", {"default": 2048, "min": 0, "max": 8192, "step": 64}),
|
||||
"reasoning_effort": (["low", "medium", "high"], {"default": "low"}),
|
||||
"xtc_threshold": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"xtc_probability": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"dry_multiplier": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"dry_base": ("FLOAT", {"default": 1.75, "min": 1.0, "max": 5.0, "step": 0.05}),
|
||||
"dry_allowed_length": ("INT", {"default": 1, "min": 0, "max": 20}),
|
||||
"auto_continue": ("BOOLEAN", {"default": True}),
|
||||
"max_continues": ("INT", {"default": 3, "min": 1, "max": 10}),
|
||||
"stop_on_error": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("answer", "thinking", "full_raw")
|
||||
FUNCTION = "execute_request"
|
||||
CATEGORY = "EmAySee/LLM"
|
||||
|
||||
def execute_request(self, prompt, system_prompt, api_url, api_key, model_name, seed,
|
||||
max_tokens_per_call, truncation_length, temperature, top_p, min_p, top_k,
|
||||
repetition_penalty, repetition_penalty_range, reasoning_effort,
|
||||
xtc_threshold, xtc_probability, dry_multiplier, dry_base,
|
||||
dry_allowed_length, auto_continue, max_continues, stop_on_error):
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
full_content = ""
|
||||
current_continue = 0
|
||||
|
||||
while current_continue <= max_continues:
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"messages": messages,
|
||||
"seed": seed,
|
||||
"max_tokens": max_tokens_per_call,
|
||||
"truncation_length": truncation_length,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"min_p": min_p,
|
||||
"top_k": top_k,
|
||||
"repetition_penalty": repetition_penalty,
|
||||
"repetition_penalty_range": repetition_penalty_range,
|
||||
"xtc_threshold": xtc_threshold,
|
||||
"xtc_probability": xtc_probability,
|
||||
"dry_multiplier": dry_multiplier,
|
||||
"dry_base": dry_base,
|
||||
"dry_allowed_length": dry_allowed_length,
|
||||
"enable_thinking": True,
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"do_sample": True,
|
||||
"add_bos_token": False,
|
||||
"skip_special_tokens": True,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=payload, timeout=600)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_detail = response.text
|
||||
print(f"!!! EmAySee Deep Reasoning Error [{response.status_code}] !!!")
|
||||
print(f"Backend Response: {error_detail}")
|
||||
if stop_on_error:
|
||||
raise Exception(f"Backend Error {response.status_code}: {error_detail}")
|
||||
return (f"Error {response.status_code}", "", error_detail)
|
||||
|
||||
result_json = response.json()
|
||||
|
||||
if "choices" in result_json and len(result_json["choices"]) > 0:
|
||||
choice = result_json["choices"][0]
|
||||
content = choice["message"]["content"]
|
||||
finish_reason = choice.get("finish_reason", "")
|
||||
|
||||
full_content += content
|
||||
|
||||
usage = result_json.get("usage", {})
|
||||
print(f"[EmAySee LLM] Chunk {current_continue + 1} | Context: {usage.get('total_tokens', '??')}/{truncation_length} | Finish: {finish_reason}")
|
||||
|
||||
if not auto_continue or (finish_reason != "length" and finish_reason != "maxlen"):
|
||||
break
|
||||
|
||||
messages.append({"role": "assistant", "content": content})
|
||||
current_continue += 1
|
||||
else:
|
||||
if stop_on_error: raise Exception("No content in response")
|
||||
return ("No content", "", str(result_json))
|
||||
|
||||
except Exception as e:
|
||||
if stop_on_error: raise e
|
||||
return (f"Error: {str(e)}", "", "ERROR")
|
||||
|
||||
think_pattern = r'<(?:think|thought)>(.*?)</(?:think|thought)>'
|
||||
think_match = re.search(think_pattern, full_content, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
if think_match:
|
||||
thinking = think_match.group(1).strip()
|
||||
answer = re.sub(think_pattern, '', full_content, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
else:
|
||||
if "<think>" in full_content.lower():
|
||||
start_idx = full_content.lower().find("<think>") + 7
|
||||
thinking = full_content[start_idx:].strip()
|
||||
answer = "[REASONING DID NOT FINISH]"
|
||||
else:
|
||||
thinking = ""
|
||||
answer = full_content.strip()
|
||||
|
||||
return (answer, thinking, full_content)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_DeepReasoningConnectorWithSeed": EmAySee_DeepReasoningConnectorWithSeed
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_DeepReasoningConnectorWithSeed": "EmAySee Deep Reasoning Connector"
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
class EmAySee_DimensionSwapper:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"width": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 8}),
|
||||
"height": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 8}),
|
||||
"swap_dimensions": (["No Swap", "Swap"],)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT",)
|
||||
RETURN_NAMES = ("WIDTH", "HEIGHT",)
|
||||
FUNCTION = "swap_dims"
|
||||
CATEGORY = "EmAySee/Dimensions"
|
||||
|
||||
def swap_dims(self, width, height, swap_dimensions):
|
||||
if swap_dimensions == "Swap":
|
||||
return (height, width,)
|
||||
else:
|
||||
return (width, height,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_DimensionSwapper": EmAySee_DimensionSwapper
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_DimensionSwapper": "EmAySee Dimension Swapper"
|
||||
}
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
class EmAySee_DynamicListProcessor:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"multiline": True, "default": "{a|b|c{s1|s2}|d}"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("processed_text",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def process(self, text, seed):
|
||||
rng = random.Random(seed)
|
||||
|
||||
def parse_text(input_str):
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(input_str):
|
||||
if input_str[i] == '{':
|
||||
start = i
|
||||
depth = 1
|
||||
i += 1
|
||||
while i < len(input_str) and depth > 0:
|
||||
if input_str[i] == '{':
|
||||
depth += 1
|
||||
elif input_str[i] == '}':
|
||||
depth -= 1
|
||||
i += 1
|
||||
|
||||
if depth == 0:
|
||||
group_content = input_str[start + 1:i - 1]
|
||||
options = []
|
||||
current_option = []
|
||||
inner_depth = 0
|
||||
|
||||
for char in group_content:
|
||||
if char == '|' and inner_depth == 0:
|
||||
options.append("".join(current_option))
|
||||
current_option = []
|
||||
else:
|
||||
if char == '{':
|
||||
inner_depth += 1
|
||||
elif char == '}':
|
||||
inner_depth -= 1
|
||||
current_option.append(char)
|
||||
options.append("".join(current_option))
|
||||
|
||||
choice = rng.choice(options)
|
||||
result.append(parse_text(choice))
|
||||
else:
|
||||
result.append(input_str[start])
|
||||
i = start + 1
|
||||
else:
|
||||
result.append(input_str[i])
|
||||
i += 1
|
||||
return "".join(result)
|
||||
|
||||
return (parse_text(text),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_DynamicListProcessor": EmAySee_DynamicListProcessor
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_DynamicListProcessor": "EmAySee Dynamic List Processor"
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
import math
|
||||
|
||||
class EmAySee_DynamicRangeSlider:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"min_val": ("FLOAT", {"default": -1.0, "step": 0.1}),
|
||||
"max_val": ("FLOAT", {"default": 2.0, "step": 0.1}),
|
||||
"step_val": ("FLOAT", {"default": 0.25, "min": 0.001, "step": 0.001}),
|
||||
"value_percent": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
RETURN_NAMES = ("value",)
|
||||
FUNCTION = "get_value"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def get_value(self, min_val, max_val, step_val, value_percent):
|
||||
if min_val > max_val:
|
||||
min_val, max_val = max_val, min_val
|
||||
|
||||
raw_value = min_val + (value_percent * (max_val - min_val))
|
||||
|
||||
quantized_value = raw_value
|
||||
if step_val > 0:
|
||||
quantized_value = round(raw_value / step_val) * step_val
|
||||
|
||||
final_value = max(min_val, min(max_val, quantized_value))
|
||||
|
||||
return (final_value,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_DynamicRangeSlider": EmAySee_DynamicRangeSlider
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_DynamicRangeSlider": "EmAySee Dynamic Range Slider"
|
||||
}
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
import folder_paths
|
||||
import os
|
||||
import random
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
class EphemeralPreview:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"images": ("IMAGE",)}}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "preview_no_cache"
|
||||
OUTPUT_NODE = True
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
results.append({
|
||||
"filename": filename,
|
||||
"subfolder": "",
|
||||
"type": self.type
|
||||
})
|
||||
|
||||
# This return structure mimics a PreviewImage node
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
# Register the node
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_EphemeralPreview": EphemeralPreview
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_EphemeralPreview": "Ephemeral Preview (Low RAM)"
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
class EmAySee_FloatToText:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("FLOAT", {"default": 0.0, "min": -3.0, "max": 3.0, "step": 0.05}),
|
||||
"prefix": ("STRING", {"default": "", "multiline": False}),
|
||||
"suffix": ("STRING", {"default": "", "multiline": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "FLOAT")
|
||||
RETURN_NAMES = ("text", "float")
|
||||
FUNCTION = "convert"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def convert(self, value, prefix, suffix):
|
||||
return (f"({prefix}:{value}{suffix})", value)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_FloatToText": EmAySee_FloatToText
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_FloatToText": "EmAySee Float To Text"
|
||||
}
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import torch
|
||||
import gc
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from llama_cpp import Llama
|
||||
from llama_cpp.llama_chat_format import Llava15ChatHandler
|
||||
except ImportError:
|
||||
Llama = None
|
||||
Llava15ChatHandler = None
|
||||
|
||||
class EmAySee_GGUFVisionLocal:
|
||||
_MODEL = None
|
||||
_CHAT_HANDLER = None
|
||||
_CURRENT_MODEL_PATH = None
|
||||
_CURRENT_MMPROJ_PATH = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"gguf_model_path": ("STRING", {"default": "/ai/models-2/models/LLM/qwen3-vl-4b.gguf"}),
|
||||
"mmproj_path": ("STRING", {"default": "/ai/models-2/models/LLM/mmproj-qwen3-vl-4b.gguf"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "Analyze this image and generate a detailed image generation prompt. Output only the prompt."}),
|
||||
"user_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"max_tokens": ("INT", {"default": 2048, "min": 16, "max": 8192}),
|
||||
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"n_gpu_layers": ("INT", {"default": -1, "min": -1, "max": 128}),
|
||||
"unload_after": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "thinking")
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "EmAySee"
|
||||
|
||||
def _tensor_to_base64(self, image_tensor):
|
||||
img0 = image_tensor[0].detach().cpu().float().clamp(0, 1)
|
||||
arr = (img0.numpy() * 255.0).round().astype(np.uint8)
|
||||
pil_img = Image.fromarray(arr, mode="RGB")
|
||||
buffered = BytesIO()
|
||||
pil_img.save(buffered, format="JPEG")
|
||||
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
return f"data:image/jpeg;base64,{img_str}"
|
||||
|
||||
def load_model(self, model_path, mmproj_path, n_gpu_layers):
|
||||
if EmAySee_GGUFVisionLocal._MODEL is not None and \
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MODEL_PATH == model_path and \
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MMPROJ_PATH == mmproj_path:
|
||||
return EmAySee_GGUFVisionLocal._MODEL
|
||||
|
||||
if Llama is None:
|
||||
raise RuntimeError("llama_cpp is not installed. Please compile llama-cpp-python with CUDA support.")
|
||||
|
||||
chat_handler = Llava15ChatHandler(clip_model_path=mmproj_path)
|
||||
|
||||
llm = Llama(
|
||||
model_path=model_path,
|
||||
chat_handler=chat_handler,
|
||||
n_ctx=4096,
|
||||
n_gpu_layers=n_gpu_layers,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
EmAySee_GGUFVisionLocal._CHAT_HANDLER = chat_handler
|
||||
EmAySee_GGUFVisionLocal._MODEL = llm
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MODEL_PATH = model_path
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MMPROJ_PATH = mmproj_path
|
||||
|
||||
return EmAySee_GGUFVisionLocal._MODEL
|
||||
|
||||
def unload_model(self):
|
||||
if EmAySee_GGUFVisionLocal._MODEL is not None:
|
||||
del EmAySee_GGUFVisionLocal._MODEL
|
||||
del EmAySee_GGUFVisionLocal._CHAT_HANDLER
|
||||
EmAySee_GGUFVisionLocal._MODEL = None
|
||||
EmAySee_GGUFVisionLocal._CHAT_HANDLER = None
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MODEL_PATH = None
|
||||
EmAySee_GGUFVisionLocal._CURRENT_MMPROJ_PATH = None
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def generate(self, image, gguf_model_path, mmproj_path, system_prompt, user_prompt, max_tokens, temperature, n_gpu_layers, unload_after):
|
||||
b64_image = self._tensor_to_base64(image)
|
||||
llm = self.load_model(gguf_model_path, mmproj_path, n_gpu_layers)
|
||||
|
||||
if system_prompt.strip() == "":
|
||||
text_instruction = "Convert the image into a rich image-generation prompt."
|
||||
else:
|
||||
text_instruction = system_prompt.strip()
|
||||
|
||||
if user_prompt and user_prompt.strip():
|
||||
text_instruction = f"{text_instruction}\nAdditional requirements:\n{user_prompt.strip()}"
|
||||
|
||||
response = llm.create_chat_completion(
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt.strip()},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "image_url", "image_url": {"url": b64_image}},
|
||||
{"type": "text", "text": text_instruction}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
output_text = response["choices"][0]["message"]["content"]
|
||||
|
||||
thinking = ""
|
||||
prompt = output_text
|
||||
|
||||
think_end_tag = "</think>"
|
||||
if think_end_tag in output_text:
|
||||
parts = output_text.split(think_end_tag, 1)
|
||||
thinking = parts[0].replace("<think>", "").strip()
|
||||
prompt = parts[1].strip()
|
||||
|
||||
if unload_after:
|
||||
self.unload_model()
|
||||
|
||||
return (prompt, thinking)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_GGUFVisionLocal": EmAySee_GGUFVisionLocal
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_GGUFVisionLocal": "EmAySee_GGUF Vision Local Generator"
|
||||
}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import io
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
class EmAySee_GeminiImageGen:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"api_key": ("STRING", {"multiline": False}),
|
||||
"prompt": ("STRING", {"multiline": True}),
|
||||
"model": (["gemini-2.0-flash-exp", "gemini-2.5-flash-image", "imagen-3.0-generate-001", "imagen-3.0-fast-generate-001"],),
|
||||
"aspect_ratio": (["1:1", "16:9", "9:16", "4:3", "3:4"],),
|
||||
"number_of_images": ("INT", {"default": 1, "min": 1, "max": 4}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("images",)
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "EmAySee"
|
||||
|
||||
def generate_image(self, api_key, prompt, model, aspect_ratio, number_of_images):
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
try:
|
||||
if number_of_images > 4:
|
||||
number_of_images = 4
|
||||
|
||||
response = client.models.generate_images(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
config=types.GenerateImagesConfig(
|
||||
number_of_images=number_of_images,
|
||||
aspect_ratio=aspect_ratio,
|
||||
safety_filter_level="BLOCK_ONLY_HIGH",
|
||||
person_generation="ALLOW_ADULT",
|
||||
)
|
||||
)
|
||||
|
||||
output_images = []
|
||||
if response.generated_images:
|
||||
for img_entry in response.generated_images:
|
||||
image_bytes = img_entry.image.image_bytes
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
|
||||
if image.mode != 'RGB':
|
||||
image = image.convert('RGB')
|
||||
|
||||
image_np = np.array(image).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_np)
|
||||
output_images.append(image_tensor)
|
||||
|
||||
if not output_images:
|
||||
print("No images returned by Gemini.")
|
||||
return (torch.zeros((1, 512, 512, 3), dtype=torch.float32),)
|
||||
|
||||
if len(output_images) > 1:
|
||||
final_tensor = torch.stack(output_images)
|
||||
else:
|
||||
final_tensor = output_images[0].unsqueeze(0)
|
||||
|
||||
return (final_tensor,)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Gemini API Error: {e}")
|
||||
empty = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
return (empty,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_GeminiImageGen": EmAySee_GeminiImageGen
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_GeminiImageGen": "EmAySee Gemini Image Gen"
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
|
||||
class EmAySee_GlobalStringReader:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"path": ("STRING", {"default": "O:/custom_nodes/storage"}),
|
||||
"file_name": ("STRING", {"default": "iteration_state"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "read_string"
|
||||
CATEGORY = "EmAySee/Logic"
|
||||
|
||||
def read_string(self, path, file_name):
|
||||
full_path = os.path.join(path, f"{file_name}.txt")
|
||||
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return ("",)
|
||||
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
return (content,)
|
||||
|
||||
class EmAySee_GlobalStringUpdater:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_text": ("STRING", {"forceInput": True}),
|
||||
"path": ("STRING", {"default": "O:/custom_nodes/storage"}),
|
||||
"file_name": ("STRING", {"default": "iteration_state"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("passthrough",)
|
||||
FUNCTION = "update_string"
|
||||
CATEGORY = "EmAySee/Logic"
|
||||
|
||||
def update_string(self, input_text, path, file_name):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
state_path = os.path.join(path, f"{file_name}.txt")
|
||||
story_path = os.path.join(path, f"{file_name}-story.txt")
|
||||
|
||||
with open(state_path, "w", encoding="utf-8") as f:
|
||||
f.write(input_text)
|
||||
|
||||
with open(story_path, "a", encoding="utf-8") as f:
|
||||
f.write(input_text + "\n\n")
|
||||
|
||||
return (input_text,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_GlobalStringReader": EmAySee_GlobalStringReader,
|
||||
"EmAySee_GlobalStringUpdater": EmAySee_GlobalStringUpdater
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_GlobalStringReader": "EmAySee Global String Reader",
|
||||
"EmAySee_GlobalStringUpdater": "EmAySee Global String Updater"
|
||||
}
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
|
||||
class EmAySee_ImageLogger:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"log_directory": ("STRING", {"default": "user/default/emaysee_logs"}),
|
||||
"log_filename": ("STRING", {"default": "production_log.txt"}),
|
||||
"history_text": ("STRING", {"forceInput": True}),
|
||||
"current_prompt": ("STRING", {"forceInput": True}),
|
||||
"generated_name": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("log_entry",)
|
||||
FUNCTION = "log_data"
|
||||
CATEGORY = "EmAySee_Automation/Logging"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def log_data(self, log_directory, log_filename, history_text, current_prompt, generated_name):
|
||||
from nodes import get_comfy_root
|
||||
|
||||
abs_dir = os.path.join(get_comfy_root(), log_directory)
|
||||
if not os.path.exists(abs_dir):
|
||||
os.makedirs(abs_dir, mode=0o777, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(abs_dir, log_filename)
|
||||
|
||||
entry = f"--- NEW ENTRY ---\nFILE: {generated_name}\nPROMPT: {current_prompt}\nSTATE HISTORY:\n{history_text}\n\n"
|
||||
|
||||
with open(file_path, "a", encoding="utf-8") as f:
|
||||
f.write(entry)
|
||||
|
||||
return (entry,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_ImageLogger": EmAySee_ImageLogger}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_ImageLogger": "EmAySee Image Logger V1"}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
import folder_paths
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import os
|
||||
import time
|
||||
|
||||
class EmAySee_ImagePreviewPassthrough:
|
||||
def __init__(self):
|
||||
# Get the temp directory from ComfyUI
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "preview_and_passthrough"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def preview_and_passthrough(self, image):
|
||||
# 1. --- Prepare images for preview ---
|
||||
# This logic is adapted from ComfyUI's PreviewImage node
|
||||
|
||||
# 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
|
||||
preview_images = []
|
||||
for img in images:
|
||||
# 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
|
||||
|
||||
# Add to preview list for the UI
|
||||
preview_images.append({
|
||||
"filename": filename,
|
||||
"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.
|
||||
return {
|
||||
"ui": { "images": preview_images }, # This triggers the inline preview
|
||||
"result": (image,) # This is the passthrough output
|
||||
}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_ImagePreviewPassthrough": EmAySee_ImagePreviewPassthrough
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_ImagePreviewPassthrough": "EmAySee Image Preview Passthrough"
|
||||
}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
import re
|
||||
|
||||
class EmAySee_LLMOutputCleaner:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("clean_text",)
|
||||
FUNCTION = "clean_text"
|
||||
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
|
||||
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)
|
||||
|
||||
# 3. Clean up extra whitespace left behind
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
return (cleaned,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LLMOutputCleaner": EmAySee_LLMOutputCleaner
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LLMOutputCleaner": "EmAySee LLM Output Cleaner"
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps
|
||||
import folder_paths
|
||||
|
||||
class EmAySee_LoadImagePlusName:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
||||
return {"required": {"image": (sorted(files), {"image_upload": True})}}
|
||||
|
||||
CATEGORY = "EmAySee"
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
|
||||
RETURN_NAMES = ("image", "mask", "filename_stem")
|
||||
FUNCTION = "load_image"
|
||||
|
||||
def load_image(self, image):
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
i = Image.open(image_path)
|
||||
i = ImageOps.exif_transpose(i)
|
||||
image_out = i.convert("RGB")
|
||||
image_out = np.array(image_out).astype(np.float32) / 255.0
|
||||
image_out = torch.from_numpy(image_out)[None,]
|
||||
|
||||
if 'A' in i.getbands():
|
||||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1.0 - mask
|
||||
mask = torch.from_numpy(mask)
|
||||
else:
|
||||
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
|
||||
|
||||
filename_stem = os.path.splitext(os.path.basename(image))[0]
|
||||
|
||||
return (image_out, mask.unsqueeze(0), filename_stem)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LoadImagePlusName": EmAySee_LoadImagePlusName
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LoadImagePlusName": "EmAySee Load Image + Name"
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
import folder_paths
|
||||
import comfy.sd
|
||||
import torch
|
||||
|
||||
class EmAySee_LoraLoaderFromInput:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
# 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}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP",)
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
FUNCTION = "EmAySee_load_lora"
|
||||
|
||||
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
|
||||
lora_path = folder_paths.get_full_path("loras", lora_filename)
|
||||
|
||||
# 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
|
||||
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
|
||||
model_lora, clip_lora = comfy.sd.load_lora(model, clip, lora_path, strength_model, strength_clip)
|
||||
|
||||
return (model_lora, clip_lora)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LoraLoaderFromInput": EmAySee_LoraLoaderFromInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LoraLoaderFromInput": "EmAySee Lora Loader From Input"
|
||||
}
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
import folder_paths
|
||||
import comfy.sd
|
||||
import comfy.utils
|
||||
|
||||
class EmAySee_LoraNameSelector:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"lora_name": (folder_paths.get_filename_list("loras"), ),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LORA_OBJECT",)
|
||||
RETURN_NAMES = ("lora_object",)
|
||||
FUNCTION = "load_lora_data"
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
|
||||
def load_lora_data(self, lora_name):
|
||||
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||
lora = None
|
||||
if lora_path is None:
|
||||
print(f"Lora not found: {lora_name}")
|
||||
return (None,)
|
||||
else:
|
||||
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
||||
return (lora,)
|
||||
|
||||
class EmAySee_LoraProcessor:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
"lora_selector": ("INT", {"default": 1, "min": 1, "max": 9}),
|
||||
"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}),
|
||||
},
|
||||
"optional": {}
|
||||
}
|
||||
|
||||
for i in range(1, 10):
|
||||
inputs["optional"][f"lora_object_{i}"] = ("LORA_OBJECT",)
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP")
|
||||
FUNCTION = "apply_selected_lora"
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
|
||||
def apply_selected_lora(self, model, clip, lora_selector, strength_model, strength_clip, **kwargs):
|
||||
selected_key = f"lora_object_{lora_selector}"
|
||||
lora_object = kwargs.get(selected_key, None)
|
||||
|
||||
if strength_model == 0 and strength_clip == 0:
|
||||
return (model, clip)
|
||||
|
||||
if lora_object is None:
|
||||
return (model, clip)
|
||||
|
||||
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora_object, strength_model, strength_clip)
|
||||
return (model_lora, clip_lora)
|
||||
|
||||
class EmAySee_NineChoiceSelector:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"selector": ("INT", {"default": 1, "min": 1, "max": 9}),
|
||||
"label_1": ("STRING", {"multiline": False, "default": "Option 1"}),
|
||||
"label_2": ("STRING", {"multiline": False, "default": "Option 2"}),
|
||||
"label_3": ("STRING", {"multiline": False, "default": "Option 3"}),
|
||||
"label_4": ("STRING", {"multiline": False, "default": "Option 4"}),
|
||||
"label_5": ("STRING", {"multiline": False, "default": "Option 5"}),
|
||||
"label_6": ("STRING", {"multiline": False, "default": "Option 6"}),
|
||||
"label_7": ("STRING", {"multiline": False, "default": "Option 7"}),
|
||||
"label_8": ("STRING", {"multiline": False, "default": "Option 8"}),
|
||||
"label_9": ("STRING", {"multiline": False, "default": "Option 9"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "STRING")
|
||||
RETURN_NAMES = ("selected_int", "selected_label")
|
||||
FUNCTION = "get_choice"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def get_choice(self, selector, label_1, label_2, label_3, label_4, label_5, label_6, label_7, label_8, label_9):
|
||||
labels = [label_1, label_2, label_3, label_4, label_5, label_6, label_7, label_8, label_9]
|
||||
idx = max(1, min(9, selector)) - 1
|
||||
return (selector, labels[idx])
|
||||
|
||||
class EmAySee_LoraStacker20:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
},
|
||||
"optional": {}
|
||||
}
|
||||
|
||||
# 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})
|
||||
inputs["required"][f"strength_clip_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP")
|
||||
FUNCTION = "apply_stack"
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
|
||||
def apply_stack(self, model, clip, **kwargs):
|
||||
current_model = model
|
||||
current_clip = clip
|
||||
|
||||
# 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
|
||||
if lora_obj is not None:
|
||||
if str_model == 0 and str_clip == 0:
|
||||
continue
|
||||
current_model, current_clip = comfy.sd.load_lora_for_models(current_model, current_clip, lora_obj, str_model, str_clip)
|
||||
|
||||
return (current_model, current_clip)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LoraNameSelector": EmAySee_LoraNameSelector,
|
||||
"EmAySee_LoraProcessor": EmAySee_LoraProcessor,
|
||||
"EmAySee_NineChoiceSelector": EmAySee_NineChoiceSelector,
|
||||
"EmAySee_LoraStacker20": EmAySee_LoraStacker20
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LoraNameSelector": "EmAySee Lora Selector (Object)",
|
||||
"EmAySee_LoraProcessor": "EmAySee Lora Processor (9-Input Switch)",
|
||||
"EmAySee_NineChoiceSelector": "EmAySee Nine Choice Selector",
|
||||
"EmAySee_LoraStacker20": "EmAySee Lora Stacker (20 Sequential)"
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
import torch
|
||||
|
||||
class EmAySee_LoraSelector:
|
||||
"""
|
||||
A universal Lora control node that provides file selection and strength controls
|
||||
as internal widgets (no inputs) and outputs the values for downstream loaders.
|
||||
"""
|
||||
@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)
|
||||
return {
|
||||
"required": {
|
||||
# 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"}),
|
||||
},
|
||||
"optional": {},
|
||||
}
|
||||
|
||||
# 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.
|
||||
return (lora_name, strength_model, strength_clip,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LoraSelector": EmAySee_LoraSelector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LoraSelector": "EmAySee Lora Name Selector"
|
||||
}
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
class EmAySee_MaskUnion:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {},
|
||||
"optional": {}
|
||||
}
|
||||
for i in range(1, 15):
|
||||
inputs["optional"][f"mask_{i}"] = ("MASK",)
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
RETURN_NAMES = ("mask",)
|
||||
FUNCTION = "combine"
|
||||
CATEGORY = "EmAySee/Mask"
|
||||
|
||||
def combine(self, **kwargs):
|
||||
masks = [v for k, v in kwargs.items() if v is not None]
|
||||
|
||||
if not masks:
|
||||
return (torch.zeros((1, 64, 64), dtype=torch.float32),)
|
||||
|
||||
target_h = max(m.shape[-2] for m in masks)
|
||||
target_w = max(m.shape[-1] for m in masks)
|
||||
|
||||
res = torch.zeros((1, target_h, target_w), dtype=torch.float32)
|
||||
|
||||
for m in masks:
|
||||
if m.dim() == 2:
|
||||
m = m.unsqueeze(0)
|
||||
|
||||
if m.shape[-2] != target_h or m.shape[-1] != target_w:
|
||||
m_temp = m.unsqueeze(0) if m.dim() == 3 else m
|
||||
m = F.interpolate(m_temp, size=(target_h, target_w), mode="bilinear").squeeze(0)
|
||||
|
||||
res = torch.max(res, m)
|
||||
|
||||
return (res,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_MaskUnion": EmAySee_MaskUnion
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_MaskUnion": "EmAySee Mask Union"
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
class EmAySee_MegapixelImageScaler:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"target_megapixels": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 64.0, "step": 0.1}),
|
||||
"multiple_of": ("INT", {"default": 8, "min": 1, "max": 64}),
|
||||
"method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], {"default": "bicubic"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT", "FLOAT")
|
||||
RETURN_NAMES = ("IMAGE", "width", "height", "actual_mp")
|
||||
FUNCTION = "resize_to_mp"
|
||||
CATEGORY = "EmAySee/Image"
|
||||
|
||||
def resize_to_mp(self, image, target_megapixels, multiple_of, method):
|
||||
_, old_h, old_w, _ = image.shape
|
||||
|
||||
aspect_ratio = old_w / old_h
|
||||
target_pixels = target_megapixels * 1_000_000
|
||||
|
||||
new_h = (target_pixels / aspect_ratio)**0.5
|
||||
new_w = new_h * aspect_ratio
|
||||
|
||||
final_w = int(round(new_w / multiple_of) * multiple_of)
|
||||
final_h = int(round(new_h / multiple_of) * multiple_of)
|
||||
|
||||
img = image.permute(0, 3, 1, 2)
|
||||
|
||||
if method == "lanczos":
|
||||
rescaled_image = F.interpolate(img, size=(final_h, final_w), mode="bicubic", align_corners=False)
|
||||
else:
|
||||
rescaled_image = F.interpolate(img, size=(final_h, final_w), mode=method, align_corners=False if method != "nearest-exact" else None)
|
||||
|
||||
rescaled_image = rescaled_image.permute(0, 2, 3, 1)
|
||||
actual_mp = (final_w * final_h) / 1_000_000
|
||||
|
||||
return (rescaled_image, final_w, final_h, actual_mp)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_MegapixelImageScaler": EmAySee_MegapixelImageScaler
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_MegapixelImageScaler": "EmAySee Megapixel Image Resizer"
|
||||
}
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
import comfy.model_management
|
||||
import json
|
||||
|
||||
class EmAySee_ModelVramTracker:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"anything": ("*", {}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "INT")
|
||||
RETURN_NAMES = ("model_list", "debug_dump", "count")
|
||||
FUNCTION = "get_models"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def get_models(self, anything):
|
||||
loaded_models_info = []
|
||||
detailed_dump = []
|
||||
loaded_models = comfy.model_management.current_loaded_models[:]
|
||||
|
||||
for idx, m in enumerate(loaded_models):
|
||||
m_info = {
|
||||
"index": idx,
|
||||
"patcher_class": m.__class__.__name__,
|
||||
"attributes": {},
|
||||
"model_wrapper": "None",
|
||||
"inner_model": "None",
|
||||
"config": "None",
|
||||
}
|
||||
|
||||
for attr in dir(m):
|
||||
if not attr.startswith("__"):
|
||||
try:
|
||||
val = getattr(m, attr)
|
||||
if isinstance(val, (str, int, float, bool)):
|
||||
m_info["attributes"][attr] = val
|
||||
except:
|
||||
pass
|
||||
|
||||
model_obj = getattr(m, "model", None)
|
||||
if model_obj:
|
||||
m_info["model_wrapper"] = model_obj.__class__.__name__
|
||||
inner_model = getattr(model_obj, "model", None)
|
||||
if inner_model:
|
||||
m_info["inner_model"] = inner_model.__class__.__name__
|
||||
|
||||
if hasattr(model_obj, "model_config"):
|
||||
m_info["config"] = model_obj.model_config.__class__.__name__
|
||||
try:
|
||||
conf_vars = vars(model_obj.model_config)
|
||||
m_info["config_details"] = {str(k): str(v) for k, v in conf_vars.items() if not k.startswith("_")}
|
||||
except:
|
||||
m_info["config_details"] = "Error dumping vars"
|
||||
|
||||
for attr in ["model_name", "name", "architecture", "type", "model_id", "sam_model", "predictor", "ckpt_name"]:
|
||||
if hasattr(model_obj, attr):
|
||||
val = getattr(model_obj, attr)
|
||||
m_info[f"found_{attr}"] = str(val.__class__.__name__ if hasattr(val, "__class__") and not isinstance(val, str) else val)
|
||||
|
||||
current_name = "Unknown"
|
||||
if m_info.get("found_model_name"): current_name = m_info["found_model_name"]
|
||||
elif m_info.get("found_sam_model"): current_name = f"SAM:{m_info['found_sam_model']}"
|
||||
elif m_info["config"] != "None": current_name = m_info["config"]
|
||||
elif m_info["inner_model"] != "None": current_name = m_info["inner_model"]
|
||||
else: current_name = m_info["model_wrapper"]
|
||||
|
||||
loaded_models_info.append(current_name)
|
||||
detailed_dump.append(json.dumps(m_info, indent=2))
|
||||
|
||||
if not loaded_models_info:
|
||||
return ("No models in VRAM", "VRAM Empty", 0)
|
||||
|
||||
final_list = "\n".join(loaded_models_info)
|
||||
final_dump = "\n\n========================================\n\n".join(detailed_dump)
|
||||
|
||||
return (final_list, final_dump, len(loaded_models_info))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_ModelVramTracker": EmAySee_ModelVramTracker
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_ModelVramTracker": "EmAySee Model VRAM Tracker"
|
||||
}
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
class EmAySee_MorphPipeIn:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"Breast Size", "Breast Sag", "Areola Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "PHP",
|
||||
"Extra 01", "Extra 02", "Extra 03", "Extra 04", "Extra 05",
|
||||
"Extra 06", "Extra 07", "Extra 08"
|
||||
]
|
||||
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("FLOAT", {"default": 0.0, "min": -2.0, "max": 3.0, "step": 0.01})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("MORPH_PIPE",)
|
||||
RETURN_NAMES = ("morph_pipe",)
|
||||
FUNCTION = "bundle"
|
||||
CATEGORY = "EmAySee/Morph"
|
||||
|
||||
def bundle(self, **kwargs):
|
||||
return (kwargs,)
|
||||
|
||||
class EmAySee_MorphPipeOut:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"morph_pipe": ("MORPH_PIPE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",) * 20
|
||||
RETURN_NAMES = (
|
||||
"Breast Size", "Breast Sag", "Areola Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "PHP",
|
||||
"Extra 01", "Extra 02", "Extra 03", "Extra 04", "Extra 05",
|
||||
"Extra 06", "Extra 07", "Extra 08"
|
||||
)
|
||||
FUNCTION = "expand"
|
||||
CATEGORY = "EmAySee/Morph"
|
||||
|
||||
def expand(self, morph_pipe):
|
||||
labels = [
|
||||
"Breast Size", "Breast Sag", "Areola Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "PHP",
|
||||
"Extra 01", "Extra 02", "Extra 03", "Extra 04", "Extra 05",
|
||||
"Extra 06", "Extra 07", "Extra 08"
|
||||
]
|
||||
|
||||
results = [morph_pipe.get(label, 0.0) for label in labels]
|
||||
return tuple(results)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_MorphPipeIn": EmAySee_MorphPipeIn,
|
||||
"EmAySee_MorphPipeOut": EmAySee_MorphPipeOut
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_MorphPipeIn": "EmAySee Morph Pipe In",
|
||||
"EmAySee_MorphPipeOut": "EmAySee Morph Pipe Out"
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
import torch
|
||||
|
||||
class EmAySee_MultiMaskToBBOX:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mask_1": ("MASK",),
|
||||
},
|
||||
"optional": {
|
||||
"mask_2": ("MASK",),
|
||||
"mask_3": ("MASK",),
|
||||
"mask_4": ("MASK",),
|
||||
"mask_5": ("MASK",),
|
||||
"mask_6": ("MASK",),
|
||||
"mask_7": ("MASK",),
|
||||
"mask_8": ("MASK",),
|
||||
"mask_9": ("MASK",),
|
||||
"mask_10": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("BBOX", "INT", "INT", "INT", "INT")
|
||||
RETURN_NAMES = ("bbox", "x", "y", "width", "height")
|
||||
FUNCTION = "extract_bbox"
|
||||
CATEGORY = "EmAySee/Masking"
|
||||
|
||||
def extract_bbox(self, mask_1, **kwargs):
|
||||
masks = [mask_1]
|
||||
for i in range(2, 11):
|
||||
m = kwargs.get(f"mask_{i}")
|
||||
if m is not None:
|
||||
masks.append(m)
|
||||
|
||||
all_nonzero = []
|
||||
for mask in masks:
|
||||
if mask.dim() == 2:
|
||||
mask = mask.unsqueeze(0)
|
||||
|
||||
nonzero = torch.nonzero(mask)
|
||||
if nonzero.size(0) > 0:
|
||||
all_nonzero.append(nonzero)
|
||||
|
||||
if not all_nonzero:
|
||||
return ([0, 0, 0, 0], 0, 0, 0, 0)
|
||||
|
||||
combined_nonzero = torch.cat(all_nonzero, dim=0)
|
||||
|
||||
y_min = torch.min(combined_nonzero[:, 1]).item()
|
||||
y_max = torch.max(combined_nonzero[:, 1]).item()
|
||||
x_min = torch.min(combined_nonzero[:, 2]).item()
|
||||
x_max = torch.max(combined_nonzero[:, 2]).item()
|
||||
|
||||
width = (x_max - x_min) + 1
|
||||
height = (y_max - y_min) + 1
|
||||
|
||||
return ([x_min, y_min, width, height], int(x_min), int(y_min), int(width), int(height))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_MultiMaskToBBOX": EmAySee_MultiMaskToBBOX
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_MultiMaskToBBOX": "EmAySee_Multi Mask to BBOX"
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
class EmAySee_PromptStateParser:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text_input": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "state")
|
||||
FUNCTION = "parse"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def parse(self, text_input):
|
||||
prompt_marker = "PROMPT:"
|
||||
state_marker = "STATE:"
|
||||
|
||||
p_idx = text_input.find(prompt_marker)
|
||||
s_idx = text_input.find(state_marker)
|
||||
|
||||
prompt_out = ""
|
||||
state_out = ""
|
||||
|
||||
if p_idx != -1:
|
||||
start = p_idx + len(prompt_marker)
|
||||
if s_idx != -1 and s_idx > p_idx:
|
||||
prompt_out = text_input[start:s_idx].strip()
|
||||
else:
|
||||
prompt_out = text_input[start:].strip()
|
||||
|
||||
if s_idx != -1:
|
||||
start = s_idx + len(state_marker)
|
||||
if p_idx != -1 and p_idx > s_idx:
|
||||
state_out = text_input[start:p_idx].strip()
|
||||
else:
|
||||
state_out = text_input[start:].strip()
|
||||
|
||||
return (prompt_out, state_out)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_PromptStateParser": EmAySee_PromptStateParser
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_PromptStateParser": "EmAySee Prompt State Parser"
|
||||
}
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
import gc
|
||||
import torch
|
||||
import numpy as np
|
||||
import psutil
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration, Qwen2_5_VLForConditionalGeneration, AutoModelForImageTextToText
|
||||
except ImportError:
|
||||
AutoProcessor = None
|
||||
Qwen3VLForConditionalGeneration = None
|
||||
Qwen2_5_VLForConditionalGeneration = None
|
||||
AutoModelForImageTextToText = None
|
||||
|
||||
class EmAySee_QwenVLLocal:
|
||||
_MODEL = None
|
||||
_PROCESSOR = None
|
||||
_CURRENT_PATH = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"model_path": ("STRING", {"default": "/ai/datasets/ComfyUI/models/LLM/DavidAU/gemma-3-4b-it-vl-Heretic-SuperBrain7x-Uncensored"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "Analyze this image and generate a detailed image generation prompt. Output only the prompt."}),
|
||||
"user_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"max_tokens": ("INT", {"default": 2048, "min": 16, "max": 8192}),
|
||||
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"max_memory_percent": ("FLOAT", {"default": 0.95, "min": 0.10, "max": 1.00, "step": 0.01}),
|
||||
"unload_after": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "thinking")
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "EmAySee"
|
||||
|
||||
def _tensor_to_pil(self, image_tensor):
|
||||
img0 = image_tensor[0].detach().cpu().float().clamp(0, 1)
|
||||
arr = (img0.numpy() * 255.0).round().astype(np.uint8)
|
||||
return Image.fromarray(arr, mode="RGB")
|
||||
|
||||
def load_model(self, model_path, max_memory_percent):
|
||||
if EmAySee_QwenVLLocal._MODEL is not None and EmAySee_QwenVLLocal._CURRENT_PATH == model_path:
|
||||
return EmAySee_QwenVLLocal._MODEL, EmAySee_QwenVLLocal._PROCESSOR
|
||||
|
||||
if AutoProcessor is None:
|
||||
raise RuntimeError("transformers library is missing or out of date.")
|
||||
|
||||
EmAySee_QwenVLLocal._PROCESSOR = AutoProcessor.from_pretrained(model_path)
|
||||
|
||||
path_lower = model_path.lower()
|
||||
if "gemma" in path_lower:
|
||||
model_class = AutoModelForImageTextToText
|
||||
elif "qwen2.5" in path_lower or "qwen2-5" in path_lower or "qwen2_5" in path_lower:
|
||||
model_class = Qwen2_5_VLForConditionalGeneration
|
||||
elif "qwen" in path_lower:
|
||||
model_class = Qwen3VLForConditionalGeneration
|
||||
else:
|
||||
model_class = AutoModelForImageTextToText
|
||||
|
||||
if model_class is None:
|
||||
raise RuntimeError("Required model architecture not found in transformers. Please update transformers.")
|
||||
|
||||
max_mem = {}
|
||||
if torch.cuda.is_available():
|
||||
for i in range(torch.cuda.device_count()):
|
||||
max_mem[i] = int(torch.cuda.get_device_properties(i).total_memory * max_memory_percent)
|
||||
max_mem["cpu"] = int(psutil.virtual_memory().total * max_memory_percent)
|
||||
|
||||
EmAySee_QwenVLLocal._MODEL = model_class.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map="auto",
|
||||
max_memory=max_mem
|
||||
)
|
||||
EmAySee_QwenVLLocal._MODEL.eval()
|
||||
EmAySee_QwenVLLocal._CURRENT_PATH = model_path
|
||||
|
||||
return EmAySee_QwenVLLocal._MODEL, EmAySee_QwenVLLocal._PROCESSOR
|
||||
|
||||
def unload_model(self):
|
||||
EmAySee_QwenVLLocal._MODEL = None
|
||||
EmAySee_QwenVLLocal._PROCESSOR = None
|
||||
EmAySee_QwenVLLocal._CURRENT_PATH = None
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
def generate(self, image, model_path, system_prompt, user_prompt, max_tokens, temperature, max_memory_percent, unload_after):
|
||||
pil_image = self._tensor_to_pil(image)
|
||||
model, processor = self.load_model(model_path, max_memory_percent)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": [{"type": "text", "text": system_prompt.strip()}]},
|
||||
{"role": "user", "content": [
|
||||
{"type": "image", "image": pil_image},
|
||||
{"type": "text", "text": user_prompt.strip()}
|
||||
]}
|
||||
]
|
||||
|
||||
inputs = processor.apply_chat_template(
|
||||
messages,
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
return_dict=True,
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
inputs = inputs.to(model.device)
|
||||
|
||||
gen_kwargs = {"max_new_tokens": int(max_tokens)}
|
||||
if temperature > 0:
|
||||
gen_kwargs.update({"do_sample": True, "temperature": float(temperature), "top_p": 0.9})
|
||||
else:
|
||||
gen_kwargs.update({"do_sample": False})
|
||||
|
||||
with torch.inference_mode():
|
||||
output_ids = model.generate(**inputs, **gen_kwargs)
|
||||
|
||||
trimmed_ids = [out_id[len(in_id):] for in_id, out_id in zip(inputs.input_ids, output_ids)]
|
||||
output_text = processor.batch_decode(trimmed_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
|
||||
|
||||
thinking = ""
|
||||
prompt = output_text
|
||||
|
||||
think_end_tag = "</think>"
|
||||
if think_end_tag in output_text:
|
||||
parts = output_text.split(think_end_tag, 1)
|
||||
thinking = parts[0].replace("<think>", "").strip()
|
||||
prompt = parts[1].strip()
|
||||
|
||||
if unload_after:
|
||||
self.unload_model()
|
||||
|
||||
return (prompt, thinking)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_QwenVLLocal": EmAySee_QwenVLLocal
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_QwenVLLocal": "EmAySee_Vision LLM Local Generator"
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
import torch
|
||||
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
|
||||
resolutions = [str(x) for x in range(784, 2241, 112)]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"longest_edge": (resolutions, {"default": "1232"}),
|
||||
"alignment": (["112", "56", "224"], {"default": "112"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT", "STRING")
|
||||
RETURN_NAMES = ("IMAGE", "width", "height", "resolution_text")
|
||||
FUNCTION = "align_and_resize"
|
||||
CATEGORY = "EmAySee/Image"
|
||||
|
||||
def align_and_resize(self, image, longest_edge, alignment):
|
||||
align_val = int(alignment)
|
||||
target_edge = int(longest_edge)
|
||||
B, H, W, C = image.shape
|
||||
|
||||
# Calculate aspect ratio and new dimensions
|
||||
current_longest = max(H, W)
|
||||
|
||||
# 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
|
||||
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
|
||||
new_height = max(align_val, new_height)
|
||||
new_width = max(align_val, new_width)
|
||||
|
||||
# Permute image to [B, C, H, W] for interpolation
|
||||
img_permuted = image.permute(0, 3, 1, 2)
|
||||
|
||||
# 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]
|
||||
final_image = resized_img.permute(0, 2, 3, 1)
|
||||
|
||||
# Create resolution string
|
||||
res_text = f"{new_width}x{new_height}"
|
||||
|
||||
return (final_image, new_width, new_height, res_text)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_QwenPixelAligner": EmAySee_QwenPixelAligner
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_QwenPixelAligner": "EmAySee Qwen Pixel Aligner"
|
||||
}
|
||||
Executable
+475
@@ -0,0 +1,475 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from io import BytesIO
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import gc
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
import requests
|
||||
|
||||
try:
|
||||
from transformers import AutoProcessor
|
||||
from transformers import Qwen2_5_VLForConditionalGeneration
|
||||
from transformers import Qwen3VLForConditionalGeneration
|
||||
except Exception:
|
||||
AutoProcessor = None
|
||||
Qwen2_5_VLForConditionalGeneration = None
|
||||
Qwen3VLForConditionalGeneration = None
|
||||
|
||||
def _tensor_image_to_pil(image_tensor: torch.Tensor) -> Image.Image:
|
||||
if not isinstance(image_tensor, torch.Tensor):
|
||||
raise TypeError("IMAGE input must be a torch.Tensor")
|
||||
if image_tensor.ndim != 4 or image_tensor.shape[-1] != 3:
|
||||
raise ValueError(f"Expected IMAGE with shape [B,H,W,3], got {tuple(image_tensor.shape)}")
|
||||
img0 = image_tensor[0].detach().cpu().float().clamp(0, 1)
|
||||
arr = (img0.numpy() * 255.0).round().astype(np.uint8)
|
||||
return Image.fromarray(arr, mode="RGB")
|
||||
|
||||
def _clean_single_line(text: str) -> str:
|
||||
text = text.strip()
|
||||
text = text.replace("\r", " ").replace("\n", " ")
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
if len(text) >= 2 and ((text[0] == '"' and text[-1] == '"') or (text[0] == "'" and text[-1] == "'")):
|
||||
text = text[1:-1].strip()
|
||||
return text
|
||||
|
||||
def _available_devices() -> Tuple[str, ...]:
|
||||
devices = []
|
||||
if torch.cuda.is_available():
|
||||
for i in range(torch.cuda.device_count()):
|
||||
name = torch.cuda.get_device_name(i)
|
||||
devices.append(f"cuda:{i} | {name}")
|
||||
devices.append("cpu")
|
||||
return tuple(devices)
|
||||
|
||||
def _parse_device_choice(device_choice: str) -> str:
|
||||
return device_choice.split("|", 1)[0].strip()
|
||||
|
||||
def _cuda_cleanup(device_index: int | None):
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
if device_index is None:
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
else:
|
||||
with torch.cuda.device(device_index):
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
def _resolve_model_family(model_id: str) -> str:
|
||||
model_lower = model_id.lower()
|
||||
if "qwen2.5" in model_lower:
|
||||
return "qwen2_5_vl"
|
||||
elif "qwen3" in model_lower:
|
||||
return "qwen3_vl"
|
||||
else:
|
||||
raise ValueError(f"Unknown model ID: {model_id}")
|
||||
|
||||
class EmAySee_QwenPromptFromImage:
|
||||
_MODEL_CACHE: Dict[Tuple[str, str, str, str], object] = {}
|
||||
_PROC_CACHE: Dict[str, object] = {}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
default_system = (
|
||||
"You convert the given image into a rich, image-generation prompt.\n"
|
||||
"Return ONLY the final prompt as a single line, no quotes, no extra text.\n"
|
||||
"Include: subject, environment, style, lighting, camera/lens, composition, key details.\n"
|
||||
"Avoid meta-commentary."
|
||||
)
|
||||
|
||||
model_list = (
|
||||
"Qwen/Qwen2.5-VL-3B-Instruct",
|
||||
"Qwen/Qwen2.5-VL-7B-Instruct",
|
||||
"Qwen/Qwen3-VL-2B-Instruct",
|
||||
"Qwen/Qwen3-VL-2B-Thinking",
|
||||
"Qwen/Qwen3-VL-8B-Instruct",
|
||||
"Qwen/Qwen3-VL-8B-Thinking",
|
||||
"Qwen/Qwen3-VL-235B-A22B-Instruct",
|
||||
"Qwen/Qwen3-VL-235B-A22B-Thinking",
|
||||
)
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"backend": (("local", "openai_compatible"),),
|
||||
"qwen_model": (model_list,),
|
||||
"local_model_path": ("STRING", {"default": ""}),
|
||||
"device": (_available_devices(),),
|
||||
"quantization": ((
|
||||
"int8_bnb",
|
||||
"int4_bnb",
|
||||
"bf16",
|
||||
"fp16",
|
||||
"fp32_cpu",
|
||||
),),
|
||||
"flash_attention": (("off", "on"),),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": default_system}),
|
||||
"user_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"max_new_tokens": ("INT", {"default": 2048, "min": 16, "max": 8192, "step": 1}),
|
||||
"temperature": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"safe_quants": ([True, False], {"default": True}),
|
||||
"unload_model": ([True, False], {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"openai_base_url": ("STRING", {"default": "http://127.0.0.1:11434"}),
|
||||
"openai_api_key": ("STRING", {"default": ""}),
|
||||
"openai_model_override": ("STRING", {"default": ""}),
|
||||
"openrouter_providers": ("STRING", {"default": ""}),
|
||||
"verbose_output": ([True, False], {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING","STRING")
|
||||
RETURN_NAMES = ("prompt_text", "thinking_text")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "d3cker/Prompt-Generator"
|
||||
|
||||
def _get_processor(self, model_id: str, load_path: str):
|
||||
if AutoProcessor is None:
|
||||
raise RuntimeError("transformers is missing or too old.")
|
||||
if self._PROC_CACHE.get(load_path) is None:
|
||||
self._PROC_CACHE[load_path] = AutoProcessor.from_pretrained(load_path)
|
||||
return self._PROC_CACHE[load_path]
|
||||
|
||||
def _load_local_model(self, model_id: str, load_path: str, device_choice: str, quantization: str, flash_attention: str, safe_quants: bool):
|
||||
model_family = _resolve_model_family(model_id)
|
||||
|
||||
if Qwen2_5_VLForConditionalGeneration is None or Qwen3VLForConditionalGeneration is None:
|
||||
raise RuntimeError("transformers is missing or too old.")
|
||||
|
||||
device = _parse_device_choice(device_choice)
|
||||
|
||||
if safe_quants:
|
||||
if device == "cpu" and quantization != "fp32_cpu":
|
||||
quantization = "fp32_cpu"
|
||||
elif device.lower().startswith("cuda"):
|
||||
if model_family == "qwen2_5_vl" and (quantization == "fp16" or quantization == "fp32_cpu"):
|
||||
quantization = "bf16"
|
||||
elif model_family == "qwen3_vl" and quantization == "fp32_cpu":
|
||||
quantization = "bf16"
|
||||
|
||||
if quantization == "fp32_cpu":
|
||||
dtype = torch.float32
|
||||
elif quantization == "bf16":
|
||||
dtype = torch.bfloat16
|
||||
elif quantization == "fp16":
|
||||
dtype = torch.float16
|
||||
else:
|
||||
dtype = torch.bfloat16 if (device.startswith("cuda") and torch.cuda.is_available()) else torch.float32
|
||||
|
||||
dtype_key = str(dtype).replace("torch.", "")
|
||||
cache_key = (load_path, quantization, device, dtype_key)
|
||||
|
||||
kwargs = {
|
||||
"torch_dtype": dtype,
|
||||
"device_map": "auto" if device.startswith("cuda") else {"": device},
|
||||
}
|
||||
|
||||
cached = self._MODEL_CACHE.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
if quantization in ("int8_bnb", "int4_bnb"):
|
||||
if not device.startswith("cuda"):
|
||||
raise RuntimeError("bitsandbytes requires a CUDA device.")
|
||||
try:
|
||||
from transformers import BitsAndBytesConfig
|
||||
import bitsandbytes
|
||||
except Exception as e:
|
||||
raise RuntimeError("BitsAndBytes missing.") from e
|
||||
|
||||
if quantization == "int8_bnb":
|
||||
qconf = BitsAndBytesConfig(load_in_8bit=True)
|
||||
else:
|
||||
qconf = BitsAndBytesConfig(
|
||||
load_in_4bit=True,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
bnb_4bit_use_double_quant=True,
|
||||
bnb_4bit_compute_dtype=dtype,
|
||||
)
|
||||
kwargs["quantization_config"] = qconf
|
||||
|
||||
if flash_attention == "on":
|
||||
kwargs["attn_implementation"] = "flash_attention_2"
|
||||
|
||||
if model_family == "qwen2_5_vl":
|
||||
self._MODEL_CACHE[cache_key] = Qwen2_5_VLForConditionalGeneration.from_pretrained(load_path, **kwargs)
|
||||
elif model_family == "qwen3_vl":
|
||||
self._MODEL_CACHE[cache_key] = Qwen3VLForConditionalGeneration.from_pretrained(load_path, **kwargs)
|
||||
|
||||
self._MODEL_CACHE[cache_key].eval()
|
||||
return self._MODEL_CACHE[cache_key]
|
||||
|
||||
def _generate_local(
|
||||
self,
|
||||
image_pil: Image.Image,
|
||||
model_id: str,
|
||||
load_path: str,
|
||||
device_choice: str,
|
||||
quantization: str,
|
||||
flash_attention: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
max_new_tokens: int,
|
||||
temperature: float,
|
||||
safe_quants: bool,
|
||||
unload_model: bool,
|
||||
) -> str:
|
||||
model = self._load_local_model(model_id, load_path, device_choice, quantization, flash_attention, safe_quants)
|
||||
processor = self._get_processor(model_id, load_path)
|
||||
|
||||
if system_prompt.strip() == "":
|
||||
text_instruction = "Convert the image into a rich image-generation prompt."
|
||||
else:
|
||||
text_instruction = system_prompt.strip()
|
||||
|
||||
if user_prompt and user_prompt.strip():
|
||||
text_instruction = f"{text_instruction}\nAdditional requirements:\n{user_prompt.strip()}"
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"type": "text", "text": system_prompt.strip()}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "image", "image": image_pil},
|
||||
{"type": "text", "text": text_instruction},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
inputs = processor.apply_chat_template(
|
||||
messages,
|
||||
tokenize=True,
|
||||
add_generation_prompt=True,
|
||||
return_dict=True,
|
||||
return_tensors="pt",
|
||||
)
|
||||
|
||||
inputs = inputs.to(model.device)
|
||||
|
||||
gen_kwargs = {
|
||||
"max_new_tokens": int(max_new_tokens),
|
||||
}
|
||||
if temperature and temperature > 0:
|
||||
gen_kwargs.update({"do_sample": True, "temperature": float(temperature), "top_p": 0.9})
|
||||
else:
|
||||
gen_kwargs.update({"do_sample": False})
|
||||
|
||||
with torch.inference_mode():
|
||||
out_ids = model.generate(**inputs, **gen_kwargs)
|
||||
|
||||
trimmed = [o[len(i):] for i, o in zip(inputs.input_ids, out_ids)]
|
||||
|
||||
out_text = processor.batch_decode(
|
||||
trimmed,
|
||||
skip_special_tokens=True,
|
||||
clean_up_tokenization_spaces=False,
|
||||
)[0]
|
||||
|
||||
sep = "</think>"
|
||||
idx = out_text.find(sep)
|
||||
if idx != -1:
|
||||
pstart = idx + len(sep)
|
||||
prompt = out_text[pstart:]
|
||||
reasoning = out_text[:idx]
|
||||
else:
|
||||
reasoning = ""
|
||||
prompt = out_text
|
||||
|
||||
if unload_model:
|
||||
for key in self._MODEL_CACHE:
|
||||
self._MODEL_CACHE[key] = None
|
||||
for key in self._PROC_CACHE:
|
||||
self._PROC_CACHE[key] = None
|
||||
model = None
|
||||
processor = None
|
||||
inputs = None
|
||||
_cuda_cleanup(None)
|
||||
|
||||
return _clean_single_line(prompt), reasoning
|
||||
|
||||
def _generate_openai_compatible(
|
||||
self,
|
||||
image_pil: Image.Image,
|
||||
model_id: str,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
model_override: str,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
max_new_tokens: int,
|
||||
temperature: float,
|
||||
providers: str = "",
|
||||
verbose: bool = True,
|
||||
) -> str:
|
||||
buf = BytesIO()
|
||||
image_pil.save(buf, format="PNG")
|
||||
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
data_url = f"data:image/png;base64,{b64}"
|
||||
if system_prompt.strip() == "":
|
||||
text_instruction = "Convert the image into a rich image-generation prompt."
|
||||
else:
|
||||
text_instruction = system_prompt.strip()
|
||||
if user_prompt and user_prompt.strip():
|
||||
text_instruction = f"{text_instruction}\nAdditional requirements:\n{user_prompt.strip()}"
|
||||
|
||||
url = base_url.rstrip("/") + "/v1/chat/completions"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key and api_key.strip():
|
||||
headers["Authorization"] = f"Bearer {api_key.strip()}"
|
||||
|
||||
payload = {
|
||||
"model": (model_override.strip() if model_override and model_override.strip() else model_id),
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt.strip()},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": text_instruction},
|
||||
{"type": "image_url", "image_url": {"url": data_url}},
|
||||
],
|
||||
},
|
||||
],
|
||||
"temperature": float(temperature),
|
||||
"max_tokens": int(max_new_tokens),
|
||||
}
|
||||
|
||||
if providers and providers.strip():
|
||||
provider_list = [p.strip() for p in providers.split(",") if p.strip()]
|
||||
if provider_list:
|
||||
payload["provider"] = {
|
||||
"order": provider_list,
|
||||
"allow_fallbacks": False
|
||||
}
|
||||
|
||||
resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=600)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"OpenAI-compatible endpoint error {resp.status_code}: {resp.text}")
|
||||
|
||||
data = resp.json()
|
||||
|
||||
if verbose:
|
||||
provider = data.get("provider", "unknown")
|
||||
model = data.get("model", "unknown")
|
||||
usage = data.get("usage", {})
|
||||
prompt_tokens = usage.get("prompt_tokens", 0)
|
||||
completion_tokens = usage.get("completion_tokens", 0)
|
||||
total_tokens = usage.get("total_tokens", 0)
|
||||
cost = usage.get("cost", 0)
|
||||
cached_tokens = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)
|
||||
|
||||
lines = [
|
||||
f"Provider: {provider}",
|
||||
f"Model: {model}",
|
||||
f"Tokens: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} total",
|
||||
]
|
||||
if cached_tokens > 0:
|
||||
lines.append(f"Cached: {cached_tokens} tokens")
|
||||
lines.append(f"Cost: ${cost:.6f}")
|
||||
|
||||
width = max(len(line) for line in lines) + 2
|
||||
title = " OpenAI API Response "
|
||||
top_border = f"┌{title}{'─' * (width - len(title))}┐"
|
||||
bottom_border = f"└{'─' * width}┘"
|
||||
|
||||
print(f"\n{top_border}")
|
||||
for line in lines:
|
||||
print(f"│ {line.ljust(width - 1)}│")
|
||||
print(f"{bottom_border}\n")
|
||||
|
||||
reasoning = (
|
||||
data.get("choices", [{}])[0]
|
||||
.get("message", {})
|
||||
.get("reasoning")
|
||||
)
|
||||
|
||||
content = (
|
||||
data.get("choices", [{}])[0]
|
||||
.get("message", {})
|
||||
.get("content")
|
||||
)
|
||||
|
||||
if reasoning == None:
|
||||
reasoning = ""
|
||||
|
||||
return _clean_single_line(content), reasoning
|
||||
|
||||
def run(
|
||||
self,
|
||||
image,
|
||||
backend,
|
||||
qwen_model,
|
||||
local_model_path,
|
||||
device,
|
||||
quantization,
|
||||
flash_attention,
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
max_new_tokens,
|
||||
temperature,
|
||||
safe_quants,
|
||||
unload_model,
|
||||
openai_base_url="http://127.0.0.1:11434",
|
||||
openai_api_key="",
|
||||
openai_model_override="",
|
||||
openrouter_providers="",
|
||||
verbose_output=True,
|
||||
):
|
||||
image_pil = _tensor_image_to_pil(image)
|
||||
load_path = local_model_path.strip() if local_model_path.strip() else qwen_model
|
||||
|
||||
if backend == "openai_compatible":
|
||||
if not openai_api_key or not openai_api_key.strip():
|
||||
if "OPENROUTER_API_KEY" in os.environ:
|
||||
openai_api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
||||
if openai_base_url in ("http://127.0.0.1:11434", "") or not openai_base_url.strip():
|
||||
openai_base_url = "https://openrouter.ai/api"
|
||||
elif "OPENAI_API_KEY" in os.environ:
|
||||
openai_api_key = os.environ.get("OPENAI_API_KEY", "")
|
||||
|
||||
prompt, reasoning = self._generate_openai_compatible(
|
||||
image_pil=image_pil,
|
||||
model_id=qwen_model,
|
||||
base_url=openai_base_url,
|
||||
api_key=openai_api_key,
|
||||
model_override=openai_model_override,
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
max_new_tokens=max_new_tokens,
|
||||
temperature=temperature,
|
||||
providers=openrouter_providers,
|
||||
verbose=verbose_output,
|
||||
)
|
||||
else:
|
||||
prompt, reasoning = self._generate_local(
|
||||
image_pil=image_pil,
|
||||
model_id=qwen_model,
|
||||
load_path=load_path,
|
||||
device_choice=device,
|
||||
quantization=quantization,
|
||||
flash_attention=flash_attention,
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
max_new_tokens=max_new_tokens,
|
||||
temperature=temperature,
|
||||
safe_quants=safe_quants,
|
||||
unload_model=unload_model
|
||||
)
|
||||
|
||||
return (prompt,reasoning,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_QwenPromptFromImage": EmAySee_QwenPromptFromImage,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_QwenPromptFromImage": "EmAySee_Qwen / OpenAI Prompt From Image",
|
||||
}
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
class EmAySee_QwenResolutionOptimizer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"scale_multiplier": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 4.0, "step": 0.1}),
|
||||
"method": (["crop", "stretch"], {"default": "crop"}),
|
||||
"upscale_method": (["bicubic", "nearest-exact", "bilinear", "area"], {"default": "bicubic"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT", "STRING")
|
||||
RETURN_NAMES = ("IMAGE", "width", "height", "aspect_ratio_name")
|
||||
FUNCTION = "optimize"
|
||||
CATEGORY = "EmAySee_Nodes/Image/Alignment"
|
||||
|
||||
def optimize(self, image, scale_multiplier, method, upscale_method):
|
||||
B, H, W, C = image.shape
|
||||
input_aspect = W / H
|
||||
|
||||
targets = [
|
||||
(1.0, 1008, 1008, "1:1 Square"),
|
||||
(1.25, 1120, 896, "4:3 Landscape"),
|
||||
(1.375, 1232, 896, "3:2 Classic"),
|
||||
(1.714, 1344, 784, "16:9 Widescreen"),
|
||||
(0.8, 896, 1120, "3:4 Portrait"),
|
||||
(0.727, 896, 1232, "2:3 Classic Portrait"),
|
||||
(0.583, 784, 1344, "9:16 Vertical")
|
||||
]
|
||||
|
||||
best_match = min(targets, key=lambda x: abs(x[0] - input_aspect))
|
||||
base_w, base_h, ratio_name = best_match[1], best_match[2], best_match[3]
|
||||
|
||||
target_w = int(round((base_w * scale_multiplier) / 112) * 112)
|
||||
target_h = int(round((base_h * scale_multiplier) / 112) * 112)
|
||||
|
||||
if target_w == 0: target_w = 112
|
||||
if target_h == 0: target_h = 112
|
||||
|
||||
samples = image.movedim(-1, 1)
|
||||
|
||||
if method == "crop":
|
||||
target_aspect = base_w / base_h
|
||||
if input_aspect > target_aspect:
|
||||
new_w = int(H * target_aspect)
|
||||
offset = (W - new_w) // 2
|
||||
samples = samples[:, :, :, offset:offset + new_w]
|
||||
else:
|
||||
new_h = int(W / target_aspect)
|
||||
offset = (H - new_h) // 2
|
||||
samples = samples[:, :, offset:offset + new_h, :]
|
||||
|
||||
samples = F.interpolate(samples, size=(target_h, target_w), mode=upscale_method)
|
||||
samples = samples.movedim(1, -1)
|
||||
|
||||
final_info = f"{ratio_name} @ {scale_multiplier}x ({target_w}x{target_h})"
|
||||
|
||||
return (samples, target_w, target_h, final_info)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_QwenResolutionOptimizer": EmAySee_QwenResolutionOptimizer
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_QwenResolutionOptimizer": "EmAySee_ Qwen Resolution Optimizer"
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
import random
|
||||
|
||||
class EmAySee_RandomStringSelector:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {}
|
||||
}
|
||||
|
||||
for i in range(1, 21):
|
||||
inputs["optional"][f"string_{i}"] = ("STRING",)
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_string",)
|
||||
FUNCTION = "select_random_string"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def select_random_string(self, seed, **kwargs):
|
||||
valid_strings = []
|
||||
for i in range(1, 21):
|
||||
s = kwargs.get(f"string_{i}", None)
|
||||
if s is not None:
|
||||
valid_strings.append(s)
|
||||
|
||||
selected_string = ""
|
||||
if valid_strings:
|
||||
random.seed(seed)
|
||||
selected_string = random.choice(valid_strings)
|
||||
|
||||
return (selected_string,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_RandomStringSelector": EmAySee_RandomStringSelector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_RandomStringSelector": "EmAySee Random String Selector (20)"
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
class EmAySee_SegsUnion:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"fallback_segs": ("SEGS",),
|
||||
}
|
||||
}
|
||||
for i in range(1, 11):
|
||||
inputs["optional"][f"segs_{i}"] = ("SEGS",)
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("SEGS",)
|
||||
RETURN_NAMES = ("segs",)
|
||||
FUNCTION = "combine"
|
||||
CATEGORY = "EmAySee/Segs"
|
||||
|
||||
def combine(self, fallback_segs=None, **kwargs):
|
||||
combined_items = []
|
||||
base_shape = None
|
||||
|
||||
for i in range(1, 11):
|
||||
seg_input = kwargs.get(f"segs_{i}")
|
||||
if seg_input is not None:
|
||||
if base_shape is None:
|
||||
base_shape = seg_input[0]
|
||||
combined_items.extend(seg_input[1])
|
||||
|
||||
if not combined_items:
|
||||
if fallback_segs is not None:
|
||||
return (fallback_segs,)
|
||||
return ((base_shape if base_shape else (64, 64), []),)
|
||||
|
||||
return ((base_shape, combined_items),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SegsUnion": EmAySee_SegsUnion
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SegsUnion": "EmAySee Segs Union"
|
||||
}
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
import torch
|
||||
import gc
|
||||
import comfy.model_management
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning, module="torch.distributed")
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="torch.nn.modules.module")
|
||||
|
||||
class EmAySee_SelectiveModelUnloader:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"model_name_filter": ("STRING", {"default": "ModelConfig"}),
|
||||
"deep_gc_scan": ("BOOLEAN", {"default": False}),
|
||||
"exclude_checkpoints": ("BOOLEAN", {"default": True}),
|
||||
"unload_mode": (["move_to_cpu", "delete_references"], {"default": "move_to_cpu"}),
|
||||
"purge_cache": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "STRING")
|
||||
RETURN_NAMES = ("image", "unloaded_count", "debug_info")
|
||||
FUNCTION = "unload_specific"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def unload_specific(self, image, model_name_filter, deep_gc_scan, exclude_checkpoints, unload_mode, purge_cache):
|
||||
unloaded_count = 0
|
||||
debug_logs = []
|
||||
|
||||
filters = [f.strip().lower() for f in model_name_filter.split(",")]
|
||||
|
||||
CHECKPOINT_TYPES = [
|
||||
"SD15", "SD21", "SDXL", "SDXLRefiner", "SVD_img2vid",
|
||||
"Flux", "SD3", "Stable_Cascade", "SD_ASR", "PlaygroundV2", "Hyvid", "Wan", "HunyuanVideo"
|
||||
]
|
||||
|
||||
remaining_models = []
|
||||
loaded_models = comfy.model_management.current_loaded_models[:]
|
||||
|
||||
for m in loaded_models:
|
||||
model_obj = m.model
|
||||
inner_model = getattr(model_obj, "model", None)
|
||||
|
||||
meta_name = "Unknown"
|
||||
is_checkpoint = False
|
||||
|
||||
if hasattr(model_obj, "model_config"):
|
||||
meta_name = model_obj.model_config.__class__.__name__
|
||||
if any(cp_type in meta_name for cp_type in CHECKPOINT_TYPES):
|
||||
is_checkpoint = True
|
||||
elif inner_model:
|
||||
meta_name = inner_model.__class__.__name__
|
||||
else:
|
||||
meta_name = model_obj.__class__.__name__
|
||||
|
||||
should_unload = (model_name_filter == "*" or
|
||||
any(f in meta_name.lower() for f in filters))
|
||||
|
||||
if should_unload:
|
||||
if exclude_checkpoints and is_checkpoint:
|
||||
debug_logs.append(f"Skipping Protected Checkpoint: {meta_name}")
|
||||
remaining_models.append(m)
|
||||
else:
|
||||
debug_logs.append(f"Standard Unload: {meta_name}")
|
||||
m.model_unload()
|
||||
unloaded_count += 1
|
||||
else:
|
||||
remaining_models.append(m)
|
||||
|
||||
comfy.model_management.current_loaded_models = remaining_models
|
||||
|
||||
if deep_gc_scan:
|
||||
for obj in list(gc.get_objects()):
|
||||
try:
|
||||
if isinstance(obj, torch.nn.Module):
|
||||
class_name = obj.__class__.__name__
|
||||
is_gpu = False
|
||||
try:
|
||||
test_tensor = next(obj.parameters(), next(obj.buffers(), None))
|
||||
if test_tensor is not None and "cuda" in str(test_tensor.device):
|
||||
is_gpu = True
|
||||
except (StopIteration, ReferenceError):
|
||||
pass
|
||||
|
||||
if is_gpu:
|
||||
is_cp_hidden = any(cp_type in class_name for cp_type in CHECKPOINT_TYPES)
|
||||
if model_name_filter == "*" or any(f in class_name.lower() for f in filters):
|
||||
if exclude_checkpoints and is_cp_hidden:
|
||||
continue
|
||||
obj.to("cpu")
|
||||
unloaded_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
if purge_cache:
|
||||
gc.collect()
|
||||
comfy.model_management.soft_empty_cache()
|
||||
|
||||
# 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)
|
||||
if hasattr(comfy.model_management, "node_cache"):
|
||||
comfy.model_management.node_cache.clear()
|
||||
|
||||
# Brute force memory cleanup
|
||||
try:
|
||||
comfy.model_management.cleanup_models(keep_free_mem=0)
|
||||
except:
|
||||
pass
|
||||
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
elif unloaded_count > 0:
|
||||
gc.collect()
|
||||
comfy.model_management.soft_empty_cache()
|
||||
|
||||
final_debug = "\n".join(debug_logs) if debug_logs else "No matches found."
|
||||
return (image, unloaded_count, final_debug)
|
||||
|
||||
class EmAySee_SelectiveModelReloader:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"model_name_filter": ("STRING", {"default": "SamImage"}),
|
||||
"force_bfloat16": ("BOOLEAN", {"default": False}),
|
||||
"purge_cache": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "STRING")
|
||||
RETURN_NAMES = ("image", "reloaded_count", "debug_info")
|
||||
FUNCTION = "reload_specific"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def reload_specific(self, image, model_name_filter, force_bfloat16, purge_cache):
|
||||
reloaded_count = 0
|
||||
debug_logs = []
|
||||
device = comfy.model_management.get_torch_device()
|
||||
filters = [f.strip().lower() for f in model_name_filter.split(",")]
|
||||
|
||||
for obj in list(gc.get_objects()):
|
||||
try:
|
||||
if isinstance(obj, torch.nn.Module):
|
||||
class_name = obj.__class__.__name__
|
||||
if any(f in class_name.lower() for f in filters):
|
||||
is_cpu = False
|
||||
try:
|
||||
test_tensor = next(obj.parameters(), next(obj.buffers(), None))
|
||||
if test_tensor is not None and "cpu" in str(test_tensor.device):
|
||||
is_cpu = True
|
||||
except (StopIteration, ReferenceError):
|
||||
pass
|
||||
|
||||
if is_cpu:
|
||||
debug_logs.append(f"Moving back to GPU: {class_name}")
|
||||
if force_bfloat16:
|
||||
obj.to(device=device, dtype=torch.bfloat16)
|
||||
else:
|
||||
obj.to(device)
|
||||
reloaded_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
if purge_cache:
|
||||
gc.collect()
|
||||
comfy.model_management.soft_empty_cache()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
elif reloaded_count > 0:
|
||||
comfy.model_management.soft_empty_cache()
|
||||
|
||||
final_debug = "\n".join(debug_logs) if debug_logs else "No models needed reloading."
|
||||
return (image, reloaded_count, final_debug)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SelectiveModelUnloader": EmAySee_SelectiveModelUnloader,
|
||||
"EmAySee_SelectiveModelReloader": EmAySee_SelectiveModelReloader
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SelectiveModelUnloader": "EmAySee Selective Model Unloader",
|
||||
"EmAySee_SelectiveModelReloader": "EmAySee Selective Model Reloader"
|
||||
}
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
import torch
|
||||
import gc
|
||||
import comfy.model_management
|
||||
|
||||
class EmAySee_SelectiveModelUnloader:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"anything": ("*", {}),
|
||||
"model_name_filter": ("STRING", {"default": "ModelConfig"}),
|
||||
"deep_gc_scan": ("BOOLEAN", {"default": False}),
|
||||
"exclude_checkpoints": ("BOOLEAN", {"default": True}),
|
||||
"unload_mode": (["move_to_cpu", "delete_references"], {"default": "move_to_cpu"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("*", "INT", "STRING")
|
||||
RETURN_NAMES = ("output", "unloaded_count", "debug_info")
|
||||
FUNCTION = "unload_specific"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def unload_specific(self, anything, model_name_filter, deep_gc_scan, exclude_checkpoints, unload_mode):
|
||||
unloaded_count = 0
|
||||
debug_logs = []
|
||||
|
||||
CHECKPOINT_TYPES = [
|
||||
"SD15", "SD21", "SDXL", "SDXLRefiner", "SVD_img2vid",
|
||||
"Flux", "SD3", "Stable_Cascade", "SD_ASR", "PlaygroundV2"
|
||||
]
|
||||
|
||||
remaining_models = []
|
||||
loaded_models = comfy.model_management.current_loaded_models[:]
|
||||
|
||||
for m in loaded_models:
|
||||
model_obj = m.model
|
||||
inner_model = getattr(model_obj, "model", None)
|
||||
|
||||
meta_name = "Unknown"
|
||||
is_checkpoint = False
|
||||
|
||||
if hasattr(model_obj, "model_config"):
|
||||
meta_name = model_obj.model_config.__class__.__name__
|
||||
if any(cp_type in meta_name for cp_type in CHECKPOINT_TYPES):
|
||||
is_checkpoint = True
|
||||
elif inner_model:
|
||||
meta_name = inner_model.__class__.__name__
|
||||
else:
|
||||
meta_name = model_obj.__class__.__name__
|
||||
|
||||
should_unload = (model_name_filter == "*" or
|
||||
model_name_filter.lower() in meta_name.lower())
|
||||
|
||||
if should_unload:
|
||||
if exclude_checkpoints and is_checkpoint:
|
||||
debug_logs.append(f"Skipping Protected Checkpoint: {meta_name}")
|
||||
remaining_models.append(m)
|
||||
else:
|
||||
debug_logs.append(f"Standard Unload: {meta_name}")
|
||||
m.model_unload()
|
||||
unloaded_count += 1
|
||||
else:
|
||||
remaining_models.append(m)
|
||||
|
||||
comfy.model_management.current_loaded_models = remaining_models
|
||||
|
||||
if deep_gc_scan:
|
||||
debug_logs.append(f"--- Deep GC Scan (Mode: {unload_mode}) ---")
|
||||
for obj in gc.get_objects():
|
||||
try:
|
||||
if isinstance(obj, torch.nn.Module):
|
||||
class_name = obj.__class__.__name__
|
||||
|
||||
is_gpu = False
|
||||
try:
|
||||
param = next(obj.parameters(), None)
|
||||
if param is not None and param.device.type == 'cuda':
|
||||
is_gpu = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_gpu:
|
||||
is_cp_hidden = any(cp_type in class_name for cp_type in CHECKPOINT_TYPES)
|
||||
|
||||
if model_name_filter == "*" or model_name_filter.lower() in class_name.lower():
|
||||
if exclude_checkpoints and is_cp_hidden:
|
||||
debug_logs.append(f"Skipping Protected Hidden Checkpoint: {class_name}")
|
||||
continue
|
||||
|
||||
if unload_mode == "move_to_cpu":
|
||||
debug_logs.append(f"Evicting to CPU: {class_name}")
|
||||
obj.to("cpu")
|
||||
else:
|
||||
debug_logs.append(f"Nuking reference: {class_name}")
|
||||
obj.to("cpu")
|
||||
|
||||
unloaded_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
if unloaded_count > 0:
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
final_debug = "\n".join(debug_logs) if debug_logs else "No matches found."
|
||||
return (anything, unloaded_count, final_debug)
|
||||
|
||||
class EmAySee_SelectiveModelReloader:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"anything": ("*", {}),
|
||||
"model_name_filter": ("STRING", {"default": "SamImage"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("*", "INT", "STRING")
|
||||
RETURN_NAMES = ("output", "reloaded_count", "debug_info")
|
||||
FUNCTION = "reload_specific"
|
||||
CATEGORY = "EmAySee/Utils"
|
||||
|
||||
def reload_specific(self, anything, model_name_filter):
|
||||
reloaded_count = 0
|
||||
debug_logs = []
|
||||
|
||||
for obj in gc.get_objects():
|
||||
try:
|
||||
if isinstance(obj, torch.nn.Module):
|
||||
class_name = obj.__class__.__name__
|
||||
|
||||
if model_name_filter.lower() in class_name.lower():
|
||||
is_cpu = False
|
||||
try:
|
||||
param = next(obj.parameters(), None)
|
||||
if param is not None and param.device.type == 'cpu':
|
||||
is_cpu = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_cpu:
|
||||
debug_logs.append(f"Moving back to GPU: {class_name}")
|
||||
obj.to(comfy.model_management.get_torch_device())
|
||||
reloaded_count += 1
|
||||
except:
|
||||
continue
|
||||
|
||||
if reloaded_count > 0:
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
final_debug = "\n".join(debug_logs) if debug_logs else "No models needed reloading."
|
||||
return (anything, reloaded_count, final_debug)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SelectiveModelUnloader": EmAySee_SelectiveModelUnloader,
|
||||
"EmAySee_SelectiveModelReloader": EmAySee_SelectiveModelReloader
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SelectiveModelUnloader": "EmAySee Selective Model Unloader",
|
||||
"EmAySee_SelectiveModelReloader": "EmAySee Selective Model Reloader"
|
||||
}
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import torch
|
||||
import numpy as np
|
||||
import requests
|
||||
import json
|
||||
|
||||
class EmAySee_SpectreAPIConnector:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"spectre_url": ("STRING", {"default": "http://10.0.0.71:11434/v1/chat/completions"}),
|
||||
"model_name": ("STRING", {"default": "qwen3-vl"}),
|
||||
"system_prompt": ("STRING", {"multiline": True, "default": "Analyze this image and generate a detailed image generation prompt. Output only the prompt."}),
|
||||
"user_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"max_tokens": ("INT", {"default": 2048, "min": 16, "max": 8192}),
|
||||
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "thinking")
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "EmAySee"
|
||||
|
||||
def _tensor_to_base64(self, image_tensor):
|
||||
img0 = image_tensor[0].detach().cpu().float().clamp(0, 1)
|
||||
arr = (img0.numpy() * 255.0).round().astype(np.uint8)
|
||||
pil_img = Image.fromarray(arr, mode="RGB")
|
||||
buffered = BytesIO()
|
||||
pil_img.save(buffered, format="JPEG")
|
||||
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
||||
def generate(self, image, spectre_url, model_name, system_prompt, user_prompt, max_tokens, temperature):
|
||||
b64_image = self._tensor_to_base64(image)
|
||||
data_url = f"data:image/jpeg;base64,{b64_image}"
|
||||
|
||||
text_instruction = system_prompt.strip()
|
||||
if user_prompt and user_prompt.strip():
|
||||
text_instruction = f"{text_instruction}\nAdditional requirements:\n{user_prompt.strip()}"
|
||||
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": text_instruction},
|
||||
{"type": "image_url", "image_url": {"url": data_url}}
|
||||
]
|
||||
}
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
try:
|
||||
response = requests.post(spectre_url, headers=headers, data=json.dumps(payload), timeout=600)
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
|
||||
output_text = response_data["choices"][0]["message"]["content"]
|
||||
|
||||
thinking = ""
|
||||
prompt = output_text
|
||||
|
||||
think_end_tag = "</think>"
|
||||
if think_end_tag in output_text:
|
||||
parts = output_text.split(think_end_tag, 1)
|
||||
thinking = parts[0].replace("<think>", "").strip()
|
||||
prompt = parts[1].strip()
|
||||
|
||||
return (prompt, thinking)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (f"API Connection Error: {str(e)}", "")
|
||||
except KeyError as e:
|
||||
return (f"Unexpected API Response format: {str(e)}", "")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SpectreAPIConnector": EmAySee_SpectreAPIConnector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SpectreAPIConnector": "EmAySee_Spectre API Connector"
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
class EmAySee_StringDetector:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"detected_value": ("INT", {"default": 1, "min": -1000000, "max": 1000000}),
|
||||
},
|
||||
"optional": {
|
||||
"text_input": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("int",)
|
||||
FUNCTION = "detect"
|
||||
CATEGORY = "EmAySee/Logic"
|
||||
|
||||
def detect(self, detected_value, text_input=None):
|
||||
if text_input is not None and text_input != "":
|
||||
return (detected_value,)
|
||||
return (0,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_StringDetector": EmAySee_StringDetector
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"f": "EmAySee String Detector"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
class EmAySee_SubmitToOobaboogaAPIWithKeyThinker:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
"api_url": ("STRING", {"default": "http://10.0.0.71:5000/v1/chat/completions"}),
|
||||
"api_key": ("STRING", {"default": "supersecretkey"}),
|
||||
"max_tokens": ("INT", {"default": 500, "min": 1, "max": 8192}),
|
||||
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 2.0, "step": 0.05}),
|
||||
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
"system_prompt": ("STRING", {"default": "You are a helpful assistant.", "multiline": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("clean_text", "thinking_content")
|
||||
FUNCTION = "submit_request"
|
||||
CATEGORY = "EmAySee/API"
|
||||
|
||||
def submit_request(self, text, api_url, api_key, max_tokens, temperature, top_p, seed, system_prompt="You are a helpful assistant."):
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
# Construct the payload for OpenAI-compatible API
|
||||
data = {
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": text}
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"seed": seed,
|
||||
"mode": "instruct", # Helpful for some Ooba configs
|
||||
"stream": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=data, timeout=120)
|
||||
response.raise_for_status()
|
||||
|
||||
result_json = response.json()
|
||||
|
||||
# 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 ---
|
||||
|
||||
# 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
|
||||
clean_text = re.sub(r'<think>.*?</think>', '', raw_content, flags=re.DOTALL).strip()
|
||||
|
||||
return (clean_text, thinking_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"EmAySee_SubmitToOobaboogaAPIWithKeyThinker Error: {e}")
|
||||
return (f"Error: {str(e)}", "")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SubmitToOobaboogaAPIWithKeyThinker": EmAySee_SubmitToOobaboogaAPIWithKeyThinker
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SubmitToOobaboogaAPIWithKeyThinker": "EmAySee Submit To Oobabooga API With Key With THinkParse"
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
class EmAySee_SubmitToOobaboogaAPIWithKey_test: # Class name now prefixed with EmAySee_
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"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
|
||||
"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
|
||||
),
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("api_response",)
|
||||
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
|
||||
"""
|
||||
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
|
||||
"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
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {apikey_input}' # <-- ADD THIS LINE
|
||||
}
|
||||
try:
|
||||
time.sleep(delay_seconds)
|
||||
response = requests.post(api_endpoint, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
try:
|
||||
api_response = response.json()
|
||||
except json.JSONDecodeError:
|
||||
error_message = f"Oobabooga API returned non-JSON. Status: {response.status_code}. Text: {response.text}"
|
||||
print(error_message)
|
||||
return (error_message,)
|
||||
|
||||
if show_api_response:
|
||||
print("Oobabooga API Response Content:")
|
||||
print(api_response)
|
||||
|
||||
# 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['text']
|
||||
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}"
|
||||
print(error_message)
|
||||
return (error_message,)
|
||||
|
||||
trimmed_text = api_response_text.strip()
|
||||
return (trimmed_text,)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_message = f"Oobabooga API Request Error: {e}. Status: {getattr(e.response, 'status_code', 'N/A')}. Content: {getattr(e.response, 'text', 'N/A')}"
|
||||
print(error_message)
|
||||
return (error_message,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"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
|
||||
}
|
||||
@@ -19,8 +19,8 @@ class EmAySee_SubmitToOobaboogaAPIWithKey: # Class name now prefixed with EmAyS
|
||||
"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", "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
|
||||
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1, "label": "Delay (seconds)"}),
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
class EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
"api_url": ("STRING", {"default": "http://10.0.0.71:5000/v1/chat/completions"}),
|
||||
"api_key": ("STRING", {"default": "supersecretkey"}),
|
||||
"model_name": ("STRING", {"default": "default"}),
|
||||
"max_tokens": ("INT", {"default": 500, "min": 1, "max": 8192}),
|
||||
"temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 2.0, "step": 0.05}),
|
||||
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"top_k": ("INT", {"default": 20, "min": 0, "max": 200}),
|
||||
"min_p": ("FLOAT", {"default": 0.05, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"repetition_penalty": ("FLOAT", {"default": 1.15, "min": 1.0, "max": 2.0, "step": 0.01}),
|
||||
"frequency_penalty": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"presence_penalty": ("FLOAT", {"default": 0.0, "min": -2.0, "max": 2.0, "step": 0.05}),
|
||||
"dry_multiplier": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"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
|
||||
"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}),
|
||||
},
|
||||
"optional": {
|
||||
"system_prompt": ("STRING", {"default": "You are a helpful assistant.", "multiline": True}),
|
||||
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"stop": ("STRING", {"default": "", "multiline": False, "placeholder": "comma, separated, stop, strings"}),
|
||||
"instruction_template": ("STRING", {"default": "", "placeholder": "e.g. Qwen, Alpaca, ChatML"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("clean_text", "thinking_content")
|
||||
FUNCTION = "submit_request"
|
||||
CATEGORY = "EmAySee/API"
|
||||
|
||||
def submit_request(self, text, api_url, api_key, model_name, max_tokens, temperature, top_p, top_k, min_p, repetition_penalty, frequency_penalty, presence_penalty, dry_multiplier, dry_base, dry_allowed_length, enable_thinking, reasoning_effort, guidance_scale, seed, system_prompt="You are a helpful assistant.", negative_prompt="", stop="", instruction_template=""):
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
|
||||
stop_list = [s.strip() for s in stop.split(",")] if stop and stop.strip() else []
|
||||
|
||||
data = {
|
||||
"model": model_name,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": text}
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"min_p": min_p,
|
||||
"repetition_penalty": repetition_penalty,
|
||||
"frequency_penalty": frequency_penalty,
|
||||
"presence_penalty": presence_penalty,
|
||||
"dry_multiplier": dry_multiplier,
|
||||
"dry_base": dry_base,
|
||||
"dry_allowed_length": dry_allowed_length,
|
||||
"enable_thinking": enable_thinking,
|
||||
"reasoning_effort": reasoning_effort, # Added here
|
||||
"guidance_scale": guidance_scale,
|
||||
"negative_prompt": negative_prompt,
|
||||
"instruction_template": instruction_template,
|
||||
"seed": seed,
|
||||
"mode": "instruct",
|
||||
"stream": False
|
||||
}
|
||||
|
||||
if stop_list:
|
||||
data["stop"] = stop_list
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=data, timeout=120)
|
||||
response.raise_for_status()
|
||||
|
||||
result_json = response.json()
|
||||
|
||||
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_SubmitToOobaboogaAPIWithKey: No content in response: {result_json}")
|
||||
|
||||
# 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
|
||||
clean_text = re.sub(r'<think>.*?</think>', '', raw_content, flags=re.DOTALL).strip()
|
||||
|
||||
return (clean_text, thinking_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"EmAySee_SubmitToOobaboogaAPIWithKey Error: {e}")
|
||||
return (f"Error: {str(e)}", "")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse": EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_SubmitToOobaboogaAPIWithKey_WithThinkParse": "EmAySee Submit To Oobabooga API With Key With Think Parse"
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
import re
|
||||
|
||||
class EmAySee_TagPruner:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_tags": ("STRING", {"multiline": True, "default": "tag1, tag2, bad_tag, tag3, 1girl"}),
|
||||
"tags_to_remove": ("STRING", {"multiline": True, "default": "bad_tag, 1girl, solo"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("cleaned_tags",)
|
||||
FUNCTION = "prune"
|
||||
CATEGORY = "EmAySee"
|
||||
|
||||
def prune(self, input_tags, tags_to_remove):
|
||||
remove_tags_list = re.split(r'[,\n]+', tags_to_remove)
|
||||
remove_set = set(tag.strip().lower() for tag in remove_tags_list if tag.strip())
|
||||
|
||||
input_tags_list = re.split(r'[,\n]+', input_tags)
|
||||
|
||||
cleaned_list = []
|
||||
for tag in input_tags_list:
|
||||
stripped_tag = tag.strip()
|
||||
if stripped_tag and stripped_tag.lower() not in remove_set:
|
||||
cleaned_list.append(stripped_tag)
|
||||
|
||||
final_string = ", ".join(cleaned_list)
|
||||
|
||||
return (final_string,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_TagPruner": EmAySee_TagPruner
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_TagPruner": "EmAySee Tag Pruner"
|
||||
}
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
class EmAySee_TextCombiner:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
inputs = {
|
||||
"required": {},
|
||||
"optional": {}
|
||||
}
|
||||
for i in range(1, 51):
|
||||
inputs["optional"][f"text_{i}"] = ("STRING", {"forceInput": True})
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "combine"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def combine(self, **kwargs):
|
||||
text_list = []
|
||||
for i in range(1, 51):
|
||||
key = f"text_{i}"
|
||||
if key in kwargs and kwargs[key] is not None and kwargs[key] != "":
|
||||
text_list.append(str(kwargs[key]))
|
||||
|
||||
return ("\n".join(text_list),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_TextCombiner": EmAySee_TextCombiner
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_TextCombiner": "EmAySee Text Combiner"
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
import re
|
||||
|
||||
class EmAySee_TwentyFloatToText:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"Breasts Size", "Breasts Sag", "Areolas Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Gap", "Body Type", "Pregnant", "Peeking Pubic Hairs"
|
||||
]
|
||||
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("FLOAT", {"default": 0.0, "min": -3.0, "max": 5.0, "step": 0.1})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("combined_text",)
|
||||
FUNCTION = "convert"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def convert(self, **kwargs):
|
||||
labels = [
|
||||
"Breasts Size", "Breasts Sag", "Areolas Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Gap", "Body Type", "Pregnant", "Peeking Pubic Hairs"
|
||||
]
|
||||
|
||||
combined_list = []
|
||||
for label in labels:
|
||||
value = kwargs.get(label, 0.0)
|
||||
if value != 0.0:
|
||||
combined_list.append(f"{label}: {value:.2f}")
|
||||
|
||||
return ("\n".join(combined_list),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToText": EmAySee_TwentyFloatToText
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToText": "EmAySee Twenty Float To Text"
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
import re
|
||||
|
||||
class EmAySee_TwentyFloatToTextAndFloat:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"Breasts Size", "Breasts Sag", "Areloas Size", "Puffie",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05",
|
||||
"Option 06", "Option 07", "Option 08"
|
||||
]
|
||||
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("FLOAT", {"default": 0.0, "min": -2.0, "max": 3.0, "step": 0.01})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",) + ("FLOAT",) * 20
|
||||
RETURN_NAMES = ("combined_text",) + (
|
||||
"Breasts Size", "Breasts Sag", "Areloas Size", "Puffie",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05",
|
||||
"Option 06", "Option 07", "Option 08"
|
||||
)
|
||||
FUNCTION = "convert"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def convert(self, **kwargs):
|
||||
labels = [
|
||||
"Breasts Size", "Breasts Sag", "Areloas Size", "Puffie",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "Pubic Hair Peek",
|
||||
"Option 01", "Option 02", "Option 03", "Option 04", "Option 05",
|
||||
"Option 06", "Option 07", "Option 08"
|
||||
]
|
||||
|
||||
combined_list = []
|
||||
float_outputs = []
|
||||
|
||||
for label in labels:
|
||||
value = kwargs.get(label, 0.0)
|
||||
float_outputs.append(value)
|
||||
if value != 0.0:
|
||||
combined_list.append(f"{label}: {value:.2f}")
|
||||
|
||||
return ("\n".join(combined_list),) + tuple(float_outputs)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToTextAndFloat": EmAySee_TwentyFloatToTextAndFloat
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToTextAndFloat": "EmAySee Twenty Float To Text And Float"
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
import torch
|
||||
|
||||
class EmAySee_TwentyFloatToTextV2:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
labels = [
|
||||
"Breast Size", "Breast Sag", "Areola Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "PHP",
|
||||
"Extra 01", "Extra 02", "Extra 03", "Extra 04", "Extra 05",
|
||||
"Extra 06", "Extra 07", "Extra 08"
|
||||
]
|
||||
|
||||
inputs = {"required": {}}
|
||||
for label in labels:
|
||||
inputs["required"][label] = ("FLOAT", {"default": 0.0, "min": -2.0, "max": 3.0, "step": 0.01})
|
||||
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",) + ("STRING",) * 20 + ("FLOAT",) * 20
|
||||
RETURN_NAMES = ("combined_text",) + (
|
||||
"T_Breast Size", "T_Breast Sag", "T_Areola Size", "T_Puffies",
|
||||
"T_Belly Fat", "T_Waist Size", "T_Butt Size", "T_Thigh Size",
|
||||
"T_Thigh Spacing", "T_Body Type", "T_Pregnant", "T_PHP",
|
||||
"T_Extra 01", "T_Extra 02", "T_Extra 03", "T_Extra 04", "T_Extra 05",
|
||||
"T_Extra 06", "T_Extra 07", "T_Extra 08",
|
||||
"F_Breast Size", "F_Breast Sag", "F_Areola Size", "F_Puffies",
|
||||
"F_Belly Fat", "F_Waist Size", "F_Butt Size", "F_Thigh Size",
|
||||
"F_Thigh Spacing", "F_Body Type", "F_Pregnant", "F_PHP",
|
||||
"F_Extra 01", "F_Extra 02", "F_Extra 03", "F_Extra 04", "F_Extra 05",
|
||||
"F_Extra 06", "F_Extra 07", "F_Extra 08"
|
||||
)
|
||||
FUNCTION = "convert"
|
||||
CATEGORY = "EmAySee/Text"
|
||||
|
||||
def convert(self, **kwargs):
|
||||
labels = [
|
||||
"Breast Size", "Breast Sag", "Areola Size", "Puffies",
|
||||
"Belly Fat", "Waist Size", "Butt Size", "Thigh Size",
|
||||
"Thigh Spacing", "Body Type", "Pregnant", "PHP",
|
||||
"Extra 01", "Extra 02", "Extra 03", "Extra 04", "Extra 05",
|
||||
"Extra 06", "Extra 07", "Extra 08"
|
||||
]
|
||||
|
||||
combined_list = []
|
||||
individual_texts = []
|
||||
float_outputs = []
|
||||
|
||||
for label in labels:
|
||||
value = kwargs.get(label, 0.0)
|
||||
float_outputs.append(value)
|
||||
|
||||
if value != 0.0:
|
||||
formatted = f"{label}: {value:.2f}"
|
||||
combined_list.append(formatted)
|
||||
individual_texts.append(formatted)
|
||||
else:
|
||||
individual_texts.append("")
|
||||
|
||||
return ("\n".join(combined_list),) + tuple(individual_texts) + tuple(float_outputs)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToTextV2": EmAySee_TwentyFloatToTextV2
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_TwentyFloatToTextV2": "EmAySee Twenty Float To Text V2"
|
||||
}
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
import comfy.utils
|
||||
import comfy.sd
|
||||
import folder_paths
|
||||
|
||||
lora_names = ["None"] + folder_paths.get_filename_list("loras")
|
||||
|
||||
class EmAySee_LoraNameSelector:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"lora_name": (lora_names,),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("lora_name",)
|
||||
FUNCTION = "get_name"
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
|
||||
def get_name(self, lora_name):
|
||||
return (lora_name,)
|
||||
|
||||
class EmAySee_LoraApplier:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"clip": ("CLIP",),
|
||||
"lora_name": ("STRING", {"default": "None", "multiline": False}),
|
||||
"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}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP")
|
||||
FUNCTION = "apply_lora"
|
||||
CATEGORY = "EmAySee/Loaders"
|
||||
|
||||
def apply_lora(self, model, clip, lora_name, strength_model, strength_clip):
|
||||
if strength_model == 0 and strength_clip == 0 or lora_name is None or lora_name == "None":
|
||||
return (model, clip)
|
||||
|
||||
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||
|
||||
if not lora_path:
|
||||
print(f"EmAySee LoRA Applier: Warning, LoRA not found: {lora_name}")
|
||||
return (model, clip)
|
||||
|
||||
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)
|
||||
|
||||
return (model_lora, clip_lora)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EmAySee_LoraNameSelector": EmAySee_LoraNameSelector,
|
||||
"EmAySee_LoraApplier": EmAySee_LoraApplier,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EmAySee_LoraNameSelector": "EmAySee LoRA Name Selector",
|
||||
"EmAySee_LoraApplier": "EmAySee LoRA Applier",
|
||||
}
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
import json
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
class EmAySee_VisualMachine_SaveImageAndText_V2:
|
||||
def __init__(self):
|
||||
self.output_dir = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "SPECTRE"}),
|
||||
"history_text": ("STRING", {"forceInput": True}),
|
||||
"current_prompt": ("STRING", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "save_images"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "EmAySee_Automation/Image"
|
||||
|
||||
def save_images(self, images, filename_prefix, history_text, current_prompt):
|
||||
from nodes import get_comfy_root
|
||||
|
||||
full_output_folder = os.path.join(get_comfy_root(), self.output_dir)
|
||||
if not os.path.exists(full_output_folder):
|
||||
os.makedirs(full_output_folder, mode=0o777, exist_ok=True)
|
||||
|
||||
results = list()
|
||||
for image in images:
|
||||
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
|
||||
file_base = f"{filename_prefix}"
|
||||
image_file = f"{file_base}.png"
|
||||
log_file = f"{file_base}.txt"
|
||||
|
||||
image_path = os.path.join(full_output_folder, image_file)
|
||||
log_path = os.path.join(full_output_folder, log_file)
|
||||
|
||||
# 2. Save the Image
|
||||
img.save(image_path, pnginfo=None, compress_level=4)
|
||||
|
||||
# 3. Save the Companion Text File
|
||||
entry = (
|
||||
f"--- SPECTRE V2 METADATA ---\n"
|
||||
f"FILENAME: {image_file}\n"
|
||||
f"METADATA_FILE: {log_file}\n"
|
||||
f"PROMPT: {current_prompt}\n"
|
||||
f"STATE HISTORY:\n{history_text}\n"
|
||||
f"----------------------------"
|
||||
)
|
||||
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write(entry)
|
||||
|
||||
return {"ui": {"images": [{"filename": image_file, "subfolder": "", "type": "output"}]}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"EmAySee_VisualMachine_SaveImageAndText_V2": EmAySee_VisualMachine_SaveImageAndText_V2}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"EmAySee_VisualMachine_SaveImageAndText_V2": "EmAySee VisualMachine Save Image And Text V2"}
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
import node_helpers
|
||||
import comfy.utils
|
||||
import math
|
||||
from typing_extensions import override
|
||||
from comfy_api.latest import ComfyExtension, io
|
||||
|
||||
class TextEncodeQwenImageEdit_V2(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="TextEncodeQwenImageEdit_V2",
|
||||
category="advanced/conditioning",
|
||||
inputs=[
|
||||
io.Clip.Input("clip"),
|
||||
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
|
||||
io.Vae.Input("vae", optional=True),
|
||||
io.Image.Input("image", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Conditioning.Output(),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, clip, prompt, vae=None, image=None) -> io.NodeOutput:
|
||||
ref_latent = None
|
||||
if image is None:
|
||||
images = []
|
||||
else:
|
||||
samples = image.movedim(-1, 1)
|
||||
total = int(1024 * 1024)
|
||||
|
||||
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
|
||||
width = round(samples.shape[3] * scale_by)
|
||||
height = round(samples.shape[2] * scale_by)
|
||||
|
||||
s = comfy.utils.common_upscale(samples, width, height, "area", "disabled")
|
||||
image = s.movedim(1, -1)
|
||||
images = [image[:, :, :, :3]]
|
||||
if vae is not None:
|
||||
ref_latent = vae.encode(image[:, :, :, :3])
|
||||
|
||||
tokens = clip.tokenize(prompt, images=images)
|
||||
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
||||
if ref_latent is not None:
|
||||
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": [ref_latent]}, append=True)
|
||||
return io.NodeOutput(conditioning)
|
||||
|
||||
|
||||
class TextEncodeQwenImageEdit_V2Plus(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="TextEncodeQwenImageEdit_V2Plus",
|
||||
category="advanced/conditioning",
|
||||
inputs=[
|
||||
io.Clip.Input("clip"),
|
||||
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
|
||||
io.Vae.Input("vae", optional=True),
|
||||
io.Image.Input("image1", optional=True),
|
||||
io.Image.Input("image2", optional=True),
|
||||
io.Image.Input("image3", optional=True),
|
||||
io.Image.Input("image4", optional=True),
|
||||
io.Image.Input("image5", optional=True),
|
||||
io.Image.Input("image6", optional=True),
|
||||
io.Image.Input("image7", optional=True),
|
||||
io.Latent.Input("target_latent", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Conditioning.Output(),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None, image4=None, target_latent=None) -> io.NodeOutput:
|
||||
ref_latents = []
|
||||
images = [image1, image2, image3, image4]
|
||||
images_vl = []
|
||||
llama_template = "<|im_start|>system\nDescribe key details of the input image (including any objects, characters, poses, facial features, clothing, setting, textures and style), then explain how the user's text instruction should alter, modify or recreate the image. Generate a new image that meets the user's requirements, which can vary from a small change to a completely new image using inputs as a guide.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
|
||||
image_prompt = ""
|
||||
|
||||
for i, image in enumerate(images):
|
||||
if image is not None:
|
||||
samples = image.movedim(-1, 1)
|
||||
total = int(384 * 384)
|
||||
|
||||
scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2]))
|
||||
width = round(samples.shape[3] * scale_by)
|
||||
height = round(samples.shape[2] * scale_by)
|
||||
|
||||
s = comfy.utils.common_upscale(samples, width, height, "lanczos", "center")
|
||||
images_vl.append(s.movedim(1, -1))
|
||||
if vae is not None:
|
||||
if target_latent is not None:
|
||||
twidth = target_latent["samples"].shape[-1] * 8
|
||||
theight = target_latent["samples"].shape[-2] * 8
|
||||
s = comfy.utils.common_upscale(samples, twidth, theight, "lanczos", "center")
|
||||
else:
|
||||
s = samples
|
||||
ref_latents.append(vae.encode(s.movedim(1, -1)[:, :, :, :3]))
|
||||
|
||||
image_prompt += "Picture {}: <|vision_start|><|image_pad|><|vision_end|>".format(i + 1)
|
||||
|
||||
tokens = clip.tokenize(image_prompt + prompt, images=images_vl, llama_template=llama_template)
|
||||
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
||||
if len(ref_latents) > 0:
|
||||
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": ref_latents}, append=True)
|
||||
return io.NodeOutput(conditioning)
|
||||
|
||||
class QwenExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||
return [
|
||||
TextEncodeQwenImageEdit_V2,
|
||||
TextEncodeQwenImageEdit_V2Plus,
|
||||
]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> QwenExtension:
|
||||
return QwenExtension()
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[project]
|
||||
name = "ComfyUI_EmAySee_CustomNodes"
|
||||
description = "**FIXED DL** Collection of nodes i've made to better my life in comfy. There is an LLM Ooobagabooga interface, text file saver, multiple randominzers for integers and string selections, and many other nodes I created. CheckboxFloatNode, DateTimeStringNode, DynamicStringSelectorNode, HostPinger, IntegerStringSelectorNode, IntegerStringSelectorNodeDynamic, multiplier_node, ProbabilityStringSelectorNode, RandomIntegerFromListNode, RandomIntegerFromTogglesNode_PremadeLabels, RandomIntFromList, RandomIntFromList2, RandomStringSelectorNode, RandomStringSelectorNode2, RandomStringSelectorNodeFourChoice, RandomStringSelectorNodeThreeChoice, RemoveDuplicateCSV, RepaintKSampler, SaveImage, SaveTextToFile, StringPoseSelectorNode, StringTupleInputNode, SubmitToOobaboogaAPI, ToggleIntNode, VarTextReplacer, VeryUniqueStringSelectorNode"
|
||||
description = "A specialized suite of over 60 custom nodes for ComfyUI. Includes advanced image processing (Qwen Pixel Aligner/Optimizer), VRAM management tools, masking logic, math expression evaluators, and high-performance LLM connectors with Auto-Continue. Amongst other fun, useful, and useless nodes TBH."
|
||||
|
||||
version = "3.2.0"
|
||||
version = "4.1.0"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
Reference in New Issue
Block a user