From 1a552c4c8ca53e3402b0425da066cd57d37ab52d Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Tue, 25 Jun 2024 03:04:09 +0200 Subject: [PATCH 1/9] add get_model_type_simple --- sn0w.py | 13 +++++++++++++ src/load_lora_from_folder.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sn0w.py b/sn0w.py index 1e25b22..49217a0 100644 --- a/sn0w.py +++ b/sn0w.py @@ -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 diff --git a/src/load_lora_from_folder.py b/src/load_lora_from_folder.py index 1ca83ff..7ba4ba9 100644 --- a/src/load_lora_from_folder.py +++ b/src/load_lora_from_folder.py @@ -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") From 8c3c56e83d040391845164e80fe32e0d2482f064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Tue, 25 Jun 2024 20:10:16 +0200 Subject: [PATCH 2/9] move api prefix to messageholder class --- sn0w.py | 6 ++++-- src/character_select.py | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sn0w.py b/sn0w.py index 49217a0..d36e984 100644 --- a/sn0w.py +++ b/sn0w.py @@ -218,6 +218,8 @@ class AnyType(str): class MessageHolder: messages = {} + routes = PromptServer.instance.routes + API_PREFIX = '/sn0w' @classmethod def addMessage(self, id, message): @@ -233,8 +235,8 @@ class MessageHolder: message = self.messages.pop(str(id),None) return message -routes = PromptServer.instance.routes -API_PREFIX = '/sn0w' +routes = MessageHolder.routes +API_PREFIX = MessageHolder.API_PREFIX @routes.post(f'{API_PREFIX}/textbox_string') async def handle_textbox_string(request): diff --git a/src/character_select.py b/src/character_select.py index ec32961..c1eab1e 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -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): From 4ffd42385f62ddd64525fe67e764e85a1c8431ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Tue, 25 Jun 2024 20:10:33 +0200 Subject: [PATCH 3/9] Only show needed custom lora settings --- __init__.py | 5 ++++- src/check_folder_paths.py | 33 +++++++++++++++++++++++++++++++++ web/js/customLoraLoaders.js | 27 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/check_folder_paths.py diff --git a/__init__.py b/__init__.py index 794fb71..607a48d 100644 --- a/__init__.py +++ b/__init__.py @@ -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 @@ -52,6 +53,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { WEB_DIRECTORY = "./web" +check_lora_folders() + current_unique_id = 0 # Global variable to track the unique ID logger = Logger() diff --git a/src/check_folder_paths.py b/src/check_folder_paths.py new file mode 100644 index 0000000..576cde1 --- /dev/null +++ b/src/check_folder_paths.py @@ -0,0 +1,33 @@ +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") + + print(existing_paths) + # 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 \ No newline at end of file diff --git a/web/js/customLoraLoaders.js b/web/js/customLoraLoaders.js index 5f77297..534a3e1 100644 --- a/web/js/customLoraLoaders.js +++ b/web/js/customLoraLoaders.js @@ -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); + } +}); From 224337bc8bd15edb6462cfffb7ee61eda49094fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Wed, 26 Jun 2024 03:11:23 +0200 Subject: [PATCH 4/9] try to fix simple sampler api problems --- sn0w.py | 11 +++++++--- src/character_select.py | 1 + src/check_folder_paths.py | 1 - src/simple_sampler_custom.py | 2 ++ web/js/simpleSamplerCustom.js | 39 +++++++++++++++++++++-------------- web/js/sn0w.js | 2 +- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/sn0w.py b/sn0w.py index d36e984..34b4152 100644 --- a/sn0w.py +++ b/sn0w.py @@ -219,15 +219,18 @@ class AnyType(str): class MessageHolder: messages = {} routes = PromptServer.instance.routes - API_PREFIX = '/sn0w' + API_PREFIX = '/api/sn0w' + logger = Logger() @classmethod def addMessage(self, id, message): + self.logger.log(f"Added message: {message}", "DEBUG") self.messages[str(id)] = message @classmethod def waitForMessage(self, id, period = 0.1): sid = str(id) + self.logger.log(f"Id: {sid} waiting for message", "DEBUG") while not (sid in self.messages): time.sleep(period) @@ -252,8 +255,9 @@ async def handle_update_sorting(request): @routes.post(f'{API_PREFIX}/widget_values') async def handle_widget_values(request): data = await request.json() + print(data) MessageHolder.addMessage(data.get("node_id"), data.get("outputs")) - return web.json_response({"status": "ok"}) + return web.json_response({"status": "ok"}, {"data": data}) @routes.post(f'{API_PREFIX}/should_decode_image') async def handle_should_decode_image(request): @@ -265,4 +269,5 @@ async def handle_should_decode_image(request): 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"}) \ No newline at end of file + return web.json_response({"status": "ok"}) + \ No newline at end of file diff --git a/src/character_select.py b/src/character_select.py index c1eab1e..6f95555 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -12,6 +12,7 @@ API_PREFIX = MessageHolder.API_PREFIX @routes.post(f'{API_PREFIX}/update_characters') async def handle_update_characters(request): + print("RECIEVED API REQUEST") CharacterSelectNode.initialize() return web.json_response({"status": "ok"}) diff --git a/src/check_folder_paths.py b/src/check_folder_paths.py index 576cde1..17621e0 100644 --- a/src/check_folder_paths.py +++ b/src/check_folder_paths.py @@ -24,7 +24,6 @@ def check_lora_folders(): except Exception as e: logger.log(f"{path} doesn't exist: {e}", "WARNING") - print(existing_paths) # Write the result to a JSON file in /web/settings data = {"loaders_enabled": existing_paths} with open(json_path, "w") as json_file: diff --git a/src/simple_sampler_custom.py b/src/simple_sampler_custom.py index 69587fb..c851a92 100644 --- a/src/simple_sampler_custom.py +++ b/src/simple_sampler_custom.py @@ -118,7 +118,9 @@ class SimpleSamplerCustom: "id": unique_id, "widgets_needed": widgets_needed, }) + self.logger.log(f"WAITING FOR SIGMAS", "DEBUG") outputs = MessageHolder.waitForMessage(unique_id) + self.logger.log(f"GOT SIGMAS: {outputs}", "DEBUG") return outputs def get_denoised_sigmas(self, sigmas, denoise): diff --git a/web/js/simpleSamplerCustom.js b/web/js/simpleSamplerCustom.js index 7964d64..2b24442 100644 --- a/web/js/simpleSamplerCustom.js +++ b/web/js/simpleSamplerCustom.js @@ -7,36 +7,36 @@ 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") { + const allSettings = new Set(); + console.log(nodeType.prototype) // 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]]; + const initialCount = countWidgetsOfType(this.widgets, "converted-widget"); + const originalSize = [this.size[0], this.size[1]]; if (slot === 3 || slot === 4) { if (inputType == "STRING" || inputType == "CONDITIONING") { - targetSlot.inputs[slot].color_on = app.canvas.default_connection_color_byType[inputType]; + this.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"]; + this.inputs[slot].color_on = app.canvas.default_connection_color_byType["VAE"]; } } else if (slot === 5) { - hideAllSchedulerWidgets(targetSlot); - SettingUtils.hideWidget(targetSlot, findWidget(targetSlot, "scheduler_name")); + hideAllSchedulerWidgets(this); + SettingUtils.hideWidget(this, findWidget(this, "scheduler_name")); - resizeNode(targetSlot, initialCount, originalSize); + resizeNode(this, initialCount, originalSize); } else if (slot === 6) { - hideAllLatentWidgets(targetSlot); - resizeNode(targetSlot, initialCount, originalSize); + hideAllLatentWidgets(this); + resizeNode(this, initialCount, originalSize); } - onConnectInput?.apply(targetSlot, type, output, originNode, originSlot); + onConnectInput?.apply(this, type, output, originNode, originSlot); } // Handle when connection is disconnected @@ -88,7 +88,12 @@ app.registerExtension({ const data = event.detail const output = getWidgetOutputs(this, data.widgets_needed); if (this.id == data.id) { - api.fetchApi(`${SettingUtils.API_PREFIX}/widget_values`, { + console.log(JSON.stringify( + { + node_id: data.id, + outputs: output, + })) + const apiRequest = api.fetchApi(`${SettingUtils.API_PREFIX}/widget_values`, { method: "POST", headers: { "Content-Type": "application/json", @@ -100,6 +105,7 @@ app.registerExtension({ } ), }) + console.log(apiRequest) } }) @@ -151,7 +157,6 @@ app.registerExtension({ 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")); @@ -167,8 +172,12 @@ app.registerExtension({ } function countWidgetsOfType(widgets, type) { + if (!Array.isArray(widgets)) { + console.error('Invalid widgets parameter:', widgets); + return 0; // or handle the error as needed + } return widgets.filter(widget => widget.type === type).length; - } + } function resizeNode(node, initialCount, originalSize) { const finalCount = countWidgetsOfType(node.widgets, "converted-widget"); diff --git a/web/js/sn0w.js b/web/js/sn0w.js index 8f6c8d7..103caff 100644 --- a/web/js/sn0w.js +++ b/web/js/sn0w.js @@ -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) { From a1d5796ccb09081280b9defb9f88826b424af557 Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Wed, 26 Jun 2024 17:34:30 +0200 Subject: [PATCH 5/9] Replace Simple Sampler Custom With Sn0w KSampler --- __init__.py | 6 +- sn0w.py | 70 +++- ...e_sampler_custom.py => simple_ksampler.py} | 140 ++----- web/js/simpleKSampler.js | 32 ++ web/js/simpleSamplerCustom.js | 383 ------------------ 5 files changed, 126 insertions(+), 505 deletions(-) rename src/{simple_sampler_custom.py => simple_ksampler.py} (52%) create mode 100644 web/js/simpleKSampler.js delete mode 100644 web/js/simpleSamplerCustom.js diff --git a/__init__.py b/__init__.py index 607a48d..91d6efa 100644 --- a/__init__.py +++ b/__init__.py @@ -16,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 = { @@ -31,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, } @@ -47,7 +47,7 @@ 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", } diff --git a/sn0w.py b/sn0w.py index 34b4152..62d7a00 100644 --- a/sn0w.py +++ b/sn0w.py @@ -212,31 +212,77 @@ 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 = {} + cancelled = False routes = PromptServer.instance.routes API_PREFIX = '/api/sn0w' logger = Logger() - + @classmethod - def addMessage(self, id, message): - self.logger.log(f"Added message: {message}", "DEBUG") - self.messages[str(id)] = message - + 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(self, id, period = 0.1): + def waitForMessage(cls, id, period = 0.1, asList = False): sid = str(id) - self.logger.log(f"Id: {sid} waiting for message", "DEBUG") - - while not (sid in self.messages): + while not (sid in cls.messages) and not ("-1" in cls.messages): + if cls.cancelled: + cls.cancelled = False + raise Cancelled() time.sleep(period) - - message = self.messages.pop(str(id),None) - return message + 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 diff --git a/src/simple_sampler_custom.py b/src/simple_ksampler.py similarity index 52% rename from src/simple_sampler_custom.py rename to src/simple_ksampler.py index c851a92..8163d69 100644 --- a/src/simple_sampler_custom.py +++ b/src/simple_ksampler.py @@ -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,153 +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, - }) - self.logger.log(f"WAITING FOR SIGMAS", "DEBUG") - outputs = MessageHolder.waitForMessage(unique_id) - self.logger.log(f"GOT SIGMAS: {outputs}", "DEBUG") - 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): @@ -196,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 diff --git a/web/js/simpleKSampler.js b/web/js/simpleKSampler.js new file mode 100644 index 0000000..4ca90d0 --- /dev/null +++ b/web/js/simpleKSampler.js @@ -0,0 +1,32 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "sn0w.SimpleKSampler", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Simple 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"]; + } + } + } +}); \ No newline at end of file diff --git a/web/js/simpleSamplerCustom.js b/web/js/simpleSamplerCustom.js deleted file mode 100644 index 2b24442..0000000 --- a/web/js/simpleSamplerCustom.js +++ /dev/null @@ -1,383 +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) { - if (nodeData.name === "Simple Sampler Custom") { - const allSettings = new Set(); - console.log(nodeType.prototype) - // 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(this.widgets, "converted-widget"); - const originalSize = [this.size[0], this.size[1]]; - - if (slot === 3 || slot === 4) { - if (inputType == "STRING" || inputType == "CONDITIONING") { - this.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}.`) - this.inputs[slot].color_on = app.canvas.default_connection_color_byType["VAE"]; - } - } else if (slot === 5) { - hideAllSchedulerWidgets(this); - SettingUtils.hideWidget(this, findWidget(this, "scheduler_name")); - - resizeNode(this, initialCount, originalSize); - } else if (slot === 6) { - hideAllLatentWidgets(this); - resizeNode(this, initialCount, originalSize); - } - - onConnectInput?.apply(this, 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) { - console.log(JSON.stringify( - { - node_id: data.id, - outputs: output, - })) - const apiRequest = api.fetchApi(`${SettingUtils.API_PREFIX}/widget_values`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify( - { - node_id: data.id, - outputs: output, - } - ), - }) - console.log(apiRequest) - } - }) - - 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) { - if (!Array.isArray(widgets)) { - console.error('Invalid widgets parameter:', widgets); - return 0; // or handle the error as needed - } - 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)); - } - } - }, -}); From 983fd487e925e16e4b402d584648d78f149818db Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Wed, 26 Jun 2024 17:34:35 +0200 Subject: [PATCH 6/9] Remove print --- src/character_select.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/character_select.py b/src/character_select.py index 6f95555..c1eab1e 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -12,7 +12,6 @@ API_PREFIX = MessageHolder.API_PREFIX @routes.post(f'{API_PREFIX}/update_characters') async def handle_update_characters(request): - print("RECIEVED API REQUEST") CharacterSelectNode.initialize() return web.json_response({"status": "ok"}) From a14a92fc0c996a80955be11aed7e84ebab53038b Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Wed, 26 Jun 2024 17:34:45 +0200 Subject: [PATCH 7/9] Move function to sn0w.py --- src/dynamic_scheduler_loader.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/dynamic_scheduler_loader.py b/src/dynamic_scheduler_loader.py index d2b2caf..b93f33b 100644 --- a/src/dynamic_scheduler_loader.py +++ b/src/dynamic_scheduler_loader.py @@ -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() } } From a9d9843452b14d34420ea837ef63f43b4f22400e Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Wed, 26 Jun 2024 17:37:35 +0200 Subject: [PATCH 8/9] Update name --- web/js/simpleKSampler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/simpleKSampler.js b/web/js/simpleKSampler.js index 4ca90d0..7c57f02 100644 --- a/web/js/simpleKSampler.js +++ b/web/js/simpleKSampler.js @@ -3,7 +3,7 @@ import { app } from "../../../scripts/app.js"; app.registerExtension({ name: "sn0w.SimpleKSampler", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "Simple KSampler") { + if (nodeData.name === "Sn0w KSampler") { const onConnectInput = nodeType.prototype.onConnectInput; nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { if (targetSlot === 3 || targetSlot === 4) { From 0cda8fe05b8945c7b234f02ff0dcf89af65496fd Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Wed, 26 Jun 2024 17:40:42 +0200 Subject: [PATCH 9/9] Remove unused api routes --- sn0w.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/sn0w.py b/sn0w.py index 62d7a00..7617e8f 100644 --- a/sn0w.py +++ b/sn0w.py @@ -297,23 +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() - print(data) - MessageHolder.addMessage(data.get("node_id"), data.get("outputs")) - return web.json_response({"status": "ok"}, {"data": data}) - -@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"}) \ No newline at end of file