+7
-4
@@ -1,8 +1,9 @@
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import folder_paths
|
||||
from .src.dynamic_lora_loader import generate_lora_node_class
|
||||
from .src.dynamic_scheduler_loader import generate_scheduler_node_class
|
||||
from .src.check_folder_paths import check_lora_folders
|
||||
from .sn0w import ConfigReader, Logger
|
||||
|
||||
from .src.lora_selector import LoraSelectorNode
|
||||
@@ -15,7 +16,7 @@ from .src.prompt_selector import PromptSelectNode
|
||||
from .src.upscale_with_model_by import UpscaleImageBy
|
||||
from .src.load_lora_from_folder import LoadLoraFolderNode
|
||||
from .src.textbox import TextboxNode
|
||||
from .src.simple_sampler_custom import SimpleSamplerCustom
|
||||
from .src.simple_ksampler import SimpleSamplerCustom
|
||||
from .src.filter_tags import FilterTags
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -30,7 +31,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Upscale Image With Model By": UpscaleImageBy,
|
||||
"Load Lora Folder": LoadLoraFolderNode,
|
||||
"Copy/Paste Textbox": TextboxNode,
|
||||
"Simple Sampler Custom": SimpleSamplerCustom,
|
||||
"Sn0w KSampler": SimpleSamplerCustom,
|
||||
"Filter Tags": FilterTags,
|
||||
}
|
||||
|
||||
@@ -46,12 +47,14 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Upscale Image With Model By": "Upscale Image With Model By",
|
||||
"Load Lora Folder": "Load Lora Folder",
|
||||
"Copy/Paste Textbox": "Textbox",
|
||||
"Simple Sampler Custom": "Simple Sampler Custom",
|
||||
"Sn0w KSampler": "Sn0w KSampler",
|
||||
"Filter Tags": "Filter Tags",
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
check_lora_folders()
|
||||
|
||||
current_unique_id = 0 # Global variable to track the unique ID
|
||||
logger = Logger()
|
||||
|
||||
|
||||
@@ -175,6 +175,19 @@ class Utility:
|
||||
def get_model_type(model_patcher):
|
||||
return model_patcher.model.__class__.__name__
|
||||
|
||||
@staticmethod
|
||||
def get_model_type_simple(model_patcher):
|
||||
model_type = Utility.get_model_type(model_patcher)
|
||||
|
||||
if model_type == "BaseModel":
|
||||
return "SD15"
|
||||
elif model_type == "SDXL":
|
||||
return "SDXL"
|
||||
elif model_type == "SD3":
|
||||
return "SD3"
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def put_favourite_on_top(cls, setting, arr):
|
||||
# Convert to a list if its a dictionary
|
||||
@@ -199,29 +212,80 @@ class Utility:
|
||||
prioritized.extend(arr)
|
||||
return prioritized
|
||||
|
||||
@classmethod
|
||||
def create_setting_entry(cls, setting_type, setting_value):
|
||||
if setting_type == "INT":
|
||||
return ("INT", {"default": setting_value[1], "min": setting_value[2], "max": setting_value[3]})
|
||||
elif setting_type == "FLOAT":
|
||||
return ("FLOAT", {"default": setting_value[1], "min": setting_value[2], "max": setting_value[3], "step": setting_value[4]})
|
||||
elif setting_type == "STRING":
|
||||
return ("STRING", {"default": setting_value[1]})
|
||||
elif setting_type == "BOOLEAN":
|
||||
return ("BOOLEAN", {"default": setting_value[1]})
|
||||
else:
|
||||
raise ValueError(f"Unsupported setting type: {setting_type}")
|
||||
|
||||
@classmethod
|
||||
def get_node_output(cls, data, node_id, output_id):
|
||||
workflow = data.get('workflow', {})
|
||||
nodes = workflow.get('nodes', [])
|
||||
|
||||
for node in nodes:
|
||||
if int(node.get("id")) == int(node_id):
|
||||
for output in node.get('outputs', []):
|
||||
if int(output['slot_index']) == int(output_id):
|
||||
return output
|
||||
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
class Cancelled(Exception):
|
||||
pass
|
||||
|
||||
class MessageHolder:
|
||||
stash = {}
|
||||
messages = {}
|
||||
|
||||
@classmethod
|
||||
def addMessage(self, id, message):
|
||||
self.messages[str(id)] = message
|
||||
|
||||
@classmethod
|
||||
def waitForMessage(self, id, period = 0.1):
|
||||
sid = str(id)
|
||||
|
||||
while not (sid in self.messages):
|
||||
time.sleep(period)
|
||||
|
||||
message = self.messages.pop(str(id),None)
|
||||
return message
|
||||
cancelled = False
|
||||
routes = PromptServer.instance.routes
|
||||
API_PREFIX = '/api/sn0w'
|
||||
logger = Logger()
|
||||
|
||||
routes = PromptServer.instance.routes
|
||||
API_PREFIX = '/sn0w'
|
||||
@classmethod
|
||||
def addMessage(cls, id, message):
|
||||
if message=='__cancel__':
|
||||
cls.messages = {}
|
||||
cls.cancelled = True
|
||||
elif message=='__start__':
|
||||
cls.messages = {}
|
||||
cls.stash = {}
|
||||
cls.cancelled = False
|
||||
else:
|
||||
cls.messages[str(id)] = message
|
||||
|
||||
@classmethod
|
||||
def waitForMessage(cls, id, period = 0.1, asList = False):
|
||||
sid = str(id)
|
||||
while not (sid in cls.messages) and not ("-1" in cls.messages):
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise Cancelled()
|
||||
time.sleep(period)
|
||||
if cls.cancelled:
|
||||
cls.cancelled = False
|
||||
raise Cancelled()
|
||||
message = cls.messages.pop(str(id),None) or cls.messages.pop("-1")
|
||||
try:
|
||||
if asList:
|
||||
return [int(x.strip()) for x in message.split(",")]
|
||||
else:
|
||||
return int(message.strip())
|
||||
except ValueError:
|
||||
cls.logger.log(f"failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}", "ERROR")
|
||||
return [1] if asList else 1
|
||||
|
||||
routes = MessageHolder.routes
|
||||
API_PREFIX = MessageHolder.API_PREFIX
|
||||
|
||||
@routes.post(f'{API_PREFIX}/textbox_string')
|
||||
async def handle_textbox_string(request):
|
||||
@@ -233,21 +297,4 @@ async def handle_textbox_string(request):
|
||||
async def handle_update_sorting(request):
|
||||
Logger.reload_config()
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
@routes.post(f'{API_PREFIX}/widget_values')
|
||||
async def handle_widget_values(request):
|
||||
data = await request.json()
|
||||
MessageHolder.addMessage(data.get("node_id"), data.get("outputs"))
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
@routes.post(f'{API_PREFIX}/should_decode_image')
|
||||
async def handle_should_decode_image(request):
|
||||
data = await request.json()
|
||||
MessageHolder.addMessage(data.get("node_id"), data.get("outputs"))
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
@routes.post(f'{API_PREFIX}/get_sigmas')
|
||||
async def handle_get_sigmas(request):
|
||||
data = await request.json()
|
||||
MessageHolder.addMessage(data.get("node_id"), data.get("outputs"))
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
from ..sn0w import ConfigReader, Logger, AnyType, Utility
|
||||
from ..sn0w import ConfigReader, Logger, AnyType, Utility, MessageHolder
|
||||
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
|
||||
any = AnyType("*")
|
||||
|
||||
routes = PromptServer.instance.routes
|
||||
API_PREFIX = '/sn0w'
|
||||
routes = MessageHolder.routes
|
||||
API_PREFIX = MessageHolder.API_PREFIX
|
||||
|
||||
@routes.post(f'{API_PREFIX}/update_characters')
|
||||
async def handle_update_characters(request):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import folder_paths
|
||||
import json
|
||||
import os
|
||||
|
||||
from ..sn0w import Logger
|
||||
|
||||
def check_lora_folders():
|
||||
logger = Logger()
|
||||
custom_paths = ["loras_15", "loras_xl", "loras_3"]
|
||||
existing_paths = []
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
if os.path.basename(dir_path) == "src":
|
||||
dir_path = os.path.dirname(dir_path)
|
||||
json_path = os.path.join(dir_path, 'web/settings/sn0w_settings.json')
|
||||
# Ensure the directory exists
|
||||
os.makedirs(os.path.dirname(json_path), exist_ok=True)
|
||||
|
||||
for path in custom_paths:
|
||||
try:
|
||||
lora_path = folder_paths.get_filename_list(path)
|
||||
if lora_path:
|
||||
existing_paths.append(path)
|
||||
except Exception as e:
|
||||
logger.log(f"{path} doesn't exist: {e}", "WARNING")
|
||||
|
||||
# Write the result to a JSON file in /web/settings
|
||||
data = {"loaders_enabled": existing_paths}
|
||||
with open(json_path, "w") as json_file:
|
||||
json.dump(data, json_file, indent=4)
|
||||
|
||||
return existing_paths
|
||||
@@ -1,20 +1,9 @@
|
||||
from .custom_schedulers.custom_schedulers import CustomSchedulers
|
||||
from ..sn0w import Utility
|
||||
|
||||
def generate_scheduler_node_class(settings, get_sigmas_function):
|
||||
class DynamicSchedulerNode:
|
||||
|
||||
@classmethod
|
||||
def create_setting_entry(cls, setting_type, setting_value):
|
||||
if setting_type == "INT":
|
||||
return ("INT", {"default": setting_value[1], "min": setting_value[2], "max": setting_value[3]})
|
||||
elif setting_type == "FLOAT":
|
||||
return ("FLOAT", {"default": setting_value[1], "min": setting_value[2], "max": setting_value[3], "step": setting_value[4]})
|
||||
elif setting_type == "STRING":
|
||||
return ("STRING", {"default": setting_value[1]})
|
||||
elif setting_type == "BOOLEAN":
|
||||
return ("BOOLEAN", {"default": setting_value[1]})
|
||||
else:
|
||||
raise ValueError(f"Unsupported setting type: {setting_type}")
|
||||
utility = Utility()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -22,7 +11,7 @@ def generate_scheduler_node_class(settings, get_sigmas_function):
|
||||
"required": {
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
**{
|
||||
key: cls.create_setting_entry(value[0], value)
|
||||
key: cls.utility.create_setting_entry(value[0], value)
|
||||
for key, value in settings['settings'].items()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +64,13 @@ class LoadLoraFolderNode:
|
||||
|
||||
# Clean and split the prompt into parts
|
||||
prompt_parts = [self.clean_string(part) for part in prompt.split(separator)]
|
||||
model_type = Utility.get_model_type(model)
|
||||
model_type = Utility.get_model_type_simple(model)
|
||||
|
||||
# Retrieve and filter Lora file paths
|
||||
full_lora_paths = folder_paths.get_filename_list("loras")
|
||||
|
||||
try:
|
||||
if model_type == "BaseModel":
|
||||
if model_type == "SD15":
|
||||
filtered_lora_paths = folder_paths.get_filename_list("loras_15")
|
||||
elif model_type == "SDXL":
|
||||
filtered_lora_paths = folder_paths.get_filename_list("loras_xl")
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from nodes import VAEDecode, EmptyLatentImage, CLIPTextEncode
|
||||
from comfy_extras.nodes_custom_sampler import SamplerCustom, BasicScheduler, PolyexponentialScheduler, VPScheduler, SplitSigmasDenoise
|
||||
from comfy_extras.nodes_align_your_steps import AlignYourStepsScheduler
|
||||
from server import PromptServer
|
||||
from ..sn0w import Logger, MessageHolder, Utility
|
||||
from ..sn0w import Logger, Utility
|
||||
from .custom_schedulers.custom_schedulers import CustomSchedulers
|
||||
from .show_sigmas import ShowSigmasNode
|
||||
import comfy.samplers
|
||||
|
||||
class SimpleSamplerCustom:
|
||||
@@ -21,6 +19,7 @@ class SimpleSamplerCustom:
|
||||
cls.logger.log(custom_scheduler_names, "DEBUG")
|
||||
cls.scheduler_list = comfy.samplers.KSampler.SCHEDULERS + ["align_your_steps", "polyexponential", "vp"] + custom_scheduler_names
|
||||
cls.scheduler_settings = {name: settings for name, settings in cls.custom_schedulers.get_scheduler_settings().items()}
|
||||
cls.custom_scheduler_defaults = cls.custom_schedulers.get_scheduler_defaults()
|
||||
|
||||
@classmethod
|
||||
def get_custom_class_sigmas(cls, scheduler_name, *args, **kwargs):
|
||||
@@ -39,151 +38,87 @@ class SimpleSamplerCustom:
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
"scheduler_name": (cls.scheduler_list, ),
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.01, "round": 0.01}),
|
||||
"width": ("INT", {"default": 0, "min": 0, "step":64}),
|
||||
"height": ("INT", {"default": 0, "min": 0, "step":64}),
|
||||
},
|
||||
"optional": {
|
||||
"positive": ("*"),
|
||||
"negative": ("*"),
|
||||
"scheduler_name": (cls.scheduler_list, ),
|
||||
"sigmas (optional)": ("SIGMAS", ),
|
||||
"latent (optional)": ("LATENT", ),
|
||||
"width": ("INT", {"default": 0, "min": 0, "step":64}),
|
||||
"height": ("INT", {"default": 0, "min": 0, "step":64}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
"hidden" : {
|
||||
"extra_info": "EXTRA_PNGINFO",
|
||||
"id": "UNIQUE_ID",
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "LATENT", "CONDITIONING", "CONDITIONING", "IMAGE")
|
||||
RETURN_NAMES = ("IMAGE", "LATENT", "POSITIVE", "NEGATIVE", "SIGMAS GRAPH")
|
||||
RETURN_TYPES = ("IMAGE", "LATENT", "CONDITIONING", "CONDITIONING",)
|
||||
RETURN_NAMES = ("IMAGE", "LATENT", "POSITIVE", "NEGATIVE",)
|
||||
|
||||
FUNCTION = "sample"
|
||||
|
||||
CATEGORY = "sampling/custom_sampling"
|
||||
|
||||
def sample(self, model, clip, vae, add_noise, noise_seed, steps, cfg, sampler_name, denoise, **kwargs):
|
||||
def sample(self, model, clip, vae, add_noise, noise_seed, steps, cfg, sampler_name, scheduler_name, denoise, width, height, **kwargs):
|
||||
custom_sampler = SamplerCustom()
|
||||
vae_decode = VAEDecode()
|
||||
text_encode = CLIPTextEncode()
|
||||
|
||||
# Encode inputs
|
||||
positive_prompt = self.get_prompt("positive", text_encode, clip, kwargs)
|
||||
negative_prompt = self.get_prompt("negative", text_encode, clip, kwargs)
|
||||
|
||||
model_type = Utility.get_model_type(model)
|
||||
self.logger.log(f"{model_type}", "DEBUG")
|
||||
|
||||
# Create latent
|
||||
if 'latent (optional)' in kwargs and kwargs['latent (optional)'] is not None:
|
||||
latent_image = kwargs['latent (optional)']
|
||||
else:
|
||||
if 'width' in kwargs and 'height' in kwargs:
|
||||
latent_image = EmptyLatentImage().generate(kwargs['width'], kwargs['height'])[0]
|
||||
else:
|
||||
raise ValueError(f"If no latent is provided width and height is needed.")
|
||||
model_type = Utility.get_model_type_simple(model)
|
||||
image_output = Utility.get_node_output(kwargs['extra_info'], kwargs['id'], 0)
|
||||
latent_image = EmptyLatentImage().generate(width, height)[0]
|
||||
|
||||
# Get sampler and sigmas
|
||||
sampler = self.get_sampler(sampler_name)[0]
|
||||
if 'sigmas (optional)' in kwargs and kwargs['sigmas (optional)'] is not None:
|
||||
sigmas = kwargs['sigmas (optional)']
|
||||
else:
|
||||
sigmas = self.get_custom_sigmas(model, model_type, kwargs["scheduler_name"], steps, denoise, kwargs["unique_id"])
|
||||
sigmas = self.get_custom_sigmas(model, model_type, scheduler_name, steps, denoise)
|
||||
|
||||
# Generate and decode image
|
||||
samples = custom_sampler.sample(model, add_noise, noise_seed, cfg, positive_prompt, negative_prompt, sampler, sigmas, latent_image)
|
||||
if (self.should_decode_image(kwargs["unique_id"])):
|
||||
if (image_output["links"] != []):
|
||||
image = vae_decode.decode(vae, samples[1])
|
||||
else:
|
||||
image = (None, )
|
||||
|
||||
sigmas_list = ShowSigmasNode.sigmas_to_list(self, sigmas)
|
||||
|
||||
PromptServer.instance.send_sync("sampler_get_sigmas", {
|
||||
"id": kwargs["unique_id"],
|
||||
"sigmas": sigmas_list,
|
||||
})
|
||||
outputs = MessageHolder.waitForMessage(kwargs["unique_id"])
|
||||
|
||||
final_tensor = ShowSigmasNode.image_to_tensor(self, outputs)
|
||||
|
||||
return (image[0], samples[1], positive_prompt, negative_prompt, final_tensor,)
|
||||
|
||||
def get_sampler(self, sampler_name):
|
||||
sampler = comfy.samplers.sampler_object(sampler_name)
|
||||
return (sampler, )
|
||||
|
||||
def get_scheduler_values(self, unique_id, widgets_needed):
|
||||
PromptServer.instance.send_sync("get_widget_values", {
|
||||
"id": unique_id,
|
||||
"widgets_needed": widgets_needed,
|
||||
})
|
||||
outputs = MessageHolder.waitForMessage(unique_id)
|
||||
return outputs
|
||||
return (image[0], samples[1], positive_prompt, negative_prompt,)
|
||||
|
||||
def get_denoised_sigmas(self, sigmas, denoise):
|
||||
if denoise == 1:
|
||||
return (None, sigmas, )
|
||||
|
||||
return SplitSigmasDenoise.get_sigmas(self, sigmas, denoise)
|
||||
|
||||
def get_custom_sigmas(self, model, model_type, scheduler_name, steps, denoise, unique_id):
|
||||
|
||||
def get_custom_sigmas(self, model, model_type, scheduler_name, steps, denoise):
|
||||
if denoise > 0 and scheduler_name not in comfy.samplers.KSampler.SCHEDULERS:
|
||||
steps = int(steps / denoise)
|
||||
|
||||
|
||||
if scheduler_name == "align_your_steps":
|
||||
if (model_type == "BaseModel"):
|
||||
model_type = "SD1"
|
||||
elif (model_type == "SD3"): # Temporary to make AYS work with sd3 models
|
||||
if (model_type == "SD3"): # Temporary to make AYS work with sd3 models
|
||||
model_type = "SDXL"
|
||||
|
||||
sigmas = AlignYourStepsScheduler.get_sigmas(self, model_type, steps, denoise)[0]
|
||||
elif scheduler_name == "polyexponential":
|
||||
values = self.get_scheduler_values(unique_id, ["sigma_max_poly", "sigma_min_poly", "rho"])
|
||||
|
||||
sigma_max = values["sigma_max_poly"]["value"]
|
||||
sigma_min = values["sigma_min_poly"]["value"]
|
||||
rho = values["rho"]["value"]
|
||||
self.logger.log(f"Sigma Max: {sigma_max}, Sigma Min: {sigma_min}, Rho: {rho}", "DEBUG")
|
||||
|
||||
sigmas = self.get_denoised_sigmas(PolyexponentialScheduler.get_sigmas(self, steps, sigma_max, sigma_min, rho)[0], denoise)[1]
|
||||
|
||||
sigmas = self.get_denoised_sigmas(PolyexponentialScheduler.get_sigmas(self, steps, 14.614642, 0.0291675, 1.0)[0], denoise)[1]
|
||||
elif scheduler_name == "vp":
|
||||
values = self.get_scheduler_values(unique_id, ["beta_d", "beta_min", "eps_s"])
|
||||
|
||||
beta_d = values["beta_d"]["value"]
|
||||
beta_min = values["beta_min"]["value"]
|
||||
eps_s = values["eps_s"]["value"]
|
||||
self.logger.log(f"Beta D: {beta_d}, Beta Min: {beta_min}, Eps S: {eps_s}", "DEBUG")
|
||||
|
||||
sigmas = self.get_denoised_sigmas(VPScheduler.get_sigmas(self, steps, beta_d, beta_min, eps_s)[0], denoise)[1]
|
||||
sigmas = self.get_denoised_sigmas(VPScheduler.get_sigmas(self, steps, 14.0, 0.05, 0.075)[0], denoise)[1]
|
||||
elif scheduler_name in comfy.samplers.KSampler.SCHEDULERS:
|
||||
sigmas = BasicScheduler.get_sigmas(self, model, scheduler_name, steps, denoise)[0]
|
||||
else:
|
||||
scheduler_settings = self.scheduler_settings.get(scheduler_name)
|
||||
if not scheduler_settings:
|
||||
raise ValueError(f"No settings found for scheduler '{scheduler_name}'")
|
||||
defaults = self.get_custom_scheduler_defaults(scheduler_name)
|
||||
sigmas = self.get_denoised_sigmas(self.get_custom_class_sigmas(scheduler_name, steps, *defaults.values())[0], denoise)[1]
|
||||
|
||||
# Fetch settings from the node API
|
||||
values = self.get_scheduler_values(unique_id, scheduler_settings)
|
||||
|
||||
# Check for missing settings keys in values
|
||||
if not all(key in values for key in scheduler_settings):
|
||||
missing_keys = [key for key in scheduler_settings if key not in values]
|
||||
self.logger.log(f"Missing keys in 'values': {missing_keys}", "WARNING")
|
||||
|
||||
# Map the settings to their actual values using the names from the scheduler settings
|
||||
mapped_settings = [values.get(setting_name, {}).get("value", None) for setting_name in scheduler_settings]
|
||||
|
||||
# Log the fetched settings
|
||||
self.logger.log(f"Fetched settings for {scheduler_name}: {mapped_settings}", "DEBUG")
|
||||
|
||||
# Get sigmas using the custom scheduler
|
||||
sigmas = self.get_denoised_sigmas(self.get_custom_class_sigmas(scheduler_name, steps, *mapped_settings)[0], denoise)[1]
|
||||
|
||||
self.logger.print_sigmas_differences(scheduler_name, sigmas)
|
||||
return sigmas
|
||||
|
||||
def get_sampler(self, sampler_name):
|
||||
sampler = comfy.samplers.sampler_object(sampler_name)
|
||||
return (sampler, )
|
||||
|
||||
def get_custom_scheduler_defaults(self, scheduler):
|
||||
return {key: value[1] for key, value in self.custom_scheduler_defaults[scheduler].items()}
|
||||
|
||||
def get_prompt(self, name, text_encode, clip, kwargs):
|
||||
if name in kwargs and kwargs[name] is not None:
|
||||
if isinstance(kwargs[name], str):
|
||||
@@ -194,10 +129,3 @@ class SimpleSamplerCustom:
|
||||
log = f"{name} prompt cannot be {type(kwargs[name]).__name__}, it has to be either a string or conditioning."
|
||||
self.logger.log(log, "ERROR")
|
||||
raise TypeError(log)
|
||||
|
||||
def should_decode_image(self, unique_id):
|
||||
PromptServer.instance.send_sync("should_decode_image", {
|
||||
"id": unique_id,
|
||||
})
|
||||
outputs = MessageHolder.waitForMessage(unique_id)
|
||||
return outputs
|
||||
@@ -1,9 +1,30 @@
|
||||
import { SettingUtils } from './sn0w.js';
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
async function fetchExtensionSettings() {
|
||||
try {
|
||||
const response = await api.fetchApi('/extensions/ComfyUI-Sn0w-Scripts/settings/sn0w_settings.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok ' + response.statusText);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const sn0wSettings = await fetchExtensionSettings();
|
||||
const defaultValue = "ExampleName1:Value1\nExampleName2:Value2";
|
||||
const tooltip = "Enter each name-value pair on a new line, separated by a colon (:).";
|
||||
|
||||
const settingsMap = {
|
||||
"sn0w.CustomLoraLoaders1.5": "loras_15",
|
||||
"sn0w.CustomLoraLoadersXL": "loras_xl",
|
||||
"sn0w.CustomLoraLoaders3": "loras_3"
|
||||
}
|
||||
|
||||
const settingsDefinitions = [
|
||||
{
|
||||
id: "sn0w.CustomLoraLoadersXL",
|
||||
@@ -42,4 +63,8 @@ const registerSetting = (settingDefinition) => {
|
||||
};
|
||||
|
||||
// Register settings
|
||||
settingsDefinitions.forEach(registerSetting);
|
||||
settingsDefinitions.forEach(setting => {
|
||||
if (sn0wSettings["loaders_enabled"].includes(settingsMap[setting.id])) {
|
||||
registerSetting(setting);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "sn0w.SimpleKSampler",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "Sn0w KSampler") {
|
||||
const onConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
if (targetSlot === 3 || targetSlot === 4) {
|
||||
if (type == "STRING" || type == "CONDITIONING") {
|
||||
this.inputs[targetSlot].color_on = app.canvas.default_connection_color_byType[type];
|
||||
} else {
|
||||
console.error(`The input type has to be STRING or CONDITIONING, it cannot be ${type}.`)
|
||||
this.inputs[targetSlot].color_on = app.canvas.default_connection_color_byType["VAE"];
|
||||
}
|
||||
}
|
||||
|
||||
onConnectInput.apply(this, arguments);
|
||||
}
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this.inputs[3].type = ["STRING", "CONDITIONING"];
|
||||
this.inputs[4].type = ["STRING", "CONDITIONING"];
|
||||
}
|
||||
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
this.inputs[3].type = ["STRING", "CONDITIONING"];
|
||||
this.inputs[4].type = ["STRING", "CONDITIONING"];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,374 +0,0 @@
|
||||
import { SettingUtils } from './sn0w.js';
|
||||
import { widgets } from '../settings/scheduler_settings.js';
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from '../../../scripts/api.js';
|
||||
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "sn0w.SimpleSamplerCustom",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
const allSettings = new Set();
|
||||
|
||||
if (nodeData.name === "Simple Sampler Custom") {
|
||||
// Set positive and negative connection colors
|
||||
const onConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const slot = type[0]
|
||||
const inputType = type[1]
|
||||
|
||||
const initialCount = countWidgetsOfType(targetSlot.widgets, "converted-widget");
|
||||
const originalSize = [targetSlot.size[0], targetSlot.size[1]];
|
||||
|
||||
if (slot === 3 || slot === 4) {
|
||||
if (inputType == "STRING" || inputType == "CONDITIONING") {
|
||||
targetSlot.inputs[slot].color_on = app.canvas.default_connection_color_byType[inputType];
|
||||
} else {
|
||||
console.error(`The input type has to be STRING or CONDITIONING, it cannot be ${inputType}.`)
|
||||
targetSlot.inputs[slot].color_on = app.canvas.default_connection_color_byType["VAE"];
|
||||
}
|
||||
} else if (slot === 5) {
|
||||
hideAllSchedulerWidgets(targetSlot);
|
||||
SettingUtils.hideWidget(targetSlot, findWidget(targetSlot, "scheduler_name"));
|
||||
|
||||
resizeNode(targetSlot, initialCount, originalSize);
|
||||
} else if (slot === 6) {
|
||||
hideAllLatentWidgets(targetSlot);
|
||||
resizeNode(targetSlot, initialCount, originalSize);
|
||||
}
|
||||
|
||||
onConnectInput?.apply(targetSlot, type, output, originNode, originSlot);
|
||||
}
|
||||
|
||||
// Handle when connection is disconnected
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
||||
nodeType.prototype.onConnectionsChange = function (side, slot, connect, link_info, output) {
|
||||
if (slot === 5) {
|
||||
if (this.inputs[slot].link === null) {
|
||||
const initialCount = countWidgetsOfType(this.widgets, "converted-widget");
|
||||
const originalSize = [this.size[0], this.size[1]];
|
||||
|
||||
const schedulerWidget = findWidget(this, "scheduler_name");
|
||||
showSchedulerInputs(this, schedulerWidget.value);
|
||||
SettingUtils.showWidget(schedulerWidget);
|
||||
if (schedulerWidget.type === undefined)
|
||||
schedulerWidget.type = "combo";
|
||||
|
||||
resizeNode(this, initialCount, originalSize);
|
||||
}
|
||||
} else if (slot === 6) {
|
||||
const initialCount = countWidgetsOfType(this.widgets, "converted-widget");
|
||||
const originalSize = [this.size[0], this.size[1]];
|
||||
|
||||
if (this.inputs[slot].link === null)
|
||||
showAllLatentWidgets(this);
|
||||
|
||||
resizeNode(this, initialCount, originalSize);
|
||||
}
|
||||
onConnectionsChange?.apply(side, slot, connect, link_info, output);
|
||||
}
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const schedulerWidget = findWidget(this, "scheduler_name");
|
||||
schedulerWidget.value = schedulerWidget.value;
|
||||
|
||||
// Set up all inputs
|
||||
createEverything(this);
|
||||
this.inputs[3].type = ["STRING", "CONDITIONING"];
|
||||
this.inputs[4].type = ["STRING", "CONDITIONING"];
|
||||
|
||||
if (schedulerWidget && schedulerWidget.callback) {
|
||||
const originalCallback = schedulerWidget.callback;
|
||||
schedulerWidget.callback = (newValue) => {
|
||||
showSchedulerInputs(this, newValue);
|
||||
if (originalCallback) originalCallback.call(this, newValue);
|
||||
};
|
||||
}
|
||||
|
||||
api.addEventListener('get_widget_values', (event) => {
|
||||
const data = event.detail
|
||||
const output = getWidgetOutputs(this, data.widgets_needed);
|
||||
if (this.id == data.id) {
|
||||
api.fetchApi(`${SettingUtils.API_PREFIX}/widget_values`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{
|
||||
node_id: data.id,
|
||||
outputs: output,
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
api.addEventListener('should_decode_image', (event) => {
|
||||
const data = event.detail
|
||||
const output = checkImageOutput(this);
|
||||
if (this.id == data.id) {
|
||||
api.fetchApi(`${SettingUtils.API_PREFIX}/should_decode_image`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{
|
||||
node_id: data.id,
|
||||
outputs: output,
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
api.addEventListener('sampler_get_sigmas', (event) => {
|
||||
const data = event.detail;
|
||||
if (this.id == data.id) {
|
||||
const imageBase64 = SettingUtils.drawSigmas(data.sigmas);
|
||||
|
||||
// Send the generated image data back to the server
|
||||
api.fetchApi(`${SettingUtils.API_PREFIX}/get_sigmas`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{
|
||||
node_id: data.id,
|
||||
outputs: {
|
||||
output: imageBase64
|
||||
}
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
const inputName = findWidget(this, "scheduler_name");
|
||||
const desiredWidgets = widgets[inputName.value] ? Object.keys(widgets[inputName.value]) : [];
|
||||
showSchedulerInputs(this, inputName, desiredWidgets);
|
||||
|
||||
|
||||
if (this.inputs[5].link !== null) {
|
||||
hideAllSchedulerWidgets(this);
|
||||
SettingUtils.hideWidget(this, findWidget(this, "scheduler_name"));
|
||||
}
|
||||
if (this.inputs[6].link !== null) {
|
||||
hideAllLatentWidgets(this);
|
||||
}
|
||||
};
|
||||
|
||||
function findWidget(node, name) {
|
||||
const widget = node.widgets.find(widget => widget.name === name);
|
||||
return widget;
|
||||
}
|
||||
|
||||
function countWidgetsOfType(widgets, type) {
|
||||
return widgets.filter(widget => widget.type === type).length;
|
||||
}
|
||||
|
||||
function resizeNode(node, initialCount, originalSize) {
|
||||
const finalCount = countWidgetsOfType(node.widgets, "converted-widget");
|
||||
node.size[0] = originalSize[0];
|
||||
node.size[1] = originalSize[1] + (initialCount - finalCount) * (70 / 3);
|
||||
}
|
||||
|
||||
function getWidgetOutputs(node, WidgetsToGet) {
|
||||
const widgets = node.widgets;
|
||||
|
||||
// Filter out only the widgets that are in the WidgetsToGet array
|
||||
const outputWidgets = widgets.filter(widget => WidgetsToGet.includes(widget.name));
|
||||
|
||||
// Create an object with the widget names as keys and their values wrapped in an object
|
||||
return Object.fromEntries(
|
||||
outputWidgets.map(widget => [
|
||||
widget.name,
|
||||
{ value: widget.value }
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function checkImageOutput(node) {
|
||||
try {
|
||||
if (node.outputs[0].links.length === 0)
|
||||
return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createWidgetsToRemove(widgets) {
|
||||
// Use a set to avoid duplicates
|
||||
const widgetNames = new Set();
|
||||
|
||||
// Iterate over each category in the widgets object
|
||||
for (const category in widgets) {
|
||||
// Get all keys from the category and add them to the set
|
||||
Object.keys(widgets[category]).forEach(key => widgetNames.add(key));
|
||||
}
|
||||
|
||||
// Convert the set back to an array
|
||||
return Array.from(widgetNames);
|
||||
}
|
||||
|
||||
function rearrangeWidgets(node, moveWidgetsBehind, totalWidgetsToMove) {
|
||||
let widgets = node.widgets;
|
||||
|
||||
// Calculate the starting index for removing the last N widgets
|
||||
let startIndex = widgets.length - totalWidgetsToMove;
|
||||
|
||||
// Check if there are enough widgets to move
|
||||
if (startIndex < 0) {
|
||||
console.warning("Not enough widgets to move.");
|
||||
return widgets;
|
||||
}
|
||||
|
||||
// Extract the last N widgets to be moved
|
||||
let widgetsToMove = widgets.splice(startIndex, totalWidgetsToMove);
|
||||
|
||||
// Find the scheduler widget and ensure it is not duplicated
|
||||
const schedulerWidget = findWidget(node, "scheduler_name");
|
||||
|
||||
// If the schedulerWidget is found, remove existing occurrences from the current widgets array
|
||||
if (schedulerWidget) {
|
||||
widgets = widgets.filter(widget => widget !== schedulerWidget);
|
||||
}
|
||||
|
||||
// Ensure the insertion index is within the bounds of the current array
|
||||
moveWidgetsBehind = Math.max(0, Math.min(moveWidgetsBehind, widgets.length));
|
||||
|
||||
// Prepare the new widgets array by inserting schedulerWidget followed by widgetsToMove
|
||||
let newWidgets = [
|
||||
...widgets.slice(0, moveWidgetsBehind),
|
||||
schedulerWidget,
|
||||
...widgetsToMove,
|
||||
...widgets.slice(moveWidgetsBehind)
|
||||
];
|
||||
|
||||
// Update the node's widgets with the newly ordered widgets
|
||||
node.widgets = newWidgets;
|
||||
|
||||
return newWidgets;
|
||||
}
|
||||
|
||||
function showSchedulerInputs(node, schedulerName, desiredWidgets = undefined) {
|
||||
const shouldResizeNode = !desiredWidgets;
|
||||
const initialCount = countWidgetsOfType(node.widgets, "converted-widget");
|
||||
const originalSize = [node.size[0], node.size[1]];
|
||||
const originalWidgetTypes = new Map();
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
originalWidgetTypes.set(widget.name, widget.type);
|
||||
}
|
||||
|
||||
// Determine which widgets should be present based on schedulerName
|
||||
if (!desiredWidgets) {
|
||||
desiredWidgets = widgets[schedulerName] ? new Set(Object.keys(widgets[schedulerName])) : new Set();
|
||||
} else {
|
||||
desiredWidgets = new Set(desiredWidgets);
|
||||
}
|
||||
|
||||
const widgetsToRemove = createWidgetsToRemove(widgets);
|
||||
|
||||
for (const widget of widgetsToRemove) {
|
||||
if (!desiredWidgets.has(widget)) {
|
||||
SettingUtils.hideWidget(node, findWidget(node, widget));
|
||||
}
|
||||
}
|
||||
|
||||
for (const widgetName of desiredWidgets) {
|
||||
const widget = findWidget(node, widgetName);
|
||||
SettingUtils.showWidget(widget);
|
||||
if (widget.type === undefined) {
|
||||
widget.type = originalWidgetTypes.get(widget.name);
|
||||
}
|
||||
}
|
||||
|
||||
let addedWidgets = 0;
|
||||
let removedWidgets = 0;
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
if (originalWidgetTypes.get(widget.name) === "converted-widget") {
|
||||
if (widget.type !== "converted-widget")
|
||||
addedWidgets++;
|
||||
} else {
|
||||
if (widget.type === "converted-widget")
|
||||
removedWidgets++;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the size based on widgets added/removed
|
||||
if (shouldResizeNode) {
|
||||
resizeNode(node, initialCount, originalSize);
|
||||
}
|
||||
}
|
||||
|
||||
function hideAllSchedulerWidgets(node) {
|
||||
let nodesHidden = 0;
|
||||
for (const widget of node.widgets) {
|
||||
if (allSettings.has(widget.name)) {
|
||||
SettingUtils.hideWidget(node, widget);
|
||||
nodesHidden++;
|
||||
}
|
||||
}
|
||||
return nodesHidden;
|
||||
}
|
||||
|
||||
function hideAllLatentWidgets(node) {
|
||||
let heightWidget = findWidget(node, "height");
|
||||
let widthWidget = findWidget(node, "width");
|
||||
|
||||
SettingUtils.hideWidget(node, heightWidget);
|
||||
SettingUtils.hideWidget(node, widthWidget);
|
||||
|
||||
// Remove inputs if they are there
|
||||
if (node.inputs.some(obj => obj.name === "width")) {
|
||||
node.inputs = node.inputs.filter(obj => obj.name !== "width");
|
||||
}
|
||||
if (node.inputs.some(obj => obj.name === "height")) {
|
||||
node.inputs = node.inputs.filter(obj => obj.name !== "height");
|
||||
}
|
||||
}
|
||||
|
||||
function showAllLatentWidgets(node) {
|
||||
let heightWidget = findWidget(node, "height");
|
||||
let widthWidget = findWidget(node, "width");
|
||||
|
||||
// Check if the height or width is an input instead of a widget
|
||||
if (!node.inputs.some(obj => obj.name === "width")) {
|
||||
SettingUtils.showWidget(widthWidget);
|
||||
if (widthWidget.type === undefined)
|
||||
widthWidget.type = "number";
|
||||
}
|
||||
if (!node.inputs.some(obj => obj.name === "height")) {
|
||||
SettingUtils.showWidget(heightWidget);
|
||||
if (heightWidget.type === undefined)
|
||||
heightWidget.type = "number";
|
||||
}
|
||||
}
|
||||
|
||||
function createEverything(node) {
|
||||
for (const [widgetKey, widgetProps] of Object.entries(widgets)) {
|
||||
for (const [prop, [type, defaultValue, min, max, step, round]] of Object.entries(widgetProps)) {
|
||||
if (type === "FLOAT") {
|
||||
ComfyWidgets[type](node, prop, [type, { default: defaultValue, min: min, max: max, step: step, round: round }], app).widget;
|
||||
}
|
||||
allSettings.add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
const originalSize = [node.size[0], node.size[1]];
|
||||
|
||||
rearrangeWidgets(node, 7, allSettings.size);
|
||||
const nodesHidden = hideAllSchedulerWidgets(node);
|
||||
|
||||
node.size[0] = originalSize[0];
|
||||
node.size[1] = originalSize[1] + (-nodesHidden * (70 / 3));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { api } from '../../../scripts/api.js';
|
||||
|
||||
export class SettingUtils {
|
||||
static API_PREFIX = '/sn0w';
|
||||
static API_PREFIX = '/api/sn0w';
|
||||
|
||||
// SETTINGS
|
||||
static async getSetting(url) {
|
||||
|
||||
Reference in New Issue
Block a user