From 3756b9b6dba7e719607c4bc43da190fcd6b01546 Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:57:27 +0300 Subject: [PATCH] Adding preset functions to the RS Models Loader node --- Rayko_Models_Loader.py | 79 ++++++++++- Rayko_MultiLatent.py | 15 ++ __init__.py | 2 +- package.json | 2 +- pyproject.toml | 2 +- web/rayko_lora_widget.js | 286 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 358 insertions(+), 28 deletions(-) diff --git a/Rayko_Models_Loader.py b/Rayko_Models_Loader.py index 35182aa..9528ba6 100644 --- a/Rayko_Models_Loader.py +++ b/Rayko_Models_Loader.py @@ -22,6 +22,12 @@ from nodes import UNETLoader, CLIPLoader, VAELoader, LoraLoader, DualCLIPLoader from server import PromptServer import aiohttp +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +PRESETS_DIR = os.path.join(CURRENT_DIR, "preset_models") + +if not os.path.exists(PRESETS_DIR): + os.makedirs(PRESETS_DIR) + class RaykoModelsLoader: @classmethod def INPUT_TYPES(cls): @@ -29,7 +35,6 @@ class RaykoModelsLoader: clip_files = folder_paths.get_filename_list("clip") vae_files = folder_paths.get_filename_list("vae") - # Добавляем "pixel_space" в список VAE (как в нативной ноде ComfyUI) vae_files_with_pixel = vae_files + ["pixel_space"] weight_dtype_opts = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"] @@ -57,7 +62,7 @@ class RaykoModelsLoader: RETURN_NAMES = ("MODEL", "CLIP", "VAE") FUNCTION = "load_models" CATEGORY = "🦊 RaykoStudio" - DESCRIPTION = "The node combines the loaders of the model, clip, vae and lore." + DESCRIPTION = "The node combines the loaders of the model, clip, vae and lore. Presets allow quick switching between model configurations (UNET/CLIP/VAE) without affecting LoRA settings." @classmethod def IS_CHANGED(cls, lora_data="[]", **kwargs): @@ -80,7 +85,6 @@ class RaykoModelsLoader: dev = None if clip_device == "default" else clip_device - # CLIP loading logic with dual CLIP support if use_clip2 and clip_name2 and clip_name2 != "None": print(f"[Rayko] Using DualCLIPLoader mode") print(f"[Rayko] CLIP1: {clip_name}") @@ -103,7 +107,6 @@ class RaykoModelsLoader: )[0] print(f"[Rayko] CLIP loaded successfully") - # VAE loading logic with pixel_space support if vae_name == "pixel_space": print(f"[Rayko] Using pixel_space VAE (fake VAE for direct pixel manipulation)") sd = {} @@ -164,4 +167,70 @@ NODE_DISPLAY_NAME_MAPPINGS = {"RaykoModelsLoader": "🦊 RS Models Loader"} @PromptServer.instance.routes.get("/rayko/get_loras") async def get_loras(request): loras = folder_paths.get_filename_list("loras") - return aiohttp.web.json_response(sorted(loras, key=lambda x: x.lower())) \ No newline at end of file + return aiohttp.web.json_response(sorted(loras, key=lambda x: x.lower())) + +@PromptServer.instance.routes.post("/rayko_models/save_preset") +async def rayko_models_save_preset(request): + try: + data = await request.json() + name = data.get("name", "").strip() + if not name: + return aiohttp.web.Response(status=400, text="Name required") + name = "".join(c for c in name if c.isalnum() or c in " _-").strip() + if not name: + return aiohttp.web.Response(status=400, text="Invalid name") + filepath = os.path.join(PRESETS_DIR, f"{name}.json") + preset_data = { + "unet_name": data.get("unet_name", ""), + "weight_dtype": data.get("weight_dtype", "default"), + "use_clip2": data.get("use_clip2", False), + "clip_name": data.get("clip_name", ""), + "clip_name2": data.get("clip_name2", ""), + "clip_type": data.get("clip_type", "stable_diffusion"), + "clip_device": data.get("clip_device", "default"), + "vae_name": data.get("vae_name", "") + } + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, indent=2) + return aiohttp.web.Response(status=200, text="OK") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_models/list_presets") +async def rayko_models_list_presets(request): + try: + presets = [] + if os.path.exists(PRESETS_DIR): + presets = [f[:-5] for f in os.listdir(PRESETS_DIR) if f.endswith('.json')] + return aiohttp.web.json_response(sorted(presets, key=lambda x: x.lower())) + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_models/load_preset") +async def rayko_models_load_preset(request): + try: + data = await request.json() + name = data.get("name") + filepath = os.path.join(PRESETS_DIR, f"{name}.json") + if os.path.exists(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + result = json.load(f) + return aiohttp.web.json_response(result) + return aiohttp.web.Response(status=404, text="Preset not found") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) + +@PromptServer.instance.routes.post("/rayko_models/delete_preset") +async def rayko_models_delete_preset(request): + try: + data = await request.json() + name = data.get("name") + if not name: + return aiohttp.web.Response(status=400, text="Name required") + filepath = os.path.join(PRESETS_DIR, f"{name}.json") + if os.path.exists(filepath): + os.remove(filepath) + return aiohttp.web.Response(status=200, text="OK") + return aiohttp.web.Response(status=404, text="Preset not found") + except Exception as e: + return aiohttp.web.Response(status=500, text=str(e)) \ No newline at end of file diff --git a/Rayko_MultiLatent.py b/Rayko_MultiLatent.py index 1040ad4..3bc05fc 100644 --- a/Rayko_MultiLatent.py +++ b/Rayko_MultiLatent.py @@ -1,3 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025-2026 Raykosan (RaykoStudio) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import torch import math diff --git a/__init__.py b/__init__.py index 19f3e49..68a089f 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.27.0" +__version__ = "0.28.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index 7530587..179f0fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.27.0", + "version": "0.28.0", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index 28cceb3..56f8790 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.27.0" +version = "0.28.0" license = { file = "LICENSE" } dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"] diff --git a/web/rayko_lora_widget.js b/web/rayko_lora_widget.js index f6fe224..5f183e7 100644 --- a/web/rayko_lora_widget.js +++ b/web/rayko_lora_widget.js @@ -35,15 +35,12 @@ app.registerExtension({ }; } - // Find widgets for CLIP2 disabled logic const useClip2Widget = this.widgets.find(w => w.name === "use_clip2"); const clipName2Widget = this.widgets.find(w => w.name === "clip_name2"); if (useClip2Widget && clipName2Widget) { - // Set initial disabled state clipName2Widget.disabled = !useClip2Widget.value; - // Apply CSS class for visual styling if (clipName2Widget.element) { if (clipName2Widget.disabled) { clipName2Widget.element.classList.add("comfy-disabled"); @@ -52,7 +49,6 @@ app.registerExtension({ } } - // Callback when use_clip2 changes const originalCallback = useClip2Widget.callback; useClip2Widget.callback = function(value) { if (originalCallback) originalCallback(value); @@ -70,6 +66,258 @@ app.registerExtension({ this.setSize([this.targetWidth, this.size[1]]); + const collectPresetData = () => { + return { + unet_name: this.widgets.find(w => w.name === "unet_name")?.value || "", + weight_dtype: this.widgets.find(w => w.name === "weight_dtype")?.value || "default", + use_clip2: this.widgets.find(w => w.name === "use_clip2")?.value || false, + clip_name: this.widgets.find(w => w.name === "clip_name")?.value || "", + clip_name2: this.widgets.find(w => w.name === "clip_name2")?.value || "", + clip_type: this.widgets.find(w => w.name === "clip_type")?.value || "stable_diffusion", + clip_device: this.widgets.find(w => w.name === "clip_device")?.value || "default", + vae_name: this.widgets.find(w => w.name === "vae_name")?.value || "" + }; + }; + + const applyPresetData = (data) => { + const setWidgetValue = (name, value) => { + const widget = this.widgets.find(w => w.name === name); + if (widget) { + widget.value = value; + if (widget.callback) widget.callback(value); + } + }; + + setWidgetValue("unet_name", data.unet_name); + setWidgetValue("weight_dtype", data.weight_dtype); + setWidgetValue("use_clip2", data.use_clip2); + setWidgetValue("clip_name", data.clip_name); + setWidgetValue("clip_name2", data.clip_name2); + setWidgetValue("clip_type", data.clip_type); + setWidgetValue("clip_device", data.clip_device); + setWidgetValue("vae_name", data.vae_name); + + this.loraRows = []; + this.syncData(); + + this.updateUI(); + if (this.graph) this.graph.setDirtyCanvas(true, true); + }; + + const presetListOverlay = document.createElement("div"); + presetListOverlay.style.cssText = "position:fixed;display:none;flex-direction:column;max-height:300px;overflow-y:auto;background:#2a2a2a;border:1px solid #5090cc;border-radius:6px;z-index:10000;box-shadow:0 4px 12px rgba(0,0,0,0.8);min-width:250px;padding:5px;"; + document.body.appendChild(presetListOverlay); + + const presetNameInput = document.createElement("div"); + presetNameInput.style.cssText = "position:fixed;display:none;background:#2a2a2a;padding:10px;border:1px solid #5090cc;border-radius:6px;z-index:10000;box-shadow:0 4px 15px rgba(0,0,0,0.7);width:220px;text-align:center;"; + + const inputLabel = document.createElement("div"); + inputLabel.style.cssText = "color:#999;font-size:11px;margin-bottom:4px;text-align:left;"; + inputLabel.textContent = "Preset name:"; + + const inputField = document.createElement("input"); + inputField.style.cssText = "width:100%;padding:5px;background:#111;color:#fff;border:1px solid #444;border-radius:3px;margin-bottom:5px;font-size:12px;box-sizing:border-box;"; + + const inputBtns = document.createElement("div"); + inputBtns.style.cssText = "display:flex;gap:5px;"; + + const inputOk = document.createElement("button"); + inputOk.style.cssText = "flex:1;padding:4px;background:#1a3a5a;color:#aadaff;border:1px solid #5090cc;border-radius:3px;cursor:pointer;font-size:11px;"; + inputOk.textContent = "OK"; + + const inputCancel = document.createElement("button"); + inputCancel.style.cssText = "flex:1;padding:4px;background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:11px;"; + inputCancel.textContent = "Cancel"; + + inputBtns.append(inputOk, inputCancel); + presetNameInput.append(inputLabel, inputField, inputBtns); + document.body.appendChild(presetNameInput); + + const deleteConfirmOverlay = document.createElement("div"); + deleteConfirmOverlay.style.cssText = "position:fixed;display:none;background:#2a2a2a;padding:10px;border:1px solid #5090cc;border-radius:6px;z-index:10000;box-shadow:0 4px 15px rgba(0,0,0,0.7);width:220px;text-align:center;"; + + const deleteText = document.createElement("div"); + deleteText.style.cssText = "color:#ccc;font-size:12px;margin-bottom:10px;word-break:break-word;"; + + const deleteBtns = document.createElement("div"); + deleteBtns.style.cssText = "display:flex;gap:5px;"; + + const deleteOk = document.createElement("button"); + deleteOk.style.cssText = "flex:1;padding:4px;background:#1a3a5a;color:#aadaff;border:1px solid #5090cc;border-radius:3px;cursor:pointer;font-size:11px;"; + deleteOk.textContent = "OK"; + + const deleteCancel = document.createElement("button"); + deleteCancel.style.cssText = "flex:1;padding:4px;background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:11px;"; + deleteCancel.textContent = "Cancel"; + + deleteBtns.append(deleteOk, deleteCancel); + deleteConfirmOverlay.append(deleteText, deleteBtns); + document.body.appendChild(deleteConfirmOverlay); + + let pendingDeleteName = null; + + const presetsRoot = document.createElement("div"); + presetsRoot.style.cssText = "display:flex;gap:4px;width:100%;margin:0;padding:0;box-sizing:border-box;height:40px;align-items:center;"; + + const savePresetBtn = document.createElement("button"); + savePresetBtn.textContent = " Save preset"; + savePresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;height:26px;margin:0;"; + + const selectPresetBtn = document.createElement("button"); + selectPresetBtn.textContent = "📂 Select preset"; + selectPresetBtn.style.cssText = "flex:1;padding:4px 2px;font-size:11px;border:1px solid #99c0ee;border-radius:5px;background:#1a3a5a;color:#aadaff;cursor:pointer;height:26px;margin:0;"; + + presetsRoot.append(savePresetBtn, selectPresetBtn); + + savePresetBtn.addEventListener("click", (e) => { + e.stopPropagation(); + presetListOverlay.style.display = "none"; + deleteConfirmOverlay.style.display = "none"; + + const saveBtnRect = savePresetBtn.getBoundingClientRect(); + presetNameInput.style.left = saveBtnRect.left + "px"; + presetNameInput.style.top = (saveBtnRect.bottom + 5) + "px"; + presetNameInput.style.display = "block"; + inputField.value = ""; + setTimeout(() => inputField.focus(), 50); + }); + + const performSave = () => { + const name = inputField.value.trim(); + if (!name) return; + presetNameInput.style.display = "none"; + const presetData = collectPresetData(); + fetch("/rayko_models/save_preset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, ...presetData }) + }); + }; + + inputOk.addEventListener("click", performSave); + inputCancel.addEventListener("click", () => { + presetNameInput.style.display = "none"; + }); + inputField.addEventListener("keydown", (e) => { + if (e.key === "Enter") performSave(); + if (e.key === "Escape") presetNameInput.style.display = "none"; + }); + + selectPresetBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + presetNameInput.style.display = "none"; + deleteConfirmOverlay.style.display = "none"; + if (presetListOverlay.style.display === "flex") { + presetListOverlay.style.display = "none"; + return; + } + + const selectBtnRect = selectPresetBtn.getBoundingClientRect(); + presetListOverlay.style.left = selectBtnRect.left + "px"; + presetListOverlay.style.top = (selectBtnRect.bottom + 5) + "px"; + + presetListOverlay.innerHTML = "