Merge pull request #3 from sn0w12/dev

Dev
This commit is contained in:
sn0w
2024-06-26 18:03:37 +02:00
committed by GitHub
11 changed files with 220 additions and 539 deletions
+7 -4
View File
@@ -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()
+81 -34
View File
@@ -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"})
+3 -4
View File
@@ -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):
+32
View File
@@ -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
+3 -14
View File
@@ -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()
}
}
+2 -2
View File
@@ -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
+26 -1
View File
@@ -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);
}
});
+32
View File
@@ -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"];
}
}
}
});
-374
View File
@@ -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
View File
@@ -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) {